Compare commits

..

1 commit
master ... dev

Author SHA1 Message Date
Imbus
a656b409ac Style demo 2023-12-14 22:16:09 +01:00
19 changed files with 103 additions and 4452 deletions

View file

@ -1,15 +1,11 @@
/* eslint-env node */ /* eslint-env node */
module.exports = { module.exports = {
extends: [ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
"eslint:recommended", parser: '@typescript-eslint/parser',
"plugin:@typescript-eslint/recommended", plugins: ['@typescript-eslint'],
"prettier",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true, root: true,
ignorePatterns: ["babel.config.js", "jest.config.js", "node_modules/"], ignorePatterns: ["babel.config.js", "jest.config.js", "node_modules/"],
rules: { rules: {
"@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-function-return-type": "error"
}, }
}; };

71
App.tsx
View file

@ -1,74 +1,15 @@
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import { Text, View } from "react-native";
import { style } from "./src/util/style"; import { style } from "./src/util/style";
import { PostsContainer } from "./src/components/PostsContainer"; import { PostsContainer } from "./src/components/PostsContainer";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { NewPostContainer } from "./src/components/NewPostContainer";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
/**
* RootStackParamList defines the possible screens in the app.
* @typedef {object} RootStackParamList
* @property {undefined} Home - Home screen.
* @property {undefined} New - New post screen.
*/
type RootStackParamList = {
Home: undefined;
New: undefined;
};
/**
* HomeScreenNavigationProp represents the navigation prop for the Home screen.
* @typedef {object} HomeScreenNavigationProp
* @property {Function} navigate - Function to navigate to a different screen.
*/
export type HomeScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
"Home"
>;
const Stack = createNativeStackNavigator();
/**
* App function represents the main application component.
* @returns {JSX.Element} - The rendered App component.
*/
export default function App(): JSX.Element { export default function App(): JSX.Element {
return ( return (
<> <View style={style.app}>
{/* Sets the status bar style */} <Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="light" /> <StatusBar style="auto" />
<PostsContainer />
{/* Navigation container for the app */} </View>
<NavigationContainer>
{/* Stack navigator for different screens */}
<Stack.Navigator>
{/* Home screen */}
<Stack.Screen
name="Home"
component={PostsContainer}
options={options}
/>
{/* New post screen */}
<Stack.Screen
name="New"
options={options}
component={NewPostContainer}
/>
</Stack.Navigator>
</NavigationContainer>
</>
); );
} }
// Header options for the screens
const options = {
headerStyle: style.header,
headerTitleStyle: style.headerTitle,
headerTintColor: "#fff",
};

View file

@ -7,8 +7,5 @@ npm-install:
clean: clean:
rm -rf node_modules .expo rm -rf node_modules .expo
fmt: npm-install
npm run format
lint: npm-install lint: npm-install
npm run lint npm run lint

View file

@ -1,20 +0,0 @@
# FrostByte Native Client
This is a native client for the FrostByte forum. It is written in TypeScript and uses the Expo build system to build for Android and iOS.
A Justfile is included for convenience.
## Details
### The server architecture, also known as a monolith, all according to the YAGNI principle
![Boi Architecture](misc/boi_architecture.svg)
### This is a state diagram of the client application
![Homosapien stuff](misc/homosapien.svg)
## Results
Draft A | Draft B | Final Product
:----------------------------:|:----------------------------:|:---------------------------:
![Draft One](misc/draft2.png) | ![Draft Two](misc/draft.png) | ![Final Product](misc/product.png)

View file

@ -11,7 +11,9 @@
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"assetBundlePatterns": ["**/*"], "assetBundlePatterns": [
"**/*"
],
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true
}, },

View file

@ -1,6 +1,6 @@
module.exports = function (api) { module.exports = function(api) {
api.cache(true); api.cache(true);
return { return {
presets: ["babel-preset-expo"], presets: ['babel-preset-expo'],
}; };
}; };

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

4058
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,21 +7,13 @@
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web", "web": "expo start --web",
"lint": "tsc --noEmit && eslint --ext .js,.jsx,.ts,.tsx ./", "lint": "tsc --noEmit && eslint --ext .js,.jsx,.ts,.tsx ./"
"format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"expo": "~49.0.15", "expo": "~49.0.15",
"expo-status-bar": "~1.6.0", "expo-status-bar": "~1.6.0",
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.72.6", "react-native": "0.72.6"
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-web": "~0.19.6",
"react-dom": "18.2.0",
"@expo/webpack-config": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0",
@ -29,8 +21,6 @@
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "3.1.1",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"private": true "private": true

View file

