Merge pull request #24 from lemeow125/feature/initial_frontend

Feature/initial frontend
This commit is contained in:
lemeow125 2023-04-17 09:30:54 +08:00 committed by GitHub
commit ce4e6735e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 6270 additions and 636 deletions

39
App.tsx
View file

@ -7,26 +7,37 @@ import DrawerScreenSettings from "./src/Components/Drawer/DrawerScreenSettings/D
import Home from "./src/Routes/Home/Home"; import Home from "./src/Routes/Home/Home";
import UserInfo from "./src/Routes/UserInfo/UserInfo"; import UserInfo from "./src/Routes/UserInfo/UserInfo";
import AddNote from "./src/Routes/AddNote/AddNote"; import NewNote from "./src/Routes/NewNote/NewNote";
import Login from "./src/Routes/Login/Login"; import Login from "./src/Routes/Login/Login";
import Register from "./src/Routes/Register/Register"; import Register from "./src/Routes/Register/Register";
import { Provider } from "react-redux";
import Store from "./src/Features/Redux/Store/Store";
import { QueryClient, QueryClientProvider } from "react-query";
import EditNote from "./src/Routes/EditNote/EditNote";
const Drawer = createDrawerNavigator(); const Drawer = createDrawerNavigator();
const queryClient = new QueryClient();
export default function App() { export default function App() {
return ( return (
<NavigationContainer> <Provider store={Store}>
<Drawer.Navigator <QueryClientProvider client={queryClient}>
initialRouteName="Home" <NavigationContainer>
drawerContent={CustomDrawerContent} <Drawer.Navigator
screenOptions={DrawerScreenSettings} initialRouteName="Home"
> drawerContent={CustomDrawerContent}
<Drawer.Screen name="Home" component={Home} /> screenOptions={DrawerScreenSettings}
<Drawer.Screen name="Add Note" component={AddNote} /> >
<Drawer.Screen name="User Info" component={UserInfo} /> <Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="Login" component={Login} /> <Drawer.Screen name="New Note" component={NewNote} />
<Drawer.Screen name="Register" component={Register} /> <Drawer.Screen name="User Info" component={UserInfo} />
</Drawer.Navigator> <Drawer.Screen name="Login" component={Login} />
</NavigationContainer> <Drawer.Screen name="Register" component={Register} />
<Drawer.Screen name="LogOut" component={Register} />
<Drawer.Screen name="EditNote" component={EditNote} />
</Drawer.Navigator>
</NavigationContainer>
</QueryClientProvider>
</Provider>
); );
} }

5236
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,21 +9,37 @@
"web": "expo start --web" "web": "expo start --web"
}, },
"dependencies": { "dependencies": {
"@expo/webpack-config": "^18.0.1",
"@react-native-async-storage/async-storage": "1.17.11",
"@react-navigation/drawer": "^6.6.2", "@react-navigation/drawer": "^6.6.2",
"@react-navigation/native": "^6.1.6", "@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"@react-navigation/stack": "^6.3.16",
"@reduxjs/toolkit": "^1.9.3",
"axios": "^1.3.4",
"expo": "~48.0.5", "expo": "~48.0.5",
"expo-linear-gradient": "~12.1.2",
"expo-status-bar": "~1.4.4", "expo-status-bar": "~1.4.4",
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.71.3", "react-dom": "18.2.0",
"react-native": "0.71.6",
"react-native-gesture-handler": "~2.9.0",
"react-native-paper": "^5.4.0",
"react-native-pell-rich-editor": "^1.8.8",
"react-native-reanimated": "~2.14.4",
"react-native-safe-area-context": "4.5.0", "react-native-safe-area-context": "4.5.0",
"react-native-screens": "~3.20.0", "react-native-screens": "~3.20.0",
"react-native-gesture-handler": "~2.9.0", "react-native-svg": "13.4.0",
"react-native-reanimated": "~2.14.4", "react-native-webview": "11.26.0",
"react-native-svg": "13.4.0" "react-native-web": "~0.18.10",
"react-query": "^3.39.3",
"react-redux": "^8.0.5",
"redux": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0",
"@types/react": "~18.0.14", "@types/react": "~18.0.14",
"@types/react-native": "^0.71.5",
"typescript": "^4.9.4" "typescript": "^4.9.4"
}, },
"private": true "private": true

142
src/Components/Api/Api.tsx Normal file
View file

