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 */
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true,
ignorePatterns: ["babel.config.js", "jest.config.js", "node_modules/"],
rules: {
"@typescript-eslint/explicit-function-return-type": "error",
},
};
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
ignorePatterns: ["babel.config.js", "jest.config.js", "node_modules/"],
rules: {
"@typescript-eslint/explicit-function-return-type": "error"
}
};

71
App.tsx
View file

@ -1,74 +1,15 @@
import { StatusBar } from "expo-status-bar";
import { Text, View } from "react-native";
import { style } from "./src/util/style";
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 {
return (
<>
{/* Sets the status bar style */}
<StatusBar style="light" />
{/* Navigation container for the app */}
<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>
</>
<View style={style.app}>
<Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="auto" />
<PostsContainer />
</View>
);
}
// Header options for the screens
const options = {
headerStyle: style.header,
headerTitleStyle: style.headerTitle,
headerTintColor: "#fff",
};

View file

@ -7,8 +7,5 @@ npm-install:
clean:
rm -rf node_modules .expo
fmt: npm-install
npm run format
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",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},

View file

@ -1,6 +1,6 @@
module.exports = function (api) {
module.exports = function(api) {
api.cache(true);
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",
"ios": "expo start --ios",
"web": "expo start --web",
"lint": "tsc --noEmit && eslint --ext .js,.jsx,.ts,.tsx ./",
"format": "prettier --write ."
"lint": "tsc --noEmit && eslint --ext .js,.jsx,.ts,.tsx ./"
},
"dependencies": {
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
"react": "18.2.0",
"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"
"react-native": "0.72.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
@ -29,8 +21,6 @@
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "3.1.1",
"typescript": "^5.3.3"
},
"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
*/
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 (
<View style={style.postView}>
<Text style={style.postFont}>{post.id + " " + post.content}</Text>
<Text style={style.postFont}>{thisPost.content}</Text>
</View>
);
}

View file

@ -1,124 +1,66 @@
import React from "react";
import { Button, RefreshControl } from "react-native";
import { getPostsInterval, Post } from "../util/api";
import { View, Text } from "react-native";
import { getPost, getPosts, Post } from "../util/api";
import { PostView } from "./PostView";
import { style } from "../util/style";
import { SafeAreaView } from "react-native";
import { VirtualizedList } from "react-native";
const WINDOW_SIZE = 20;
type ItemData = {
id: string;
title: string;
};
/**
* PostsContainer component for displaying a list of posts.
*
* @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);
type ItemProps = {
title: string;
};
const handleEndReached = (): void => {
setOffset(offset + WINDOW_SIZE);
fetchMorePosts();
};
const Item = ({title}: ItemProps): JSX.Element => (
<View style={style.postView}>
<Text style={style.postFont}>{title}</Text>
</View>
);
/**
* Handles the pull-to-refresh action.
* @function
* @returns {void}
*/
function onRefresh(): void {
refreshPosts();
}
export function PostsContainer(): JSX.Element {
// const [posts, setPosts] = React.useState<Post[]>([]);
React.useEffect(() => {
refreshPosts();
}, []);
// React.useEffect(() => {
// refreshPosts();
// }, []);
/**
* Full refresh of posts.
* @function
* @returns {void}
*/
function refreshPosts(): void {
setRefreshing(true);
setPostData([]);
setOffset(WINDOW_SIZE);
const getItemCount = (_data: unknown): number => 50;
// Fetches posts from the API and updates the state.
async function fetchPosts(): Promise<void> {
setPostData(await getPostsInterval(0, WINDOW_SIZE));
}
// function refreshPosts(): void {
// async function fetchPosts(): Promise<void> {
// const posts = await getPosts();
// setPosts(posts);
// }
// fetchPosts();
// }
fetchPosts();
setRefreshing(false);
}
// function getItemCount(): number {
// return posts.length;
// }
/**
* Fetches more posts from the API and updates the state.
* @function
* @returns {void}
*/
function fetchMorePosts(): void {
async function fetchPosts(): Promise<void> {
setPostData([...postData, ...(await getPostsInterval(offset, WINDOW_SIZE))]);
}
const getItem = (_data: unknown, index: number): ItemData => ({
id: Math.random().toString(12).substring(0),
title: `Item ${index + 1}`,
});
fetchPosts();
}
// 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];
// const getItem = async (data: any, index: number): Promise<Post> => {
// const p = await getPost(index);
// return p;
// };
return (
<>
{/* Navigation button to create a new post */}
<NavButton onPress={() => navigation.navigate("New")} text="New Post" />
<SafeAreaView style={style.postsContainer}>
{/* VirtualizedList to render the list of posts */}
<VirtualizedList
data={postData}
initialNumToRender={4}
renderItem={({ item }) => <PostView key={item.id} post={item} />}
keyExtractor={(item: Post) => item.id}
getItemCount={getItemCount}
getItem={getItem}
onEndReached={handleEndReached}
onEndReachedThreshold={0.4}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["#007AFF"]}
/>
}
/>
</SafeAreaView>
</>
<SafeAreaView style={style.postsContainer}>
<VirtualizedList
initialNumToRender={4}
renderItem={({ item }) => <Item title={item.title} />}
keyExtractor={(item: ItemData) => item.id}
getItemCount={getItemCount}
getItem={getItem}
/>
</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;
}
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.
* @async
@ -107,7 +98,7 @@ export async function createPost(post: NewPost): Promise<void> {
export async function submitRegistration(
username: string,
password: string,
captcha: string,
captcha: string
): Promise<AuthResponse | undefined> {
const response = await fetch(URL + "/api/register", {
method: "POST",
@ -128,7 +119,7 @@ export async function submitRegistration(
*/
export async function submitLogin(
username: string,
password: string,
password: string
): Promise<AuthResponse | undefined> {
if (username == "" || password == "") return;

View file

@ -1,28 +1,28 @@
import { StyleSheet } from "react-native";
import { StyleSheet } from 'react-native';
const colorScheme = {
background: "#273043",
postBox: "#9197AE",
postFont: "#EFF6EE",
accept: "#4D8B31",
error: "#F02D3A",
};
error: "#F02D3A"
}
export const style = StyleSheet.create({
app: {
app: { // Outermost container
flex: 1,
backgroundColor: colorScheme.background, // For the "entire" app
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
margin: 0,
},
postsContainer: {
margin: 0,
height: "100%",
width: "100%",
height: '100%',
width: '100%',
padding: 5,
paddingBottom: 10,
justifyContent: "flex-start",
paddingVertical: 30,
justifyContent: 'flex-start',
backgroundColor: colorScheme.background, // For the container holding the posts
},
postView: {
@ -35,38 +35,4 @@ export const style = StyleSheet.create({
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
},
});
});

View file

@ -3,4 +3,4 @@
"compilerOptions": {
"strict": true
}
}
}