@ -1,86 +0,0 @@
import React from "react";
import { View, Text, TextInput, Button } from "react-native";
import { style } from "../util/style";
import { createPost, NewPost, submitLogin } from "../util/api";
/**
* NewPostContainer component for handling new post creation.
*
* @component
* @param {object} props - React component props.
* @param {object} props.navigation - Navigation object for navigating between screens.
* @returns {JSX.Element} - Rendered NewPostContainer component.
*/
export function NewPostContainer({ navigation }): JSX.Element {
/**
* State hook for managing the current input text.
* @type {[string, React.Dispatch<React.SetStateAction<string>>]}
*/
const [currentInput, setCurrentInput] = React.useState<string>("");
/**
* State hook for managing the current input error message.
* @type {[string, React.Dispatch<React.SetStateAction<string>>]}
*/
const [currentInputError, setCurrentInputError] = React.useState<string>("");
return (
<View style={style.newPostContainer}>
{/* Input field for the new post */}
<TextInput
style={style.newPostInput}
placeholder="New Post"
onChangeText={(text) => {
setCurrentInput(text);
setCurrentInputError("");
}}
value={currentInput}
/>
{/* Button to submit the new post */}
<Button
title="Post"
onPress={() => {
/**
* Asynchronously submits the new post.
*
* @async
* @function
* @returns {Promise<void>} - A Promise that resolves when the post is successfully submitted.
*/
async function submit(): Promise<void> {
// Submit login to obtain user token
const user = (await submitLogin("demouser", "demopw")) ?? {
token: "",
};
// Create new post object
const newPost: NewPost = {
content: currentInput,
token: user.token,
};
// Call API to create the post
await createPost(newPost);
// Reset the input field
setCurrentInput("");
}
// Check if the input is not empty before submitting
if (currentInput.length > 0) {
submit();
// Navigate to the Home screen after successful post submission
navigation.navigate("Home");
} else {
// Set an error message if the input is empty
setCurrentInputError("Post must not be empty!");
}
}}
/>
{/* Display error message if there's any */}
<Text style={style.errorText}>{currentInputError}</Text>
</View>
);
}

View file

@ -10,9 +10,13 @@ import { style } from "../util/style";
* @returns {JSX.Element} The JSX for the Post * @returns {JSX.Element} The JSX for the Post
*/ */
export function PostView({ post }: { post: Post }): JSX.Element { export function PostView({ post }: { post: Post }): JSX.Element {
// WARNING THIS IS NOT ACCEPTABLE WARNING REMOVE WARNING
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [thisPost, setThisPost] = React.useState<Post>(post);
return ( return (
<View style={style.postView}> <View style={style.postView}>
<Text style={style.postFont}>{post.id + " " + post.content}</Text> <Text style={style.postFont}>{thisPost.content}</Text>
</View> </View>
); );
} }

View file

@ -1,124 +1,66 @@
import React from "react"; import React from "react";
import { Button, RefreshControl } from "react-native"; import { View, Text } from "react-native";
import { getPostsInterval, Post } from "../util/api"; import { getPost, getPosts, Post } from "../util/api";
import { PostView } from "./PostView"; import { PostView } from "./PostView";
import { style } from "../util/style"; import { style } from "../util/style";
import { SafeAreaView } from "react-native"; import { SafeAreaView } from "react-native";
import { VirtualizedList } from "react-native"; import { VirtualizedList } from "react-native";
const WINDOW_SIZE = 20; type ItemData = {
id: string;
title: string;
};
/** type ItemProps = {
* PostsContainer component for displaying a list of posts. title: string;
* };
* @component
* @param {object} props - React component props.
* @param {object} props.navigation - Navigation object for navigating between screens.
* @returns {JSX.Element} - Rendered PostsContainer component.
*/
export function PostsContainer({ navigation }): JSX.Element {
const [postData, setPostData] = React.useState<Post[]>([]);
const [offset, setOffset] = React.useState<number>(0);
const [refreshing, setRefreshing] = React.useState<boolean>(false);
const handleEndReached = (): void => { const Item = ({title}: ItemProps): JSX.Element => (
setOffset(offset + WINDOW_SIZE); <View style={style.postView}>
fetchMorePosts(); <Text style={style.postFont}>{title}</Text>
}; </View>
);
/** export function PostsContainer(): JSX.Element {
* Handles the pull-to-refresh action. // const [posts, setPosts] = React.useState<Post[]>([]);
* @function
* @returns {void}
*/
function onRefresh(): void {
refreshPosts();
}
React.useEffect(() => { // React.useEffect(() => {
refreshPosts(); // refreshPosts();
}, []); // }, []);
/** const getItemCount = (_data: unknown): number => 50;
* Full refresh of posts.
* @function
* @returns {void}
*/
function refreshPosts(): void {
setRefreshing(true);
setPostData([]);
setOffset(WINDOW_SIZE);
// Fetches posts from the API and updates the state. // function refreshPosts(): void {
async function fetchPosts(): Promise<void> { // async function fetchPosts(): Promise<void> {
setPostData(await getPostsInterval(0, WINDOW_SIZE)); // const posts = await getPosts();
} // setPosts(posts);
// }
// fetchPosts();
// }
fetchPosts(); // function getItemCount(): number {
setRefreshing(false); // return posts.length;
} // }
/** const getItem = (_data: unknown, index: number): ItemData => ({
* Fetches more posts from the API and updates the state. id: Math.random().toString(12).substring(0),
* @function title: `Item ${index + 1}`,
* @returns {void} });
*/
function fetchMorePosts(): void {
async function fetchPosts(): Promise<void> {
setPostData([...postData, ...(await getPostsInterval(offset, WINDOW_SIZE))]);
}
fetchPosts(); // const getItem = async (data: any, index: number): Promise<Post> => {
} // const p = await getPost(index);
// return p;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // };
const getItemCount = (_data: unknown): number => postData.length;
const getItem = (_data: unknown, index: number): Post => postData[index];
return ( return (
<>
{/* Navigation button to create a new post */}
<NavButton onPress={() => navigation.navigate("New")} text="New Post" />
<SafeAreaView style={style.postsContainer}> <SafeAreaView style={style.postsContainer}>
{/* VirtualizedList to render the list of posts */}
<VirtualizedList <VirtualizedList
data={postData}
initialNumToRender={4} initialNumToRender={4}
renderItem={({ item }) => <PostView key={item.id} post={item} />} renderItem={({ item }) => <Item title={item.title} />}
keyExtractor={(item: Post) => item.id} keyExtractor={(item: ItemData) => item.id}
getItemCount={getItemCount} getItemCount={getItemCount}
getItem={getItem} getItem={getItem}
onEndReached={handleEndReached}
onEndReachedThreshold={0.4}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["#007AFF"]}
/>
}
/> />
</SafeAreaView> </SafeAreaView>
</>
); );
} }
/**
* NavButton component for rendering a navigation button.
*
* @component
* @param {object} props - React component props.
* @param {Function} props.onPress - Function to be called on button press.
* @param {string} props.text - Text to be displayed on the button.
* @returns {JSX.Element} - Rendered NavButton component.
*/
function NavButton({
onPress,
text,
}: {
onPress: () => void;
text: string;
}): JSX.Element {
return <Button title={text} onPress={onPress} />;
}