@ -0,0 +1,142 @@
import axios from "axios";
import AsyncStorage from "@react-native-async-storage/async-storage";
import {
ActivationParams,
UpdateNoteParams,
AddNoteParams,
LoginParams,
RegistrationParams,
} from "../../Interfaces/Interfaces";
// Note APIs
const instance = axios.create({
baseURL: "https://keannu126.pythonanywhere.com",
});
export async function GetNotes() {
const token = JSON.parse(await AsyncStorage.getItem("token") || "{}");
return instance
.get("/api/v1/notes/", {
headers: {
Authorization: "Token " + token,
},
})
.then((response) => {
return response.data;
});
}
export function GetPublicNotes() {
return instance.get("/api/v1/public_notes/").then((response) => {
return response.data;
});
}
export async function GetNote(id: number) {
const token = JSON.parse(await AsyncStorage.getItem("token") || "{}");
return instance
.get("/api/v1/notes/" + id + "/", {
headers: {
Authorization: "Token " + token,
},
})
.then((response) => {
return response.data;
});
}
export async function UpdateNote(note: UpdateNoteParams) {
const token = JSON.parse(await AsyncStorage.getItem("token") || "{}");
return instance
.patch("/api/v1/notes/" + note.id + "/", note, {
headers: {
Authorization: "Token " + token,
},
})
.then((response) => {
return response.data;
})
.catch((error) => {
return error;
});
}
export async function AddNote(note: AddNoteParams) {
const token = JSON.parse(await AsyncStorage.getItem("token") || "{}");
return instance
.post("/api/v1/notes/", note, {
headers: {
Authorization: "Token " + token,
},
})
.then((response) => {
return response.data;
})
.catch((error) => {
return error;
});
}
export async function DeleteNote(id: number) {
const token = JSON.parse(await AsyncStorage.getItem("token") || "{}");
return instance
.delete("/api/v1/notes/" + id + "/", {
headers: {
Authorization: "Token " + token,
},
})
.catch((error) => {
return error;
});
}
// User APIs
export function UserRegister(register: RegistrationParams) {
return instance
.post("/api/v1/accounts/users/", register)
.then(async (response) => {
return true;
})
.catch((error) => {
return false;
});
}
export function UserLogin(user: LoginParams) {
return instance
.post("/api/v1/accounts/token/login/", user)
.then(async (response) => {
AsyncStorage.setItem("token", JSON.stringify(response.data.auth_token));
return true;
})
.catch((error) => {
console.log("Login Failed: " + error);
return false;
});
}
export async function UserInfo() {
const token = JSON.parse(await AsyncStorage.getItem("token") || "{}");
return instance
.get("/api/v1/accounts/users/me/", {
headers: {
Authorization: "Token " + token,
},
})
.then((response) => {
return response.data;
});
}
export function UserActivate(activation: ActivationParams) {
return instance
.post("/api/v1/accounts/users/activation/", activation)
.then(async (response) => {
return true;
})
.catch((error) => {
return false;
});
}

View file

