Polished up some UI and added dark/light mode
BIN
FastNotes/Diagrams/ER.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
FastNotes/Diagrams/createNote.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
FastNotes/Diagrams/deleteNote.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
FastNotes/Diagrams/login.png
Normal file
|
After Width: | Height: | Size: 517 KiB |
BIN
FastNotes/Diagrams/signout.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
FastNotes/Diagrams/signup.png
Normal file
|
After Width: | Height: | Size: 473 KiB |
BIN
FastNotes/Diagrams/updateNote.png
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
FastNotes/Diagrams/viewNote.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
@@ -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 (
|
||||
<Stack>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
contentStyle: { backgroundColor: palette.background },
|
||||
headerStyle: { backgroundColor: palette.surface },
|
||||
headerShadowVisible: false,
|
||||
headerTintColor: palette.text,
|
||||
headerTitleStyle: { color: palette.text },
|
||||
}}
|
||||
>
|
||||
<Stack.Protected guard={isLoggedIn}>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="index" options={{ headerShown: false,title: "Notes", headerBackTitle: "Notes" }} />
|
||||
<Stack.Screen name="newNote" options={{ headerShown: true, title: 'New Note' }} />
|
||||
<Stack.Screen name="detail" options={{ headerShown: true, title: 'Note' }} />
|
||||
</Stack.Protected>
|
||||
@@ -28,27 +37,57 @@ function RootNavigator() {
|
||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="signup" options={{ headerShown: false }} />
|
||||
</Stack.Protected>
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<SafeAreaProvider>
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DefaultTheme : DefaultTheme}>
|
||||
<ThemeProvider value={navigationTheme}>
|
||||
<AuthProvider>
|
||||
<NotesProvider>
|
||||
<RootNavigator />
|
||||
</NotesProvider>
|
||||
</AuthProvider>
|
||||
<StatusBar style="auto" />
|
||||
<StatusBar
|
||||
style={colorScheme === "dark" ? "light" : "dark"}
|
||||
backgroundColor={palette.statusBar}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<AppThemeProvider>
|
||||
<ThemedRootLayout />
|
||||
</AppThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [statusMessage, setStatusMessage] = useState<string | null>(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<string | null>(null)
|
||||
const [statusMessage, setStatusMessage] = useState<string | null>(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,77 +82,89 @@ 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 (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Note not found</Text>
|
||||
<Text style={styles.content}>The note may have been deleted.</Text>
|
||||
<View style={[styles.container, { backgroundColor: palette.background, padding: 16 }]}>
|
||||
<Text style={[styles.title, { color: palette.text }]}>Note not found</Text>
|
||||
<Text style={[styles.content, { color: palette.mutedText }]}>The note may have been deleted.</Text>
|
||||
</View>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return(
|
||||
<View style={styles.container}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={headerHeight}
|
||||
style={styles.keyboardAvoider}
|
||||
>
|
||||
<View style={[styles.container, { backgroundColor: palette.background }]}>
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.formContent, { paddingBottom: insets.bottom + 112 }]}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<TextInput
|
||||
editable={canEdit}
|
||||
onChangeText={setTitle}
|
||||
style={styles.titleInput}
|
||||
style={[styles.titleInput, { color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]}
|
||||
value={title}
|
||||
placeholderTextColor={palette.mutedText}
|
||||
/>
|
||||
<Text style={styles.signature}>Created by {note.creatorLabel}</Text>
|
||||
<Text style={styles.signature}>
|
||||
<Text style={[styles.signature, { color: palette.mutedText }]}>Created by {note.creatorLabel}</Text>
|
||||
<Text style={[styles.signature, { color: palette.mutedText }]}>
|
||||
Last changed {formatTimestamp(note.lastChangedAt)}
|
||||
</Text>
|
||||
<TextInput
|
||||
editable={canEdit}
|
||||
multiline
|
||||
onChangeText={setContent}
|
||||
style={styles.contentInput}
|
||||
style={[styles.contentInput, { color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]}
|
||||
textAlignVertical="top"
|
||||
value={content}
|
||||
placeholderTextColor={palette.mutedText}
|
||||
/>
|
||||
{!canEdit ? (
|
||||
<Text style={styles.readOnlyText}>
|
||||
<Text style={[styles.readOnlyText, { color: palette.mutedText }]}>
|
||||
Only the creator of this note can update or delete it.
|
||||
</Text>
|
||||
) : null}
|
||||
{localErrorMessage ? <Text style={styles.errorText}>{localErrorMessage}</Text> : null}
|
||||
{!localErrorMessage && errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||
{statusMessage ? <Text style={styles.successText}>{statusMessage}</Text> : null}
|
||||
</ScrollView>
|
||||
{canEdit ? (
|
||||
<View style={styles.actions}>
|
||||
<Pressable disabled={isSaving} onPress={onSave} style={styles.primaryButton}>
|
||||
<Text style={styles.primaryButtonText}>
|
||||
<View style={[styles.actions, { bottom: insets.bottom + 16 }]}>
|
||||
<Pressable disabled={isSaving} onPress={onSave} style={[styles.primaryButton, { backgroundColor: palette.accent }]}>
|
||||
<Text style={[styles.primaryButtonText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>
|
||||
{isSaving ? "Saving..." : "Save changes"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable disabled={isDeleting} onPress={confirmDelete} style={styles.deleteButton}>
|
||||
<Pressable disabled={isDeleting} onPress={confirmDelete} style={[styles.deleteButton, { backgroundColor: palette.destructive }]}>
|
||||
<Text style={styles.deleteButtonText}>
|
||||
{isDeleting ? "Deleting..." : "Delete note"}
|
||||
</Text>
|
||||
@@ -154,13 +172,16 @@ export default function DetailScreen()
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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<TabKey>("my-notes");
|
||||
const insets = useSafeAreaInsets();
|
||||
const userId = claims?.sub;
|
||||
const { claims } = useAuthContext()
|
||||
const { errorMessage, isLoading, notes } = useNotes()
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("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"
|
||||
@@ -40,9 +43,9 @@ export default function HomeScreen()
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.topBar, { paddingTop: insets.top + 8 }]}>
|
||||
<Text style={styles.screenTitle}>FastNotes</Text>
|
||||
<View style={[styles.container, { backgroundColor: palette.background }]}>
|
||||
<View style={[styles.topBar, { paddingTop: insets.top + 8, backgroundColor: palette.surface, borderBottomColor: palette.border }]}>
|
||||
<Text style={[styles.screenTitle, { color: palette.text }]}>FastNotes</Text>
|
||||
<SignOutButton />
|
||||
</View>
|
||||
|
||||
@@ -51,12 +54,14 @@ return (
|
||||
onPress={() => setActiveTab("my-notes")}
|
||||
style={[
|
||||
styles.tabButton,
|
||||
{ backgroundColor: palette.elevated, borderColor: palette.border },
|
||||
activeTab === "my-notes" ? styles.tabButtonActive : null,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabButtonText,
|
||||
{ color: palette.text },
|
||||
activeTab === "my-notes" ? styles.tabButtonTextActive : 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,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabButtonText,
|
||||
{ color: palette.text },
|
||||
activeTab === "work-notes" ? styles.tabButtonTextActive : null,
|
||||
]}
|
||||
>
|
||||
@@ -94,7 +101,7 @@ return (
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
style={styles.noteItem}
|
||||
style={[styles.noteItem, { backgroundColor: palette.surface, borderColor: palette.border }]}
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: "/detail",
|
||||
@@ -102,10 +109,10 @@ return (
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text style={styles.noteTitle}>{item.title}</Text>
|
||||
<Text numberOfLines={2} style={styles.notePreview}>{item.content}</Text>
|
||||
<Text style={styles.noteMeta}>Created by {item.creatorLabel}</Text>
|
||||
<Text style={styles.noteMeta}>
|
||||
<Text style={[styles.noteTitle, { color: palette.text }]}>{item.title}</Text>
|
||||
<Text numberOfLines={2} style={[styles.notePreview, { color: palette.mutedText }]}>{item.content}</Text>
|
||||
<Text style={[styles.noteMeta, { color: palette.mutedText }]}>Created by {item.creatorLabel}</Text>
|
||||
<Text style={[styles.noteMeta, { color: palette.mutedText }]}>
|
||||
Last changed {formatTimestamp(item.lastChangedAt)}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -114,14 +121,14 @@ return (
|
||||
|
||||
{activeTab === "my-notes" ? (
|
||||
<Pressable
|
||||
style={[styles.fab, { bottom: insets.bottom + 24, right: insets.right + 40 }]}
|
||||
style={[styles.fab, { bottom: insets.bottom + 24, right: insets.right + 40, backgroundColor: palette.accent }]}
|
||||
onPress={() => router.push("/newNote")}
|
||||
>
|
||||
<Text style={styles.fabText}>+</Text>
|
||||
<Text style={[styles.fabText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>+</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
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"}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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<string | null>(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 (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Login' }} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Login</Text>
|
||||
<View style={[styles.container, { backgroundColor: palette.background }]}>
|
||||
<Text style={[styles.title, { color: palette.text }]}>Login</Text>
|
||||
|
||||
<Text style={styles.label}>E-mail</Text>
|
||||
<Text style={[styles.label, { color: palette.text }]}>E-mail</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
onChangeText={setEmail}
|
||||
placeholder="name@email.com"
|
||||
placeholderTextColor="#666"
|
||||
style={styles.input}
|
||||
placeholderTextColor={palette.mutedText}
|
||||
style={[styles.input, { color: palette.text, backgroundColor: palette.input, borderColor: palette.border }]}
|
||||
value={email}
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Password</Text>
|
||||
<Text style={[styles.label, { color: palette.text }]}>Password</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="password"
|
||||
onChangeText={setPassword}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#666"
|
||||
placeholderTextColor={palette.mutedText}
|
||||
secureTextEntry
|
||||
style={styles.input}
|
||||
style={[styles.input, { color: palette.text, backgroundColor: palette.input, borderColor: palette.border }]}
|
||||
value={password}
|
||||
/>
|
||||
|
||||
@@ -79,7 +81,7 @@ export default function LoginScreen() {
|
||||
</Pressable>
|
||||
|
||||
<Link href="/signup" style={styles.link}>
|
||||
<Text style={styles.linkText}>Create a new account</Text>
|
||||
<Text style={[styles.linkText, { color: colorScheme === "dark" ? "#8ab4ff" : "#0b57d0" }]}>Create a new account</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
const insets = useSafeAreaInsets()
|
||||
const headerHeight = useHeaderHeight()
|
||||
const { colorScheme, palette } = useAppTheme()
|
||||
const [contentHeight, setContentHeight] = useState(160)
|
||||
const scrollRef = useRef<ScrollView>(null)
|
||||
|
||||
@@ -43,22 +45,24 @@ export default function NewNoteScreen()
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={{ flex: 1}} behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
<KeyboardAvoidingView
|
||||
style={styles.keyboardAvoider}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={headerHeight}>
|
||||
<View style={styles.container}>
|
||||
<ScrollView ref={scrollRef} contentContainerStyle={{ padding: 16, paddingBottom: 120, gap: 10}}
|
||||
<View style={[styles.container, { backgroundColor: palette.background }]}>
|
||||
<ScrollView ref={scrollRef} contentContainerStyle={[styles.formContent, { paddingBottom: insets.bottom + 112 }]}
|
||||
keyboardShouldPersistTaps="handled">
|
||||
<Text style={styles.label}>Title</Text>
|
||||
<TextInput value={title} onChangeText={setTitle}
|
||||
placeholder="Give it a title..." style={styles.input}
|
||||
placeholder="Give it a title..." style={[styles.titleInput, { color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]}
|
||||
placeholderTextColor={palette.mutedText}
|
||||
returnKeyType="next"/>
|
||||
|
||||
<Text style={styles.label}>Content</Text>
|
||||
<TextInput value={content} onChangeText={setContent} placeholder="Write your note..."
|
||||
style={[styles.input, { height: Math.max(160, contentHeight) }]} multiline
|
||||
style={[styles.contentInput, { minHeight: Math.max(200, contentHeight), color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]} multiline
|
||||
placeholderTextColor={palette.mutedText}
|
||||
textAlignVertical="top"
|
||||
onContentSizeChange={(e) => {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}
|
||||
|
||||
</ScrollView>
|
||||
<View>
|
||||
<View style={[styles.actions, { bottom: insets.bottom + 16 }]}>
|
||||
<Pressable disabled={isSaving} onPress={onSave}
|
||||
style={[styles.saveFloating, { bottom: insets.bottom + 16 }]}>
|
||||
<Text style={styles.saveFloatingText}>
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
style={[styles.saveButton, { backgroundColor: palette.accent }]}>
|
||||
<Text style={[styles.saveFloatingText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>
|
||||
{isSaving ? "Saving..." : "Save note"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
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"
|
||||
},
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(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 (
|
||||
<>
|
||||
<Stack.Screen options={{title: "Signup"}}/>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Sign up</Text>
|
||||
<View style={[styles.container, { backgroundColor: palette.background }]}>
|
||||
<Text style={[styles.title, { color: palette.text }]}>Sign up</Text>
|
||||
|
||||
<Text style={styles.label}>E-mail</Text>
|
||||
<Text style={[styles.label, { color: palette.text }]}>E-mail</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
onChangeText={setEmail}
|
||||
placeholder="name@email.com"
|
||||
placeholderTextColor="#666"
|
||||
style={styles.input}
|
||||
placeholderTextColor={palette.mutedText}
|
||||
style={[styles.input, { color: palette.text, backgroundColor: palette.input, borderColor: palette.border }]}
|
||||
value={email}
|
||||
/>
|
||||
|
||||
<Text style={styles.label} >Password</Text>
|
||||
<Text style={[styles.label, { color: palette.text }]} >Password</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="password"
|
||||
onChangeText={setPassword}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#666"
|
||||
placeholderTextColor={palette.mutedText}
|
||||
secureTextEntry
|
||||
style={styles.input}
|
||||
style={[styles.input, { color: palette.text, backgroundColor: palette.input, borderColor: palette.border }]}
|
||||
value={password}
|
||||
/>
|
||||
|
||||
@@ -106,7 +108,7 @@ export default function SignupScreen(){
|
||||
</Pressable>
|
||||
|
||||
<Link href="/login" style={styles.link}>
|
||||
<Text style={styles.linkText}>Already have an account? Log in</Text>
|
||||
<Text style={[styles.linkText, { color: colorScheme === "dark" ? "#8ab4ff" : "#0b57d0" }]}>Already have an account? Log in</Text>
|
||||
</Link>
|
||||
|
||||
</View>
|
||||
@@ -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',
|
||||
|
||||
@@ -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 <Button title="Sign out" onPress={onSignOutButtonPress} />
|
||||
const { palette } = useAppTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onSignOutButtonPress}
|
||||
style={[styles.button, { borderColor: palette.border, backgroundColor: palette.elevated }]}
|
||||
>
|
||||
<Text style={[styles.text, { color: palette.text }]}>Sign out</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
text: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { StyleSheet, Text, type TextProps } from 'react-native';
|
||||
import { StyleSheet, Text, type TextProps } from 'react-native'
|
||||
|
||||
import { useThemeColor } from '@/hooks/use-theme-color';
|
||||
import { useThemeColor } from '@/hooks/use-theme-color'
|
||||
|
||||
export type ThemedTextProps = TextProps & {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
|
||||
};
|
||||
lightColor?: string
|
||||
darkColor?: string
|
||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'
|
||||
}
|
||||
|
||||
export function ThemedText({
|
||||
style,
|
||||
@@ -15,7 +15,7 @@ export function ThemedText({
|
||||
type = 'default',
|
||||
...rest
|
||||
}: ThemedTextProps) {
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text')
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -30,7 +30,7 @@ export function ThemedText({
|
||||
]}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -57,4 +57,4 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
color: '#0a7ea4',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { View, type ViewProps } from 'react-native';
|
||||
import { View, type ViewProps } from 'react-native'
|
||||
|
||||
import { useThemeColor } from '@/hooks/use-theme-color';
|
||||
import { useThemeColor } from '@/hooks/use-theme-color'
|
||||
|
||||
export type ThemedViewProps = ViewProps & {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
};
|
||||
lightColor?: string
|
||||
darkColor?: string
|
||||
}
|
||||
|
||||
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
|
||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background')
|
||||
|
||||
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
|
||||
return <View style={[{ backgroundColor }, style]} {...otherProps} />
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { useColorScheme } from 'react-native';
|
||||
export { useAppColorScheme as useColorScheme } from '@/src/theme/AppThemeProvider';
|
||||
|
||||
@@ -1,21 +1 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useColorScheme as useRNColorScheme } from 'react-native';
|
||||
|
||||
/**
|
||||
* To support static rendering, this value needs to be re-calculated on the client side for web
|
||||
*/
|
||||
export function useColorScheme() {
|
||||
const [hasHydrated, setHasHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasHydrated(true);
|
||||
}, []);
|
||||
|
||||
const colorScheme = useRNColorScheme();
|
||||
|
||||
if (hasHydrated) {
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
return 'light';
|
||||
}
|
||||
export { useAppColorScheme as useColorScheme } from '@/src/theme/AppThemeProvider';
|
||||
|
||||
@@ -5,7 +5,7 @@ export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const theme = useColorScheme();
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store';
|
||||
import 'react-native-url-polyfill/auto';
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store'
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import Constants from "expo-constants"
|
||||
import { Platform } from 'react-native';
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
const ExpoSecureStoreAdapter = {
|
||||
getItem: (key: string) => {
|
||||
console.debug("getItem", { key, getItemAsync })
|
||||
return getItemAsync(key)
|
||||
},
|
||||
setItem: (key: string, value: string) => {
|
||||
@@ -18,12 +17,12 @@ const ExpoSecureStoreAdapter = {
|
||||
removeItem: (key: string) => {
|
||||
return deleteItemAsync(key)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const extra = (Constants.expoConfig?.extra ?? Constants.manifest?.extra) as {
|
||||
supabaseUrl?: string;
|
||||
supabaseKey?: string;
|
||||
};
|
||||
supabaseUrl?: string
|
||||
supabaseKey?: string
|
||||
}
|
||||
|
||||
const supabaseUrl = extra?.supabaseUrl
|
||||
const supabaseAnonKey = extra?.supabaseKey
|
||||
|
||||
@@ -2,50 +2,41 @@ import { AuthContext } from '@/hooks/use-auth-context'
|
||||
import { supabase } from '@/libs/supabase'
|
||||
import { PropsWithChildren, useEffect, useState } from 'react'
|
||||
|
||||
const buildClaims = (user?: { id: string; email?: string | null } | null) =>
|
||||
user
|
||||
? {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
}
|
||||
: null
|
||||
|
||||
export default function AuthProvider({ children }: PropsWithChildren) {
|
||||
const [claims, setClaims] = useState<Record<string, any> | undefined | null>()
|
||||
const [profile, setProfile] = useState<any>()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
const syncAuthState = async () => {
|
||||
setIsLoading(true)
|
||||
|
||||
const hydrateSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
data: { session },
|
||||
error,
|
||||
} = await supabase.auth.getUser()
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching user:', error)
|
||||
console.error('Error hydrating session:', error)
|
||||
}
|
||||
|
||||
setClaims(
|
||||
user
|
||||
? {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
}
|
||||
: null
|
||||
)
|
||||
setClaims(buildClaims(session?.user))
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
void syncAuthState()
|
||||
void hydrateSession()
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(async (_event, session) => {
|
||||
console.log('Auth state changed:', { event: _event })
|
||||
|
||||
setClaims(
|
||||
session?.user
|
||||
? {
|
||||
sub: session.user.id,
|
||||
email: session.user.email,
|
||||
}
|
||||
: null
|
||||
)
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setClaims(buildClaims(session?.user))
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -148,7 +148,7 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
// Poll for remote changes so other users' edits appear without a manual refresh.
|
||||
const intervalId = setInterval(() => {
|
||||
void loadNotes()
|
||||
}, 60000)
|
||||
}, 30000)
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId)
|
||||
|
||||
58
FastNotes/src/theme/AppThemeProvider.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react"
|
||||
import { Appearance } from "react-native"
|
||||
|
||||
import { AppColorScheme, getThemePalette } from "@/src/theme/palette"
|
||||
|
||||
type AppThemeContextValue = {
|
||||
colorScheme: AppColorScheme
|
||||
palette: ReturnType<typeof getThemePalette>
|
||||
}
|
||||
|
||||
const AppThemeContext = createContext<AppThemeContextValue | undefined>(undefined)
|
||||
|
||||
function resolveColorScheme(colorScheme: "light" | "dark" | null | undefined): AppColorScheme {
|
||||
return colorScheme === "dark" ? "dark" : "light"
|
||||
}
|
||||
|
||||
export function AppThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [colorScheme, setColorScheme] = useState<AppColorScheme>(() =>
|
||||
resolveColorScheme(Appearance.getColorScheme())
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = Appearance.addChangeListener(({ colorScheme: nextColorScheme }) => {
|
||||
setColorScheme(resolveColorScheme(nextColorScheme))
|
||||
})
|
||||
|
||||
setColorScheme(resolveColorScheme(Appearance.getColorScheme()))
|
||||
|
||||
return () => {
|
||||
subscription.remove()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AppThemeContext.Provider
|
||||
value={{
|
||||
colorScheme,
|
||||
palette: getThemePalette(colorScheme),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAppTheme() {
|
||||
const value = useContext(AppThemeContext)
|
||||
|
||||
if (!value) {
|
||||
throw new Error("useAppTheme must be used inside AppThemeProvider")
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function useAppColorScheme() {
|
||||
return useAppTheme().colorScheme
|
||||
}
|
||||
31
FastNotes/src/theme/palette.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export type AppColorScheme = "light" | "dark"
|
||||
|
||||
const lightPalette = {
|
||||
background: "#f5efe6",
|
||||
surface: "#fffdf8",
|
||||
elevated: "#ffffff",
|
||||
text: "#111111",
|
||||
mutedText: "#5f5a52",
|
||||
border: "#77736e",
|
||||
input: "#ffffff",
|
||||
accent: "#111111",
|
||||
destructive: "#b71c1c",
|
||||
statusBar: "#fffdf8",
|
||||
}
|
||||
|
||||
const darkPalette = {
|
||||
background: "#000000",
|
||||
surface: "#0b0b0b",
|
||||
elevated: "#111111",
|
||||
text: "#ffffff",
|
||||
mutedText: "#b8b8b8",
|
||||
border: "#ffffff",
|
||||
input: "#151515",
|
||||
accent: "#ffffff",
|
||||
destructive: "#ff6b6b",
|
||||
statusBar: "#000000",
|
||||
}
|
||||
|
||||
export function getThemePalette(colorScheme: AppColorScheme) {
|
||||
return colorScheme === "dark" ? darkPalette : lightPalette
|
||||
}
|
||||