Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
|
a656b409ac |
19 changed files with 103 additions and 4452 deletions
|
@ -1,15 +1,11 @@
|
|||
/* eslint-env node */
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
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",
|
||||
},
|
||||
"@typescript-eslint/explicit-function-return-type": "error"
|
||||
}
|
||||
};
|
71
App.tsx
71
App.tsx
|
@ -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",
|
||||
};
|
||||
|
||||
|
|
3
Justfile
3
Justfile
|
@ -7,8 +7,5 @@ npm-install:
|
|||
clean:
|
||||
rm -rf node_modules .expo
|
||||
|
||||
fmt: npm-install
|
||||
npm run format
|
||||
|
||||
lint: npm-install
|
||||
npm run lint
|
20
README.md
20
README.md
|
@ -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)
|
4
app.json
4
app.json
|
@ -11,7 +11,9 @@
|
|||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
|
|
|
@ -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 |
BIN
misc/draft.png
BIN
misc/draft.png
Binary file not shown.
Before Width: | Height: | Size: 171 KiB |
BIN
misc/draft2.png
BIN
misc/draft2.png
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 |
BIN
misc/product.png
BIN
misc/product.png
Binary file not shown.
Before Width: | Height: | Size: 415 KiB |
4058
package-lock.json
generated
4058
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
renderItem={({ item }) => <Item title={item.title} />}
|
||||
keyExtractor={(item: ItemData) => 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} />;
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
});
|
Loading…
Reference in a new issue