@ -2,12 +2,14 @@ import * as React from "react";
import { View } from "react-native"; import { View } from "react-native";
import styles from "../../styles"; import styles from "../../styles";
export interface props { export interface props {
children: React.ReactNode; children: React.ReactNode;
} }
export default function Background(props: props) { export default function Background(props: props) {
return ( return (
<View style={styles.background}> <View style={styles.background}>
<View style={{ margin: 8 }} /> <View style={{ margin: 8 }} />
{props.children} {props.children}

View file

@ -13,79 +13,141 @@ import LoginIcon from "../../../Icons/LoginIcon/LoginIcon";
import SignupIcon from "../../../Icons/SignupIcon/SignupIcon"; import SignupIcon from "../../../Icons/SignupIcon/SignupIcon";
import UserIcon from "../../../Icons/UserIcon/UserIcon"; import UserIcon from "../../../Icons/UserIcon/UserIcon";
import AppIcon from "../../../Icons/AppIcon/AppIcon"; import AppIcon from "../../../Icons/AppIcon/AppIcon";
import LogoutIcon from "../../../Icons/LogoutIcon/LogoutIcon";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../../Features/Redux/Store/Store";
import { Toggle_Login } from "../../../../Features/Redux/Slices/LoginSlice/LoginSlice";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function CustomDrawerContent(props: {}) { export default function CustomDrawerContent(props: {}) {
const navigation = useNavigation<RootDrawerParamList>(); const navigation = useNavigation<RootDrawerParamList>();
const dispatch = useDispatch();
const width = 224; const width = 224;
return ( const logged_in = useSelector((state: RootState) => state.logged_in.value);
<DrawerContentScrollView {...props}> const logged_in_user = useSelector(
<View (state: RootState) => state.logged_in_user.value
style={{
...styles.flex_row,
...{ justifyContent: "center" },
}}
>
<AppIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Clip Notes
</Text>
</View>
<ButtonAlignLeft
color="Blue"
width={width}
onPress={() => {
navigation.navigate("Home");
}}
>
<HomeIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>Home</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Green"
width={width}
onPress={() => {
navigation.navigate("Add Note");
}}
>
<AddIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Add Note
</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Green"
width={width}
onPress={() => {
navigation.navigate("Login");
}}
>
<LoginIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>Login</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Yellow"
width={width}
onPress={() => {
navigation.navigate("User Info");
}}
>
<UserIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
User Info
</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Yellow"
width={width}
onPress={() => {
navigation.navigate("Register");
}}
>
<SignupIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Register
</Text>
</ButtonAlignLeft>
</DrawerContentScrollView>
); );
if (logged_in) {
return (
<DrawerContentScrollView {...props}>
<View
style={{
...styles.flex_row,
...{ justifyContent: "center" },
}}
>
<AppIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Clip Notes
</Text>
</View>
<Text style={{ ...styles.text_white, ...{ fontSize: 16 } }}>
Logged in as {logged_in_user.username}
</Text>
<ButtonAlignLeft
color="Blue"
width={width}
onPress={() => {
navigation.navigate("Home");
}}
>
<HomeIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Home
</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Green"
width={width}
onPress={() => {
navigation.navigate("New Note");
}}
>
<AddIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
New Note
</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Yellow"
width={width}
onPress={() => {
navigation.navigate("User Info");
}}
>
<UserIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
User Info
</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Red"
width={width}
onPress={() => {
dispatch(Toggle_Login());
AsyncStorage.removeItem("token");
}}
>
<LogoutIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Log Out
</Text>
</ButtonAlignLeft>
</DrawerContentScrollView>
);
} else {
return (
<DrawerContentScrollView {...props}>
<View
style={{
...styles.flex_row,
...{ justifyContent: "center" },
}}
>
<AppIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Clip Notes
</Text>
</View>
<ButtonAlignLeft
color="Blue"
width={width}
onPress={() => {
navigation.navigate("Home");
}}
>
<HomeIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Home
</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Green"
width={width}
onPress={() => {
navigation.navigate("Login");
}}
>
<LoginIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Login
</Text>
</ButtonAlignLeft>
<ButtonAlignLeft
color="Yellow"
width={width}
onPress={() => {
navigation.navigate("Register");
}}
>
<SignupIcon size={32} color="white" />
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Register
</Text>
</ButtonAlignLeft>
</DrawerContentScrollView>
);
}
} }

View file

@ -1,6 +1,7 @@
import { View, Image, Text } from "react-native"; import { View, Image, Text } from "react-native";
import type { DrawerNavigationOptions } from "@react-navigation/drawer"; import type { DrawerNavigationOptions } from "@react-navigation/drawer";
import AppIcon from "../../Icons/AppIcon/AppIcon"; import AppIcon from "../../Icons/AppIcon/AppIcon";
import PreviousSessionChecker from "../../PreviousSessionChecker/PreviousSessionChecker";
const DrawerScreenSettings: DrawerNavigationOptions = { const DrawerScreenSettings: DrawerNavigationOptions = {
headerTitleStyle: { color: "white", fontSize: 26 }, headerTitleStyle: { color: "white", fontSize: 26 },
unmountOnBlur: true, unmountOnBlur: true,
@ -18,6 +19,7 @@ const DrawerScreenSettings: DrawerNavigationOptions = {
<View <View
style={{ flexDirection: "row", marginRight: 16, alignItems: "center" }} style={{ flexDirection: "row", marginRight: 16, alignItems: "center" }}
> >
<PreviousSessionChecker />
<AppIcon size={32} color="white" /> <AppIcon size={32} color="white" />
</View> </View>
), ),

View file

@ -0,0 +1,25 @@
import * as React from "react";
import { IconProps } from "../../../Interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
export default function LogoutIcon(props: IconProps) {
return (
<>
<Svg
width={props.size}
height={props.size}
viewBox="0 0 24 24"
strokeWidth="2"
stroke={props.color}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></Path>
<Path d="M7 12h14l-3 -3m0 6l3 -3"></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,63 @@
import * as React from "react";
import { View, Text, TextInput, ScrollView } from "react-native";
import styles from "../../styles";
import { NoteProps, RootDrawerParamList } from "../../Interfaces/Interfaces";
import ButtonCentered from "../Buttons/ButtonCentered/ButtonCentered";
import { useQueryClient, useMutation } from "react-query";
import { DeleteNote } from "../Api/Api";
import { useNavigation } from "@react-navigation/native";
export default function Note(props: NoteProps) {
const navigation = useNavigation<RootDrawerParamList>();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: DeleteNote,
onSuccess: () => {
queryClient.invalidateQueries("notes");
queryClient.invalidateQueries("public_notes");
},
});
return (
<View style={styles.addnotecont}>
<View style={styles.tle}>
<TextInput
style={styles.title}
value={props.title}
maxLength={20}
editable={false}
/>
</View>
<View style={styles.typehere}>
<ScrollView style={styles.typeinput} nestedScrollEnabled={true}>
<Text style={styles.typeinput}>{props.content}</Text>
</ScrollView>
</View>
<View style={styles.flex_row}>
<ButtonCentered
color={"Red"}
onPress={() => {
console.log("Deleted note id " + props.id);
mutation.mutate(props.id);
}}
width={64}
>
<Text style={{ ...styles.text_white, ...{ fontSize: 16 } }}>
Delete Note
</Text>
</ButtonCentered>
<ButtonCentered
color={"Yellow"}
onPress={() => {
navigation.navigate("EditNote", { noteId: props.id });
console.log("Editing note id " + props.id);
}}
width={64}
>
<Text style={{ ...styles.text_white, ...{ fontSize: 16 } }}>
Edit Note
</Text>
</ButtonCentered>
</View>
</View>
);
}

View file

@ -0,0 +1,90 @@
import * as React from "react";
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
import { useQuery } from "react-query";
import { GetNotes } from "../Api/Api";
import styles from "../../styles";
import { useSelector } from "react-redux";
import { RootState } from "../../Features/Redux/Store/Store";
import { NoteProps, RootDrawerParamList } from "../../Interfaces/Interfaces";
import { useNavigation } from "@react-navigation/native";
import Note from "../Note/Note";
export default function Notes() {
const navigation = useNavigation<RootDrawerParamList>();
const {
data: notes,
isLoading,
error,
} = useQuery("notes", GetNotes, { retry: 0 });
const logged_in = useSelector((state: RootState) => state.logged_in.value);
if (isLoading) {
return (
<Text style={{ ...styles.no, ...{ textAlign: "center" } }}>
Loading notes...
</Text>
);
} else if (!logged_in && error) {
return (
<Text style={{ ...styles.no, ...{ textAlign: "center" } }}>
Please login to use Clip Notes
</Text>
);
} else if (error) {
return (
<Text style={{ ...styles.no, ...{ textAlign: "center", color: "red" } }}>
Error contacting Notes server
</Text>
);
} else if (notes.length === 0) {
return (
<View>
<Text style={{ ...styles.no, ...{ textAlign: "center" } }}>
No notes exist yet. Create one!
</Text>
<TouchableOpacity
style={{
...styles.homecont,
...{ display: "flex", alignSelf: "center" },
}}
onPress={() => {
navigation.navigate("New Note");
}}
>
<Text style={styles.newnote}>+</Text>
<Text style={styles.no}>New note...</Text>
</TouchableOpacity>
</View>
);
}
return (
<ScrollView contentContainerStyle={{ justifyContent: "center" }}>
{notes.map((note: NoteProps, index: number) => {
return (
<Note
id={note.id}
key={index}
title={note.title}
content={note.content}
date_created={note.date_created}
owner={note.owner}
public={note.public}
/>
);
})}
<TouchableOpacity
style={{
...styles.homecont,
...{ display: "flex", alignSelf: "center" },
}}
onPress={() => {
navigation.navigate("New Note");
}}
>
<Text style={styles.newnote}>+</Text>
<Text style={styles.no}>New note...</Text>
</TouchableOpacity>
<View style={{ margin: 16 }} />
</ScrollView>
);
}

View file

@ -0,0 +1,33 @@
import * as React from "react";
import { View, Text, TextInput, ScrollView } from "react-native";
import { useEffect, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { UserInfo } from "../Api/Api";
import { Toggle_Login } from "../../Features/Redux/Slices/LoginSlice/LoginSlice";
import { SetUser } from "../../Features/Redux/Slices/LoggedInUserSlice/LoggedInUserSlice";
import { set_checked } from "../../Features/Redux/Slices/OldSession/OldSessionSlice";
import { RootState } from "../../Features/Redux/Store/Store";
export default function PreviousSessionChecker() {
const dispatch = useDispatch();
const logged_in = useSelector((state: RootState) => state.logged_in.value);
// Function to check for previous login session
const check = useCallback(async () => {
if (await UserInfo()) {
if (logged_in !== true) {
console.log("Previous session found. Restoring");
await dispatch(Toggle_Login());
await dispatch(SetUser(await UserInfo()));
}
} else {
console.log("No old session found");
localStorage.removeItem("token");
}
await dispatch(set_checked());
}, [dispatch, logged_in]);
useEffect(() => {
if (!logged_in) {
check();
}
}, [check, logged_in]);
return <View />;
}

View file

@ -0,0 +1,24 @@
import * as React from "react";
import { View, Text, TextInput, ScrollView } from "react-native";
import styles from "../../styles";
import { NoteProps } from "../../Interfaces/Interfaces";
export default function PublicNote(props: NoteProps) {
return (
<View style={styles.addnotecont}>
<View style={styles.tle}>
<TextInput
style={styles.title}
value={props.title}
maxLength={20}
editable={false}
/>
</View>
<View style={styles.typehere}>
<ScrollView style={styles.typeinput} nestedScrollEnabled={true}>
<Text style={styles.typeinput}>{props.content}</Text>
</ScrollView>
</View>
</View>
);
}

View file

@ -0,0 +1,56 @@
import * as React from "react";
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
import { useQuery } from "react-query";
import { GetNotes, GetPublicNotes } from "../Api/Api";
import styles from "../../styles";
import { useSelector } from "react-redux";
import { RootState } from "../../Features/Redux/Store/Store";
import { NoteProps, RootDrawerParamList } from "../../Interfaces/Interfaces";
import { useNavigation } from "@react-navigation/native";
import Note from "../Note/Note";
import PublicNote from "../PublicNote/PublicNote";
export default function PublicNotes() {
const navigation = useNavigation<RootDrawerParamList>();
const {
data: notes,
isLoading,
error,
} = useQuery("public_notes", GetPublicNotes, { retry: 0 });
if (isLoading) {
return (
<Text style={{ ...styles.no, ...{ textAlign: "center" } }}>
Loading public notes...
</Text>
);
} else if (error) {
return (
<Text style={{ ...styles.no, ...{ textAlign: "center", color: "red" } }}>
Error contacting Notes server
</Text>
);
} else if (notes.length === 0) {
return (
<Text style={{ ...styles.no, ...{ textAlign: "center" } }}>
There are no public notes...
</Text>
);
}
return (
<ScrollView contentContainerStyle={{ justifyContent: "center" }}>
{notes.map((note: NoteProps, index: number) => {
return (
<PublicNote
id={note.id}
key={index}
title={note.title}
content={note.content}
date_created={note.date_created}
owner={note.owner}
public={note.public}
/>
);
})}
</ScrollView>
);
}

View file

@ -0,0 +1,33 @@
import { createSlice } from "@reduxjs/toolkit";
export const LoggedInUserSlice = createSlice({
name: "Login",
initialState: {
value: {
email: "",
id: 0,
username: "",
},
},
reducers: {
SetUser: (state, action) => {
state.value = {
email: action.payload.email,
id: action.payload.id,
username: action.payload.username,
};
},
UnsetUser: (state) => {
state.value = {
email: "",
id: 0,
username: "",
};
},
},
});
// Action creators are generated for each case reducer function
export const { SetUser, UnsetUser } = LoggedInUserSlice.actions;
export default LoggedInUserSlice.reducer;

View file

@ -0,0 +1,18 @@
import { createSlice } from "@reduxjs/toolkit";
export const LoginSlice = createSlice({
name: "Login",
initialState: {
value: false,
},
reducers: {
Toggle_Login: (state) => {
state.value = !state.value;
},
},
});
// Action creators are generated for each case reducer function
export const { Toggle_Login } = LoginSlice.actions;
export default LoginSlice.reducer;

View file

@ -0,0 +1,17 @@
import { createSlice } from "@reduxjs/toolkit";
export const OldSessionSlice = createSlice({
name: "old_session_checked",
initialState: {
value: false,
},
reducers: {
set_checked: (state) => {
state.value = !state.value;
},
},
});
export const { set_checked } = OldSessionSlice.actions;
export default OldSessionSlice.reducer;

View file

@ -0,0 +1,18 @@
import { configureStore } from "@reduxjs/toolkit";
import LoginReducer from "../Slices/LoginSlice/LoginSlice";
import LoggedInUserReucer from "../Slices/LoggedInUserSlice/LoggedInUserSlice";
import OldSessionReducer from "../Slices/OldSession/OldSessionSlice";
const store = configureStore({
reducer: {
logged_in: LoginReducer,
logged_in_user: LoggedInUserReucer,
old_session_checked: OldSessionReducer,
},
});
export default store;
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View file

@ -6,3 +6,65 @@ export interface IconProps {
export interface RootDrawerParamList { export interface RootDrawerParamList {
navigate: any; navigate: any;
} }
// Redux Interfaces
export interface LoginState {
Login: { logged_in: boolean };
}
export interface LoggedInUserState {
LoggedInUser: {
value: {
email: string;
id: number;
username: string;
};
};
}
// Component Props Interfaces
export interface NoteProps {
title: string;
content: string;
id: number;
date_created: Date;
owner: string;
public: boolean;
}
export interface IconProps {
size: number;
color: string;
}
// API Interfaces
export interface RegistrationParams {
email: string;
username: string;
password: string;
}
export interface LoginParams {
username: string;
password: string;
}
export interface ActivationParams {
uid: string;
token: string;
}
export interface AddNoteParams {
title: string;
content: string;
public: boolean;
}
export interface UpdateNoteParams {
id: number;
title: string;
content: string;
public: boolean;
}

View file

@ -1,12 +0,0 @@
import * as React from 'react';
import {View, Text} from 'react-native';
import styles from '../../styles';
import Background from '../../Components/Background/Background';
export default function AddNote() {
return (
<Background>
<Text style={{...styles.text_white, ...{fontSize: 32}}}>Add Note</Text>
</Background>
);
}

View file

@ -0,0 +1,135 @@
import * as React from "react";
import { View, Text, TextInput, Switch } from "react-native";
import styles from "../../styles";
import Background from "../../Components/Background/Background";
import { SafeAreaView } from "react-native-safe-area-context";
import { useEffect, useState } from "react";
import { TouchableOpacity } from "react-native";
import { RootDrawerParamList } from "../../Interfaces/Interfaces";
import { useNavigation } from "@react-navigation/native";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { AddNote, GetNote, UpdateNote } from "../../Components/Api/Api";
export default function EditNote({ navigation, route }: any) {
const { noteId } = route.params;
const [note, setNote] = useState({
title: "",
content: "",
public: false,
});
async function retrieve() {
let a = await GetNote(noteId);
setNote(a);
return a;
}
const { data, isLoading, error } = useQuery("note", retrieve, {
retry: 0,
});
useEffect(() => {
setNote(data);
}, [data]);
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: UpdateNote,
onSuccess: () => {
queryClient.invalidateQueries("notes");
queryClient.invalidateQueries("public_notes");
},
});
if (error) {
return (
<Background>
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Error retrieving specific note
</Text>
</Background>
);
}
if (isLoading) {
return (
<Background>
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Loading note...
</Text>
</Background>
);
}
if (data && note) {
return (
<Background>
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
Edit Note
</Text>
<View style={styles.addnotecont}>
<View style={styles.tle}>
<TextInput
style={styles.title}
placeholder="Title"
placeholderTextColor="white"
value={note.title}
onChangeText={(text) => {
setNote({ ...note, title: text });
}}
maxLength={20}
/>
</View>
<View style={styles.typehere}>
<TextInput
style={styles.typeinput}
placeholder="Type here...."
placeholderTextColor="white"
value={note.content}
multiline={true}
onChangeText={async (text) => {
await setNote({ ...note, content: text });
}}
/>
</View>
<View
style={{
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
marginLeft: 16,
alignItems: "center",
}}
>
<Switch
onValueChange={() => setNote({ ...note, public: !note.public })}
value={note.public}
/>
<Text style={{ ...styles.text_white, ...{ fontSize: 16 } }}>
Public?
</Text>
</View>
<TouchableOpacity
style={styles.savebtn}
onPress={async () => {
try {
await mutation.mutate({
title: note.title,
content: note.content,
public: note.public,
id: noteId,
});
navigation.navigate("Home");
} catch (error) {}
console.log(note.content);
}}
>
<Text style={styles.savenote}>SAVE</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cancelbtn}
onPress={() => {
navigation.navigate("Home");
}}
>
<Text style={styles.cancel}>CANCEL</Text>
</TouchableOpacity>
</View>
</Background>
);
}
}

View file

@ -1,25 +1,52 @@
import * as React from 'react'; import * as React from "react";
import {View, Text} from 'react-native'; import { View, Text } from "react-native";
import styles from '../../styles'; import styles from "../../styles";
import Background from '../../Components/Background/Background'; import Background from "../../Components/Background/Background";
import AppIcon from '../../Components/Icons/AppIcon/AppIcon'; import Notes from "../../Components/Notes/Notes";
import { Switch } from "react-native-gesture-handler";
import { useState } from "react";
import PublicNotes from "../../Components/PublicNotes/Notes";
export default function Home() { export default function Home() {
const [switchLabel, setLabel] = useState("Viewing public notes");
const [togglePublic, setToggled] = useState(true);
function Preview() {
if (togglePublic) {
return <PublicNotes />;
} else {
return <Notes />;
}
}
return ( return (
<Background> <Background>
<Text style={{ ...styles.text_white, ...{ fontSize: 25 } }}>
Clip Notes
</Text>
<View <View
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', justifyContent: "flex-start",
justifyContent: 'center', marginLeft: 16,
}}> alignItems: "center",
<AppIcon size={64} color="white" /> }}
<Text style={{...styles.text_white, ...{fontSize: 32}}}> >
Clip Notes <Switch
onValueChange={() => {
setToggled(!togglePublic);
if (togglePublic) {
setLabel("Viewing own notes");
} else {
setLabel("Viewing public notes");
}
}}
value={togglePublic}
/>
<Text style={{ ...styles.text_white, ...{ fontSize: 16 } }}>
{switchLabel}
</Text> </Text>
</View> </View>
<View style={{margin: 16}} /> <Preview />
</Background> </Background>
); );
} }

