Compare commits

..

31 commits
dev ... master

Author SHA1 Message Date
Imbus
75d07ba6f8 Dependencies for web 2023-12-15 16:39:35 +01:00
Imbus
da95ad937a Readme update 2023-12-15 15:25:18 +01:00
Imbus
0108ff95ba Readme, additional draft 2023-12-15 15:01:15 +01:00
Imbus
b0a698ae4a Results and draft 2023-12-15 14:49:04 +01:00
Imbus
4e39ecf4e5 Reorder readme 2023-12-15 14:41:59 +01:00
Imbus
d70fe466ef Slick and trendy readme 2023-12-15 14:36:15 +01:00
Imbus
6deaa676d5 Transparent svg 2023-12-15 13:57:23 +01:00
Imbus
f03f6a2c06 Better svg 2023-12-15 13:53:31 +01:00
Imbus
aa3b5f9dfa Path fix 2023-12-15 13:48:41 +01:00
Imbus
7f14425b22 Typo 2023-12-15 13:47:10 +01:00
Imbus
3c3c393947 Readme with image 2023-12-15 13:46:23 +01:00
dDogge
26177beeab Added comments to App 2023-12-15 13:39:02 +01:00
dDogge
a4fefbe9ad Added comments to PostsContainer 2023-12-15 12:41:02 +01:00
dDogge
7b01c9e69a Added comments to NewPostContainer 2023-12-15 12:07:24 +01:00
Imbus
39833cabf2 Unique key fix 2023-12-15 03:50:23 +01:00
Imbus
53927b7cc5 Styling fixes 2023-12-15 03:17:14 +01:00
Imbus
556bf52865 So called working 2023-12-15 03:07:15 +01:00
Imbus
79328b6b62 NavButton breakout 2023-12-15 02:19:00 +01:00
Imbus
afe511b448 Slight restructure, basic navigation 2023-12-15 02:08:29 +01:00
Imbus
cdb110a19f Justfile target modifications 2023-12-15 01:58:10 +01:00
Imbus
78a3cdea40 fmt target for just 2023-12-15 01:56:19 +01:00
Imbus
a97e9779d1 Formatting entire project 2023-12-15 01:32:21 +01:00
Imbus
74a5b3930b Better route splitting 2023-12-15 01:31:02 +01:00
Imbus
1ec6a90ff9 Prettier formatter and script target 2023-12-15 01:30:54 +01:00
Imbus
72a4b26c82 New refresh logic 2023-12-15 01:24:37 +01:00
Imbus
3855a587a0 Boilerplate for NewPostContainer 2023-12-15 01:19:54 +01:00
Imbus
3e906f2c66 Initial navigation draft 2023-12-14 23:51:05 +01:00
Imbus
60c0d4f746 Correct color for navbar icons 2023-12-14 23:39:53 +01:00
Imbus
605d0990d3 Reloads now works without duplicating posts 2023-12-14 23:17:34 +01:00
Imbus
57b876bf05 Styling modifications and some formatting 2023-12-14 23:14:51 +01:00
Imbus
b59456505a Fixing infinite scroll 2023-12-14 23:14:33 +01:00
19 changed files with 4452 additions and 103 deletions

View file

@ -1,11 +1,15 @@
/* eslint-env node */
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
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"
}
"@typescript-eslint/explicit-function-return-type": "error",
},
};

71
App.tsx
View file

@ -1,15 +1,74 @@
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 (
<View style={style.app}>
<Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="auto" />
<PostsContainer />
</View>
<>
{/* 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>
</>
);
}
// Header options for the screens
const options = {
headerStyle: style.header,
headerTitleStyle: style.headerTitle,
headerTintColor: "#fff",
};

View file

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

20
README.md Normal file
View file

@ -0,0 +1,20 @@
# 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,9 +11,7 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},

View file

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

21
misc/boi_architecture.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

BIN
misc/draft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
misc/draft2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

21
misc/homosapien.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7 KiB

BIN
misc/product.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

4058
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,13 +7,21 @@
"android": "expo start --android",
"ios": "expo start --ios",
"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": {
"@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": "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": {
"@babel/core": "^7.20.0",
@ -21,6 +29,8 @@
"@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

@ -0,0 +1,86 @@
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,13 +10,9 @@ 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}>{thisPost.content}</Text>
<Text style={style.postFont}>{post.id + " " + post.content}</Text>
</View>
);
}

View file

@ -1,66 +1,124 @@
import React from "react";
import { View, Text } from "react-native";
import { getPost, getPosts, Post } from "../util/api";
import { Button, RefreshControl } from "react-native";
import { getPostsInterval, Post } from "../util/api";
import { PostView } from "./PostView";
import { style } from "../util/style";
import { SafeAreaView } from "react-native";
import { VirtualizedList } from "react-native";
type ItemData = {
id: string;
title: string;
const WINDOW_SIZE = 20;
/**
* 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);
const handleEndReached = (): void => {
setOffset(offset + WINDOW_SIZE);
fetchMorePosts();
};
type ItemProps = {
title: string;
};
/**
* Handles the pull-to-refresh action.
* @function
* @returns {void}
*/
function onRefresh(): void {
refreshPosts();
}
const Item = ({title}: ItemProps): JSX.Element => (
<View style={style.postView}>
<Text style={style.postFont}>{title}</Text>
</View>
);
React.useEffect(() => {
refreshPosts();
}, []);
export function PostsContainer(): JSX.Element {
// const [posts, setPosts] = React.useState<Post[]>([]);
/**
* Full refresh of posts.
* @function
* @returns {void}
*/
function refreshPosts(): void {
setRefreshing(true);
setPostData([]);
setOffset(WINDOW_SIZE);
// React.useEffect(() => {
// refreshPosts();
// }, []);
// Fetches posts from the API and updates the state.
async function fetchPosts(): Promise<void> {
setPostData(await getPostsInterval(0, WINDOW_SIZE));
}
const getItemCount = (_data: unknown): number => 50;
fetchPosts();
setRefreshing(false);
}
// function refreshPosts(): void {
// async function fetchPosts(): Promise<void> {
// const posts = await getPosts();
// setPosts(posts);
// }
// fetchPosts();
// }
/**
* 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))]);
}
// function getItemCount(): number {
// return posts.length;
// }
fetchPosts();
}
const getItem = (_data: unknown, index: number): ItemData => ({
id: Math.random().toString(12).substring(0),
title: `Item ${index + 1}`,
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getItemCount = (_data: unknown): number => postData.length;
// const getItem = async (data: any, index: number): Promise<Post> => {
// const p = await getPost(index);
// return p;
// };
const getItem = (_data: unknown, index: number): Post => postData[index];
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 }) => <Item title={item.title} />}
keyExtractor={(item: ItemData) => item.id}
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>
</>
);
}
/**
* 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,6 +56,15 @@ 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
@ -98,7 +107,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",
@ -119,7 +128,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: { // Outermost container
app: {
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,
paddingVertical: 30,
justifyContent: 'flex-start',
paddingBottom: 10,
justifyContent: "flex-start",
backgroundColor: colorScheme.background, // For the container holding the posts
},
postView: {
@ -35,4 +35,38 @@ 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
},
});