View file

@ -56,15 +56,6 @@ export async function getPosts(): Promise<Post[]> {
return data; return data;
} }
export async function getPostsInterval(
offset: number,
limit: number,
): Promise<Post[]> {
const res = await fetch(URL + `/api/posts?offset=${offset}&limit=${limit}`);
const data = await res.json();
return data;
}
/** /**
* Fetches a specific post by its ID from the API. * Fetches a specific post by its ID from the API.
* @async * @async
@ -107,7 +98,7 @@ export async function createPost(post: NewPost): Promise<void> {
export async function submitRegistration( export async function submitRegistration(
username: string, username: string,
password: string, password: string,
captcha: string, captcha: string
): Promise<AuthResponse | undefined> { ): Promise<AuthResponse | undefined> {
const response = await fetch(URL + "/api/register", { const response = await fetch(URL + "/api/register", {
method: "POST", method: "POST",
@ -128,7 +119,7 @@ export async function submitRegistration(
*/ */
export async function submitLogin( export async function submitLogin(
username: string, username: string,
password: string, password: string
): Promise<AuthResponse | undefined> { ): Promise<AuthResponse | undefined> {
if (username == "" || password == "") return; if (username == "" || password == "") return;

View file

@ -1,28 +1,28 @@
import { StyleSheet } from "react-native"; import { StyleSheet } from 'react-native';
const colorScheme = { const colorScheme = {
background: "#273043", background: "#273043",
postBox: "#9197AE", postBox: "#9197AE",
postFont: "#EFF6EE", postFont: "#EFF6EE",
accept: "#4D8B31", accept: "#4D8B31",
error: "#F02D3A", error: "#F02D3A"
}; }
export const style = StyleSheet.create({ export const style = StyleSheet.create({
app: { app: { // Outermost container
flex: 1, flex: 1,
backgroundColor: colorScheme.background, // For the "entire" app backgroundColor: colorScheme.background, // For the "entire" app
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
margin: 0, margin: 0,
}, },
postsContainer: { postsContainer: {
margin: 0, margin: 0,
height: "100%", height: '100%',
width: "100%", width: '100%',
padding: 5, padding: 5,
paddingBottom: 10, paddingVertical: 30,
justifyContent: "flex-start", justifyContent: 'flex-start',
backgroundColor: colorScheme.background, // For the container holding the posts backgroundColor: colorScheme.background, // For the container holding the posts
}, },
postView: { postView: {
@ -35,38 +35,4 @@ export const style = StyleSheet.create({
postFont: { postFont: {
color: colorScheme.postFont, color: colorScheme.postFont,
}, },
header: {
backgroundColor: colorScheme.background,
},
headerTitle: {
fontSize: 20,
fontWeight: "bold",
color: colorScheme.postFont,
},
newPostContainer: {
backgroundColor: colorScheme.background,
margin: 0,
padding: 0,
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "flex-start",
},
newPostInput: {
backgroundColor: colorScheme.postBox,
borderRadius: 6,
margin : 10,
width: "90%",
minHeight: "50%",
textAlignVertical: "top",
padding: 15,
color: colorScheme.postFont,
},
errorText: {
color: colorScheme.error,
},
navButton: {
// backgroundColor: colorScheme.background,
color: colorScheme.accept
},
}); });