View file

@ -1,12 +1,100 @@
import * as React from 'react'; import * as React from "react";
import {View, Text} from 'react-native'; import { View, Text, TextInput } from "react-native";
import styles from '../../styles'; import styles from "../../styles";
import Background from '../../Components/Background/Background'; import Background from "../../Components/Background/Background";
import {
SafeAreaView,
NativeSyntheticEvent,
TextInputChangeEventData,
} from "react-native";
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import { TouchableOpacity } from "react-native";
import {
NavigationHelpersContext,
useNavigation,
} from "@react-navigation/native";
import { useDispatch } from "react-redux";
import { SetUser } from "../../Features/Redux/Slices/LoggedInUserSlice/LoggedInUserSlice";
import { Toggle_Login } from "../../Features/Redux/Slices/LoginSlice/LoginSlice";
import { UserInfo, UserLogin } from "../../Components/Api/Api";
import { RootDrawerParamList } from "../../Interfaces/Interfaces";
export default function Login() { export default function Login() {
const navigation = useNavigation<RootDrawerParamList>();
const dispatch = useDispatch();
const [user, setUser] = useState({
username: "",
password: "",
});
const [error, setError] = useState("");
return ( return (
<Background> <Background>
<Text style={{...styles.text_white, ...{fontSize: 32}}}>Login</Text> <Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}></Text>
<SafeAreaView>
<View style={styles.container}>
<Text style={styles.loginlabel}>Login to Clip Notes</Text>
<StatusBar style="auto" />
<View style={styles.inputView}>
<TextInput
style={styles.TextInput}
placeholder="Username"
placeholderTextColor="white"
value={user.username}
maxLength={20}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, username: e.nativeEvent.text });
console.log(user.username);
}}
/>
</View>
<View style={styles.inputView}>
<TextInput
style={styles.TextInput}
placeholder="Password"
placeholderTextColor="white"
secureTextEntry={true}
value={user.password}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, password: e.nativeEvent.text });
console.log(user.password);
}}
/>
</View>
<TouchableOpacity
onPress={async () => {
setUser({
username: "",
password: "",
});
if (await UserLogin(user)) {
await dispatch(Toggle_Login());
await dispatch(SetUser(await UserInfo()));
navigation.navigate("Home");
} else {
setError("Invalid Login");
}
}}
style={styles.loginBtn}
>
<Text style={styles.loginText}>LOGIN</Text>
</TouchableOpacity>
<Text style={styles.text_small_red}>{error}</Text>
<TouchableOpacity
onPress={() => {
navigation.navigate("Register");
}}
style={styles.registerbtn}
>
<Text style={styles.registertext}>REGISTER</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</Background> </Background>
); );
} }

