diff --git a/FastNotes/Diagrams/ER.png b/FastNotes/Diagrams/ER.png new file mode 100644 index 0000000..2e21e5f Binary files /dev/null and b/FastNotes/Diagrams/ER.png differ diff --git a/FastNotes/Diagrams/createNote.png b/FastNotes/Diagrams/createNote.png new file mode 100644 index 0000000..88b7371 Binary files /dev/null and b/FastNotes/Diagrams/createNote.png differ diff --git a/FastNotes/Diagrams/deleteNote.png b/FastNotes/Diagrams/deleteNote.png new file mode 100644 index 0000000..ae4b3f4 Binary files /dev/null and b/FastNotes/Diagrams/deleteNote.png differ diff --git a/FastNotes/Diagrams/login.png b/FastNotes/Diagrams/login.png new file mode 100644 index 0000000..8241044 Binary files /dev/null and b/FastNotes/Diagrams/login.png differ diff --git a/FastNotes/Diagrams/signout.png b/FastNotes/Diagrams/signout.png new file mode 100644 index 0000000..e4705c0 Binary files /dev/null and b/FastNotes/Diagrams/signout.png differ diff --git a/FastNotes/Diagrams/signup.png b/FastNotes/Diagrams/signup.png new file mode 100644 index 0000000..9e55a3f Binary files /dev/null and b/FastNotes/Diagrams/signup.png differ diff --git a/FastNotes/Diagrams/updateNote.png b/FastNotes/Diagrams/updateNote.png new file mode 100644 index 0000000..e9a362c Binary files /dev/null and b/FastNotes/Diagrams/updateNote.png differ diff --git a/FastNotes/Diagrams/viewNote.png b/FastNotes/Diagrams/viewNote.png new file mode 100644 index 0000000..0bab7b3 Binary files /dev/null and b/FastNotes/Diagrams/viewNote.png differ diff --git a/FastNotes/app/_layout.tsx b/FastNotes/app/_layout.tsx index 3289d0c..3613f09 100644 --- a/FastNotes/app/_layout.tsx +++ b/FastNotes/app/_layout.tsx @@ -1,26 +1,35 @@ -import { DefaultTheme, ThemeProvider } from '@react-navigation/native'; -import { Stack } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; -import 'react-native-reanimated'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { useAuthContext } from '@/hooks/use-auth-context' +import AuthProvider from '@/providers/auth-provider' import { NotesProvider } from "@/src/notes/NotesContext" -import { useColorScheme } from '@/hooks/use-color-scheme'; -import { useAuthContext } from '@/hooks/use-auth-context'; -import AuthProvider from '@/providers/auth-provider'; +import { AppThemeProvider, useAppTheme } from '@/src/theme/AppThemeProvider' +import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native' +import { Stack } from 'expo-router' +import { StatusBar } from 'expo-status-bar' +import 'react-native-reanimated' +import { SafeAreaProvider } from 'react-native-safe-area-context' // Separate RootNavigator so we can access the AuthContext function RootNavigator() { const { isLoggedIn, isLoading } = useAuthContext() + const { palette } = useAppTheme() if (isLoading) { return null } return ( - + - + @@ -28,27 +37,57 @@ function RootNavigator() { - ) } -export default function RootLayout() { - const colorScheme = useColorScheme() - /*TODO - Fix ThemeProvider to work with dark theme - */ +function ThemedRootLayout() { + const { colorScheme, palette } = useAppTheme() + const navigationTheme = colorScheme === "dark" + ? { + ...DarkTheme, + colors: { + ...DarkTheme.colors, + background: palette.background, + card: palette.surface, + text: palette.text, + border: palette.border, + primary: palette.accent, + }, + } + : { + ...DefaultTheme, + colors: { + ...DefaultTheme.colors, + background: palette.background, + card: palette.surface, + text: palette.text, + border: palette.border, + primary: palette.accent, + }, + } return ( - + - + - ); + ) +} + +export default function RootLayout() { + return ( + + + + ) } diff --git a/FastNotes/app/detail.tsx b/FastNotes/app/detail.tsx index 1ce0262..46b5eea 100644 --- a/FastNotes/app/detail.tsx +++ b/FastNotes/app/detail.tsx @@ -1,66 +1,72 @@ -import { useEffect, useState } from "react"; -import { Alert, Pressable, StyleSheet, Text, TextInput, View } from "react-native"; -import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react" +import { Alert, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from "react-native" +import { router, useLocalSearchParams } from "expo-router" +import { useHeaderHeight } from "@react-navigation/elements" +import { useSafeAreaInsets } from "react-native-safe-area-context" +import { useAppTheme } from "@/src/theme/AppThemeProvider" -import { useAuthContext } from "@/hooks/use-auth-context"; -import { useNotes } from "@/src/notes/NotesContext"; +import { useAuthContext } from "@/hooks/use-auth-context" +import { useNotes } from "@/src/notes/NotesContext" export default function DetailScreen() { const { id } = useLocalSearchParams< { - id?: string; - }>(); - const { claims } = useAuthContext(); - const { deleteNote, errorMessage, notes, updateNote } = useNotes(); - const note = notes.find((entry) => entry.id === id); - const canEdit = note?.createdBy === claims?.sub; - const [title, setTitle] = useState(note?.title ?? ""); - const [content, setContent] = useState(note?.content ?? ""); - const [isSaving, setIsSaving] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [localErrorMessage, setLocalErrorMessage] = useState(null); - const [statusMessage, setStatusMessage] = useState(null); + id?: string + }>() + const { claims } = useAuthContext() + const { deleteNote, errorMessage, notes, updateNote } = useNotes() + const note = notes.find((entry) => entry.id === id) + const canEdit = note?.createdBy === claims?.sub + const [title, setTitle] = useState(note?.title ?? "") + const [content, setContent] = useState(note?.content ?? "") + const [isSaving, setIsSaving] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + const [localErrorMessage, setLocalErrorMessage] = useState(null) + const [statusMessage, setStatusMessage] = useState(null) + const insets = useSafeAreaInsets() + const headerHeight = useHeaderHeight() + const { colorScheme, palette } = useAppTheme() const formatTimestamp = (value: string) => { - const parsed = new Date(value); + const parsed = new Date(value) if (Number.isNaN(parsed.getTime())) { - return "Unknown"; + return "Unknown" } - return parsed.toLocaleString(); - }; + return parsed.toLocaleString() + } useEffect(() => { - setTitle(note?.title ?? ""); - setContent(note?.content ?? ""); - }, [note?.content, note?.title]); + setTitle(note?.title ?? "") + setContent(note?.content ?? "") + }, [note?.content, note?.title]) const onSave = async () => { if (!id) { - setLocalErrorMessage("This note could not be found."); - return; + setLocalErrorMessage("This note could not be found.") + return } if (!title.trim() || !content.trim()) { - setLocalErrorMessage("Title and content are required."); - return; + setLocalErrorMessage("Title and content are required.") + return } - setIsSaving(true); - setLocalErrorMessage(null); - setStatusMessage(null); + setIsSaving(true) + setLocalErrorMessage(null) + setStatusMessage(null) - const wasSaved = await updateNote(id, title, content); + const wasSaved = await updateNote(id, title, content) - setIsSaving(false); + setIsSaving(false) if (wasSaved) { - setStatusMessage("Note updated."); + setStatusMessage("Note updated.") } - }; + } const confirmDelete = () => { // Require explicit confirmation before deleting the note. @@ -76,91 +82,106 @@ export default function DetailScreen() text: "Delete", style: "destructive", onPress: () => { - void onDelete(); + void onDelete() }, }, ] - ); - }; + ) + } const onDelete = async () => { if (!id) { - setLocalErrorMessage("This note could not be found."); - return; + setLocalErrorMessage("This note could not be found.") + return } - setIsDeleting(true); - setLocalErrorMessage(null); - setStatusMessage(null); + setIsDeleting(true) + setLocalErrorMessage(null) + setStatusMessage(null) - const wasDeleted = await deleteNote(id); + const wasDeleted = await deleteNote(id) - setIsDeleting(false); + setIsDeleting(false) if (wasDeleted) { - router.replace("/"); + router.replace("/") } - }; + } if (!note) { return ( - - Note not found - The note may have been deleted. + + Note not found + The note may have been deleted. - ); + ) } - return( - - - Created by {note.creatorLabel} - - Last changed {formatTimestamp(note.lastChangedAt)} - - - {!canEdit ? ( - - Only the creator of this note can update or delete it. - - ) : null} - {localErrorMessage ? {localErrorMessage} : null} - {!localErrorMessage && errorMessage ? {errorMessage} : null} - {statusMessage ? {statusMessage} : null} - {canEdit ? ( - - - - {isSaving ? "Saving..." : "Save changes"} + return( + + + + + Created by {note.creatorLabel} + + Last changed {formatTimestamp(note.lastChangedAt)} + + + {!canEdit ? ( + + Only the creator of this note can update or delete it. - - - - {isDeleting ? "Deleting..." : "Delete note"} - - - - ) : null} - - ); + ) : null} + {localErrorMessage ? {localErrorMessage} : null} + {!localErrorMessage && errorMessage ? {errorMessage} : null} + {statusMessage ? {statusMessage} : null} + + {canEdit ? ( + + + + {isSaving ? "Saving..." : "Save changes"} + + + + + {isDeleting ? "Deleting..." : "Delete note"} + + + + ) : null} + + + ) } const styles = StyleSheet.create( { - container: { flex: 1, padding: 16, gap: 12 }, + keyboardAvoider: { flex: 1 }, + container: { flex: 1 }, + formContent: { padding: 16, gap: 12 }, title: { fontSize: 22, fontWeight:"700" }, content: { fontSize: 16 }, titleInput: { @@ -175,7 +196,6 @@ const styles = StyleSheet.create( color: "#666", }, contentInput: { - flex: 1, minHeight: 200, borderWidth: 1, borderRadius: 8, @@ -192,28 +212,41 @@ const styles = StyleSheet.create( color: "#666", }, actions: { + position: "absolute", + left: 16, + right: 16, + flexDirection: "row", gap: 12, }, primaryButton: { + flex: 1, borderRadius: 12, paddingVertical: 14, alignItems: "center", - backgroundColor: "#111", + shadowColor: "#000", + shadowOpacity: 0.18, + shadowOffset: { width: 0, height: 8 }, + shadowRadius: 16, + elevation: 8, }, primaryButtonText: { - color: "#fff", fontSize: 16, fontWeight: "700", }, deleteButton: { + flex: 1, borderRadius: 12, paddingVertical: 14, alignItems: "center", - backgroundColor: "#b71c1c", + shadowColor: "#000", + shadowOpacity: 0.18, + shadowOffset: { width: 0, height: 8 }, + shadowRadius: 16, + elevation: 8, }, deleteButtonText: { color: "#fff", fontSize: 16, fontWeight: "700", }, - }); + }) diff --git a/FastNotes/app/index.tsx b/FastNotes/app/index.tsx index 1621f28..2cd2b9e 100644 --- a/FastNotes/app/index.tsx +++ b/FastNotes/app/index.tsx @@ -1,20 +1,23 @@ -import { useMemo, useState } from "react"; -import { FlatList, Pressable, StyleSheet, Text, View } from "react-native"; -import { router } from "expo-router"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useAuthContext } from "@/hooks/use-auth-context"; -import { useNotes } from "@/src/notes/NotesContext"; +import { useMemo, useState } from "react" +import { Appearance, FlatList, Pressable, StyleSheet, Text, View } from "react-native" +import { router } from "expo-router" +import { useSafeAreaInsets } from "react-native-safe-area-context" +import { useAuthContext } from "@/hooks/use-auth-context" +import { useNotes } from "@/src/notes/NotesContext" import SignOutButton from '@/components/social-auth-buttons/sign-out-button' +import { useAppTheme } from "@/src/theme/AppThemeProvider" + type TabKey = "my-notes" | "work-notes" export default function HomeScreen() { - const { claims } = useAuthContext(); - const { errorMessage, isLoading, notes } = useNotes(); - const [activeTab, setActiveTab] = useState("my-notes"); - const insets = useSafeAreaInsets(); - const userId = claims?.sub; + const { claims } = useAuthContext() + const { errorMessage, isLoading, notes } = useNotes() + const [activeTab, setActiveTab] = useState("my-notes") + const insets = useSafeAreaInsets() + const { colorScheme, palette } = useAppTheme() + const userId = claims?.sub const filteredNotes = useMemo( () => @@ -22,7 +25,7 @@ export default function HomeScreen() activeTab === "my-notes" ? note.createdBy === userId : note.createdBy !== userId ), [activeTab, notes, userId] - ); + ) const emptyText = activeTab === "my-notes" @@ -39,10 +42,10 @@ export default function HomeScreen() return parsed.toLocaleString() } -return ( - - - FastNotes + return ( + + + FastNotes @@ -51,12 +54,14 @@ return ( onPress={() => setActiveTab("my-notes")} style={[ styles.tabButton, + { backgroundColor: palette.elevated, borderColor: palette.border }, activeTab === "my-notes" ? styles.tabButtonActive : null, ]} > @@ -67,12 +72,14 @@ return ( onPress={() => setActiveTab("work-notes")} style={[ styles.tabButton, + { backgroundColor: palette.elevated, borderColor: palette.border }, activeTab === "work-notes" ? styles.tabButtonActive : null, ]} > @@ -94,7 +101,7 @@ return ( } renderItem={({ item }) => ( router.push({ pathname: "/detail", @@ -102,10 +109,10 @@ return ( }) } > - {item.title} - {item.content} - Created by {item.creatorLabel} - + {item.title} + {item.content} + Created by {item.creatorLabel} + Last changed {formatTimestamp(item.lastChangedAt)} @@ -114,14 +121,14 @@ return ( {activeTab === "my-notes" ? ( router.push("/newNote")} > - + + + ) : null} - ); + ) } const styles = StyleSheet.create({ @@ -132,6 +139,7 @@ const styles = StyleSheet.create({ alignItems: "center", paddingHorizontal: 16, paddingBottom: 8, + borderBottomWidth: 1, }, screenTitle: { fontSize: 24, @@ -142,21 +150,21 @@ const styles = StyleSheet.create({ gap: 8, paddingHorizontal: 16, paddingBottom: 8, + paddingTop: 12 }, tabButton: { flex: 1, + borderWidth: 1, borderRadius: 999, paddingVertical: 10, alignItems: "center", - backgroundColor: "#e6e6e6", }, tabButtonActive: { - backgroundColor: "#111", + backgroundColor: Appearance.getColorScheme() === "light" ? "#000000":"#8a8888", }, tabButtonText: { fontSize: 14, fontWeight: "600", - color: "#111", }, tabButtonTextActive: { color: "#fff", @@ -164,8 +172,8 @@ const styles = StyleSheet.create({ list: { padding: 16, gap: 12, paddingTop: 8 }, noteItem: { padding: 16, borderWidth: 1, borderRadius:12, gap: 8 }, noteTitle: { fontSize: 16, fontWeight: "600" }, - notePreview: { fontSize: 14, color: "#444" }, - noteMeta: { fontSize: 12, color: "#666" }, + notePreview: { fontSize: 14 }, + noteMeta: { fontSize: 12 }, emptyText: { textAlign: "center", paddingVertical: 32, @@ -181,7 +189,11 @@ const styles = StyleSheet.create({ position: "absolute", width: 56, height: 56, borderRadius: 28, alignItems: "center", justifyContent: "center", - backgroundColor: "grey" + shadowColor: "#000", + shadowOpacity: 0.18, + shadowOffset: { width: 0, height: 8 }, + shadowRadius: 16, + elevation: 8, }, fabText: { fontSize: 28, lineHeight: 28, fontWeight: "700"} -}); +}) diff --git a/FastNotes/app/login.tsx b/FastNotes/app/login.tsx index 0adecc4..609cb56 100644 --- a/FastNotes/app/login.tsx +++ b/FastNotes/app/login.tsx @@ -1,13 +1,15 @@ import { useState } from 'react' import { Link, Stack } from 'expo-router' -import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native' +import { Appearance, Pressable, StyleSheet, Text, TextInput, View } from 'react-native' import { supabase } from '@/libs/supabase' +import { useAppTheme } from '@/src/theme/AppThemeProvider' export default function LoginScreen() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [errorMessage, setErrorMessage] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) + const { colorScheme, palette } = useAppTheme() const onLogin = async () => { if (!email.trim() || !password) { @@ -33,30 +35,30 @@ export default function LoginScreen() { return ( <> - - Login + + Login - E-mail + E-mail - Password + Password @@ -79,7 +81,7 @@ export default function LoginScreen() { - Create a new account + Create a new account @@ -92,26 +94,20 @@ const styles = StyleSheet.create({ justifyContent: 'center', padding: 24, gap: 12, - backgroundColor: '#f7f7f7', }, title: { fontSize: 28, fontWeight: '700', - color: '#111', }, label: { fontSize: 14, fontWeight: '600', - color: '#111', }, input: { borderWidth: 1, - borderColor: '#cfcfcf', borderRadius: 8, padding: 12, fontSize: 16, - color: '#111', - backgroundColor: '#fff', }, errorText: { color: '#c62828', @@ -121,7 +117,7 @@ const styles = StyleSheet.create({ borderRadius: 12, paddingVertical: 14, alignItems: 'center', - backgroundColor: '#111', + backgroundColor: Appearance.getColorScheme() === "light" ? '#000000':'#696969', marginTop: 8, }, loginButtonPressed: { diff --git a/FastNotes/app/newNote.tsx b/FastNotes/app/newNote.tsx index 41fc817..b47a291 100644 --- a/FastNotes/app/newNote.tsx +++ b/FastNotes/app/newNote.tsx @@ -2,12 +2,13 @@ import { StyleSheet, Text, View, KeyboardAvoidingView, Platform, Pressable, TextInput, ScrollView -} from "react-native"; -import { router, } from "expo-router"; -import { useNotes } from "@/src/notes/NotesContext"; -import { useState, useRef } from "react"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useHeaderHeight } from "@react-navigation/elements"; +} from "react-native" +import { router, } from "expo-router" +import { useNotes } from "@/src/notes/NotesContext" +import { useState, useRef } from "react" +import { useSafeAreaInsets } from "react-native-safe-area-context" +import { useHeaderHeight } from "@react-navigation/elements" +import { useAppTheme } from "@/src/theme/AppThemeProvider" @@ -20,6 +21,7 @@ export default function NewNoteScreen() const [localErrorMessage, setLocalErrorMessage] = useState(null) const insets = useSafeAreaInsets() const headerHeight = useHeaderHeight() + const { colorScheme, palette } = useAppTheme() const [contentHeight, setContentHeight] = useState(160) const scrollRef = useRef(null) @@ -43,22 +45,24 @@ export default function NewNoteScreen() } return ( - - - + - Title - Content {setContentHeight(e.nativeEvent.contentSize.height); - scrollRef.current?.scrollToEnd({ animated: true }); + onContentSizeChange={(e) => {setContentHeight(e.nativeEvent.contentSize.height) + scrollRef.current?.scrollToEnd({ animated: true }) }}/> {localErrorMessage ? ( @@ -70,59 +74,60 @@ export default function NewNoteScreen() ) : null} - + - - {isSaving ? "Saving..." : "Save"} + style={[styles.saveButton, { backgroundColor: palette.accent }]}> + + {isSaving ? "Saving..." : "Save note"} - ); + ) } const styles = StyleSheet.create( { - container: { flex: 1, gap: 10 }, - label: { fontSize: 14, fontWeight: "600" }, - input: + keyboardAvoider: { flex: 1 }, + container: { flex: 1 }, + formContent: { padding: 16, gap: 12 }, + titleInput: { - borderWidth: 1, borderRadius: 7, - padding: 12, fontSize: 16 + borderWidth: 1, + borderRadius: 8, + padding: 12, + fontSize: 22, + fontWeight: "700", }, - saveBtn: + contentInput: { - borderRadius: 12, - paddingVertical: 14, alignItems: "center", - backgroundColor: "grey" + borderWidth: 1, + borderRadius: 8, + padding: 12, + fontSize: 16, }, - saveText: { color: "white", fontSize: 16, fontWeight: "700"}, - saveBar: - { - position: "absolute", - left: 0, - right: 0, - bottom: 0, - paddingTop: 12, - paddingHorizontal: 16, - backgroundColor: "white", - borderTopWidth: 1 - }, - saveFloating: - { + actions: { position: "absolute", left: 16, right: 16, + flexDirection: "row", + gap: 12, + }, + saveButton: + { + flex: 1, borderRadius: 12, paddingVertical: 14, alignItems: "center", - backgroundColor: "#111" + shadowColor: "#000", + shadowOpacity: 0.18, + shadowOffset: { width: 0, height: 8 }, + shadowRadius: 16, + elevation: 8, }, saveFloatingText: { - color: "white", fontSize: 16, fontWeight: "700" }, @@ -130,4 +135,4 @@ const styles = StyleSheet.create( color: "#c62828" }, } -); +) diff --git a/FastNotes/app/signup.tsx b/FastNotes/app/signup.tsx index cb0a9c3..5b17d0a 100644 --- a/FastNotes/app/signup.tsx +++ b/FastNotes/app/signup.tsx @@ -1,7 +1,8 @@ -import { supabase } from "@/libs/supabase"; -import { Link, Stack } from "expo-router"; -import { useState } from "react"; +import { supabase } from "@/libs/supabase" +import { Link, Stack } from "expo-router" +import { useState } from "react" import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native' +import { useAppTheme } from "@/src/theme/AppThemeProvider" export default function SignupScreen(){ const [email, setEmail] = useState('') @@ -9,6 +10,7 @@ export default function SignupScreen(){ const [errorMessage, setErrorMessage] = useState(null) const [successMessage, setSuccessMessage] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) + const { colorScheme, palette } = useAppTheme() const onSignup = async () => { if(!email.trim() || !password){ @@ -56,30 +58,30 @@ export default function SignupScreen(){ return ( <> - - Sign up + + Sign up - E-mail + E-mail - Password + Password @@ -106,7 +108,7 @@ export default function SignupScreen(){ - Already have an account? Log in + Already have an account? Log in @@ -121,26 +123,20 @@ const styles = StyleSheet.create({ justifyContent: 'center', padding: 24, gap: 12, - backgroundColor: '#f7f7f7', }, title: { fontSize: 28, fontWeight: '700', - color: '#111', }, label: { fontSize: 14, fontWeight: '600', - color: '#111', }, input: { borderWidth: 1, - borderColor: '#cfcfcf', borderRadius: 8, padding: 12, fontSize: 16, - color: '#111', - backgroundColor: '#fff', }, errorText: { color: '#c62828', diff --git a/FastNotes/components/social-auth-buttons/sign-out-button.tsx b/FastNotes/components/social-auth-buttons/sign-out-button.tsx index 0aa20d1..ed5f476 100644 --- a/FastNotes/components/social-auth-buttons/sign-out-button.tsx +++ b/FastNotes/components/social-auth-buttons/sign-out-button.tsx @@ -1,7 +1,8 @@ import { supabase } from '@/libs/supabase' import { router } from 'expo-router' import React from 'react' -import { Button } from 'react-native' +import { Pressable, StyleSheet, Text } from 'react-native' +import { useAppTheme } from '@/src/theme/AppThemeProvider' async function onSignOutButtonPress() { const { error } = await supabase.auth.signOut() @@ -15,5 +16,27 @@ async function onSignOutButtonPress() { } export default function SignOutButton() { - return