View file

@ -0,0 +1,107 @@
import * as React from "react";
import { View, Text, TextInput, Switch } from "react-native";
import styles from "../../styles";
import Background from "../../Components/Background/Background";
import { SafeAreaView } from "react-native-safe-area-context";
import { useState } from "react";
import { TouchableOpacity } from "react-native";
import { RootDrawerParamList } from "../../Interfaces/Interfaces";
import { useNavigation } from "@react-navigation/native";
import { useMutation, useQueryClient } from "react-query";
import { AddNote } from "../../Components/Api/Api";
export default function NewNote() {
const [note, setNote] = useState({
title: "",
content: "",
public: false,
});
const navigation = useNavigation<RootDrawerParamList>();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: AddNote,
onSuccess: () => {
queryClient.invalidateQueries("notes");
queryClient.invalidateQueries("public_notes");
},
});
return (
<Background>
<Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}>
New Note
</Text>
<SafeAreaView>
<View style={styles.addnotecont}>
<View style={styles.tle}>
<TextInput
style={styles.title}
placeholder="Title"
placeholderTextColor="white"
value={note.title}
onChangeText={(text) => {
setNote({ ...note, title: text });
}}
maxLength={20}
/>
</View>
<View style={styles.typehere}>
<TextInput
style={styles.typeinput}
placeholder="Type here...."
placeholderTextColor="white"
value={note.content}
multiline={true}
onChangeText={async (text) => {
await setNote({ ...note, content: text });
}}
/>
</View>
<View
style={{
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
marginLeft: 16,
alignItems: "center",
}}
>
<Switch
onValueChange={() => setNote({ ...note, public: !note.public })}
value={note.public}
/>
<Text style={{ ...styles.text_white, ...{ fontSize: 16 } }}>
Public?
</Text>
</View>
<TouchableOpacity
style={styles.savebtn}
onPress={async () => {
try {
await mutation.mutate({
title: note.title,
content: note.content,
public: note.public,
});
navigation.navigate("Home");
} catch (error) {}
console.log(note.content);
}}
>
<Text style={styles.savenote}>SAVE</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cancelbtn}
onPress={() => {
navigation.navigate("Home");
}}
>
<Text style={styles.cancel}>CANCEL</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</Background>
);
}

View file

@ -1,12 +1,73 @@
import * as React from 'react'; import * as React from "react";
import {View, Text} from 'react-native'; import { View, Text, TextInput } from "react-native";
import styles from '../../styles'; import styles from "../../styles";
import Background from '../../Components/Background/Background'; import Background from "../../Components/Background/Background";
import { SafeAreaView } from "react-native-safe-area-context";
import { useState } from "react";
import { TouchableOpacity } from "react-native";
import { UserRegister } from "../../Components/Api/Api";
export default function Register() { export default function Register() {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [feedback, setFeedback] = useState("");
return ( return (
<Background> <Background>
<Text style={{...styles.text_white, ...{fontSize: 32}}}>Register</Text> <Text style={{ ...styles.text_white, ...{ fontSize: 32 } }}></Text>
<SafeAreaView>
<View style={styles.container}>
<Text style={styles.loginlabel}>Create an Account</Text>
<View style={styles.inputView}>
<TextInput
style={styles.TextInput}
placeholder="Email"
placeholderTextColor="white"
onChangeText={setEmail}
value={email}
/>
</View>
<View style={styles.inputView}>
<TextInput
style={styles.TextInput}
placeholder="Username"
placeholderTextColor="white"
onChangeText={setUsername}
value={username}
/>
</View>
<View style={styles.inputView}>
<TextInput
style={styles.TextInput}
placeholder="Password"
placeholderTextColor="white"
secureTextEntry={true}
onChangeText={setPassword}
value={password}
/>
</View>
<TouchableOpacity
onPress={async () => {
setUsername("");
setPassword("");
setEmail("");
console.log("heh");
if (await UserRegister({ username, email, password })) {
setFeedback(
"Registration success. Please check your email address for activation"
);
} else {
setFeedback("Invalid credentials specified");
}
}}
style={styles.registerbtn}
>
<Text style={styles.registertext}>REGISTER</Text>
</TouchableOpacity>
<Text style={styles.registertext}>{feedback}</Text>
</View>
</SafeAreaView>
</Background> </Background>
); );
} }

View file

@ -1,12 +1,53 @@
import * as React from 'react'; import * as React from "react";
import {View, Text} from 'react-native'; import { View, Text } from "react-native";
import styles from '../../styles'; import styles from "../../styles";
import Background from '../../Components/Background/Background'; import Background from "../../Components/Background/Background";
import { SafeAreaView } from "react-native-safe-area-context";
import { UserInfo } from "../../Components/Api/Api";
import { useQuery } from "react-query";
import { useSelector } from "react-redux";
import { RootState } from "../../Features/Redux/Store/Store";
export default function UserInfo() { export default function UserPage() {
const { data, isLoading, error } = useQuery("user", UserInfo, {
retry: 0,
onSuccess: (data) => console.log(data),
});
const logged_in = useSelector((state: RootState) => state.logged_in.value);
if (isLoading && !error) {
return (
<View style={styles.background}>
<View style={styles.addnotecont}>
<Text style={{ ...styles.text_white, ...{ fontSize: 25 } }}>
Loading...
</Text>
</View>
</View>
);
} else if (error) {
return (
<View style={styles.background}>
<View style={styles.addnotecont}>
<Text style={{ ...styles.text_white, ...{ fontSize: 25 } }}>
An error has occured
</Text>
</View>
</View>
);
}
return ( return (
<Background> <Background>
<Text style={{...styles.text_white, ...{fontSize: 32}}}>UserInfo</Text> <View style={styles.addnotecont}>
<Text style={{ ...styles.text_white, ...{ fontSize: 25 } }}>
Username: {data.username}
</Text>
<Text style={{ ...styles.text_white, ...{ fontSize: 25 } }}>
Email: {data.email}
</Text>
<Text style={{ ...styles.text_white, ...{ fontSize: 25 } }}>
User ID: {data.id}
</Text>
</View>
</Background> </Background>
); );
} }

View file

@ -1,34 +1,281 @@
import {StyleSheet} from 'react-native'; import { StyleSheet } from "react-native";
const styles = StyleSheet.create({ const styles = StyleSheet.create({
background: { background: {
backgroundColor: '#002d4c', height: "100%",
height: '100%', width: "100%",
width: '100%', backgroundColor: "#002d4d",
position: "absolute",
}, },
text_white: { text_white: {
color: 'white', color: "white",
fontWeight: 'bold', fontWeight: "bold",
textAlign: 'center', textAlign: "center",
}, },
button_generic: { button_generic: {
justifyContent: 'center', justifyContent: "center",
alignSelf: 'center', alignSelf: "center",
alignItems: 'center', alignItems: "center",
display: 'flex', display: "flex",
flexDirection: 'row', flexDirection: "row",
margin: 8, margin: 8,
padding: 8, padding: 8,
borderRadius: 16, borderRadius: 16,
}, },
flex_row: { flex_row: {
display: 'flex', display: "flex",
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
}, },
flex_column: { flex_column: {
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
alignItems: 'center', alignItems: "center",
},
input: {
height: 40,
margin: 12,
borderWidth: 1,
},
inputView: {
backgroundColor: "black",
borderRadius: 30,
width: "70%",
height: 45,
marginBottom: 20,
},
TextInput: {
color: "white",
height: 50,
flex: 1,
padding: 10,
marginLeft: 5,
},
forgot_button: {
fontWeight: "bold",
color: "white",
height: 30,
marginBottom: 10,
},
loginBtn: {
width: "50%",
borderRadius: 25,
height: 50,
alignItems: "center",
justifyContent: "center",
backgroundColor: "lightgreen",
},
registerbtn: {
width: "80%",
borderRadius: 25,
height: 50,
alignItems: "center",
justifyContent: "center",
marginTop: 40,
backgroundColor: "orange",
},
loginText: {
color: "white",
fontWeight: "bold",
fontSize: 20,
},
registertext: {
color: "white",
fontWeight: "bold",
fontSize: 20,
},
container: {
marginTop: 30,
marginLeft: 22,
height: 500,
width: 350,
borderRadius: 25,
backgroundColor: "#0087e4",
alignItems: "center",
justifyContent: "center",
},
loginlabel: {
fontWeight: "bold",
fontSize: 25,
marginBottom: 30,
color: "white",
},
box: {
width: "100%",
height: 200,
},
userinfocont: {
marginTop: 10,
marginLeft: 50,
height: 80,
width: 300,
borderRadius: 25,
backgroundColor: "orange",
alignItems: "center",
justifyContent: "center",
},
userlabel: {
fontWeight: "bold",
fontSize: 25,
marginBottom: 10,
color: "white",
},
addnote: {
marginLeft: 180,
width: "10%",
borderRadius: 25,
height: 30,
alignItems: "center",
justifyContent: "center",
backgroundColor: "lightgreen",
},
addtext: {
fontSize: 20,
color: "white",
},
addnotescont: {
marginTop: 20,
marginLeft: 20,
height: 200,
width: 350,
backgroundColor: "blue",
alignItems: "flex-start",
justifyContent: "center",
},
no: {
fontSize: 20,
color: "white",
marginTop: 20,
alignItems: "center",
},
typehere: {
backgroundColor: "black",
borderRadius: 10,
width: "75%",
height: 300,
marginTop: 20,
},
typeinput: {
color: "white",
marginBottom: 16,
marginLeft: 5,
minHeight: 128,
maxHeight: 768,
},
title: {
color: "white",
flex: 1,
marginLeft: 5,
},
tle: {
backgroundColor: "black",
borderRadius: 10,
width: "80%",
height: 40,
marginTop: 20,
},
addnotecont: {
marginTop: 30,
marginLeft: 22,
paddingBottom: 30,
minHeight: 500,
width: 350,
borderRadius: 25,
backgroundColor: "#001018",
alignItems: "center",
justifyContent: "center",
},
savebtn: {
width: "40%",
borderRadius: 25,
height: 40,
alignItems: "center",
justifyContent: "center",
marginTop: 10,
backgroundColor: "green",
},
cancelbtn: {
width: "40%",
borderRadius: 25,
height: 40,
alignItems: "center",
justifyContent: "center",
marginTop: 10,
backgroundColor: "green",
},
savenote: {
color: "white",
fontWeight: "bold",
fontSize: 20,
},
cancel: {
color: "white",
fontWeight: "bold",
fontSize: 20,
},
homecont: {
marginTop: 30,
height: 150,
width: 350,
borderRadius: 25,
backgroundColor: "#7cb9e8",
alignItems: "center",
justifyContent: "center",
},
newnote: {
color: "white",
fontWeight: "bold",
fontSize: 50,
},
//
form: {
flex: 1,
alignItems: "center",
},
inputRow: {
width: "60%",
height: 50,
marginTop: 50,
marginLeft: 10,
justifyContent: "center",
},
label: {
marginBottom: 5,
marginLeft: 5,
fontSize: 20,
color: "white",
},
disabledInput: {
backgroundColor: "white",
borderRadius: 10,
},
editButton: {
backgroundColor: "green",
width: "20%",
borderRadius: 25,
height: 40,
alignItems: "center",
justifyContent: "center",
marginTop: 20,
marginLeft: 20,
},
buttonText: {
color: "white",
},
inputUser: {
marginLeft: 10,
height: 40,
width: "100%",
color: "black",
backgroundColor: "white",
borderRadius: 10,
},
text_small_red: {
color: "#bc231e",
fontSize: 10,
fontWeight: "bold",
textAlign: "center",
}, },
}); });