Added 'cloud storage' functionality
This commit is contained in:
@@ -1,31 +1,54 @@
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
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 { 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';
|
||||
|
||||
|
||||
// Separate RootNavigator so we can access the AuthContext
|
||||
function RootNavigator() {
|
||||
const { isLoggedIn, isLoading } = useAuthContext()
|
||||
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Protected guard={isLoggedIn}>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="newNote" options={{ headerShown: true, title: 'New Note' }} />
|
||||
<Stack.Screen name="detail" options={{ headerShown: true, title: 'Note' }} />
|
||||
</Stack.Protected>
|
||||
<Stack.Protected guard={!isLoggedIn}>
|
||||
<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();
|
||||
|
||||
const colorScheme = useColorScheme()
|
||||
/*TODO
|
||||
Sort ThemeProvider to work with dark theme on iOS
|
||||
Fix ThemeProvider to work with dark theme
|
||||
*/
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<NotesProvider>
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DefaultTheme : DefaultTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="index" options={{ title: 'FastNotes' }} />
|
||||
<Stack.Screen name="newNote" options={{ title: 'New Note'}}/>
|
||||
<Stack.Screen name="detail" options={{ title: 'Detail'}}/>
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
</NotesProvider>
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DefaultTheme : DefaultTheme}>
|
||||
<AuthProvider>
|
||||
<NotesProvider>
|
||||
<RootNavigator />
|
||||
</NotesProvider>
|
||||
</AuthProvider>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,158 @@
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
|
||||
import { useAuthContext } from "@/hooks/use-auth-context";
|
||||
import { useNotes } from "@/src/notes/NotesContext";
|
||||
|
||||
|
||||
export default function DetailScreen()
|
||||
{
|
||||
const { title, content } = useLocalSearchParams<
|
||||
const { id } = useLocalSearchParams<
|
||||
{
|
||||
title?: string;
|
||||
content?: string;
|
||||
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);
|
||||
|
||||
return(
|
||||
const formatTimestamp = (value: string) => {
|
||||
const parsed = new Date(value);
|
||||
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
return parsed.toLocaleString();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(note?.title ?? "");
|
||||
setContent(note?.content ?? "");
|
||||
}, [note?.content, note?.title]);
|
||||
|
||||
const onSave = async () => {
|
||||
if (!id) {
|
||||
setLocalErrorMessage("This note could not be found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!title.trim() || !content.trim()) {
|
||||
setLocalErrorMessage("Title and content are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setLocalErrorMessage(null);
|
||||
setStatusMessage(null);
|
||||
|
||||
const wasSaved = await updateNote(id, title, content);
|
||||
|
||||
setIsSaving(false);
|
||||
|
||||
if (wasSaved) {
|
||||
setStatusMessage("Note updated.");
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
// Require explicit confirmation before deleting the note.
|
||||
Alert.alert(
|
||||
"Delete note",
|
||||
"Are you sure you want to delete this note?",
|
||||
[
|
||||
{
|
||||
text: "Cancel",
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: () => {
|
||||
void onDelete();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (!id) {
|
||||
setLocalErrorMessage("This note could not be found.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
setLocalErrorMessage(null);
|
||||
setStatusMessage(null);
|
||||
|
||||
const wasDeleted = await deleteNote(id);
|
||||
|
||||
setIsDeleting(false);
|
||||
|
||||
if (wasDeleted) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return(
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>{title ?? "(No title)"}</Text>
|
||||
<Text style={styles.content}>{content ?? ""}</Text>
|
||||
<TextInput
|
||||
editable={canEdit}
|
||||
onChangeText={setTitle}
|
||||
style={styles.titleInput}
|
||||
value={title}
|
||||
/>
|
||||
<Text style={styles.signature}>Created by {note.creatorLabel}</Text>
|
||||
<Text style={styles.signature}>
|
||||
Last changed {formatTimestamp(note.lastChangedAt)}
|
||||
</Text>
|
||||
<TextInput
|
||||
editable={canEdit}
|
||||
multiline
|
||||
onChangeText={setContent}
|
||||
style={styles.contentInput}
|
||||
textAlignVertical="top"
|
||||
value={content}
|
||||
/>
|
||||
{!canEdit ? (
|
||||
<Text style={styles.readOnlyText}>
|
||||
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}
|
||||
{canEdit ? (
|
||||
<View style={styles.actions}>
|
||||
<Pressable disabled={isSaving} onPress={onSave} style={styles.primaryButton}>
|
||||
<Text style={styles.primaryButtonText}>
|
||||
{isSaving ? "Saving..." : "Save changes"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable disabled={isDeleting} onPress={confirmDelete} style={styles.deleteButton}>
|
||||
<Text style={styles.deleteButtonText}>
|
||||
{isDeleting ? "Deleting..." : "Delete note"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -22,5 +162,58 @@ const styles = StyleSheet.create(
|
||||
{
|
||||
container: { flex: 1, padding: 16, gap: 12 },
|
||||
title: { fontSize: 22, fontWeight:"700" },
|
||||
content: { fontSize: 16 }
|
||||
});
|
||||
content: { fontSize: 16 },
|
||||
titleInput: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 22,
|
||||
fontWeight: "700",
|
||||
},
|
||||
signature: {
|
||||
fontSize: 12,
|
||||
color: "#666",
|
||||
},
|
||||
contentInput: {
|
||||
flex: 1,
|
||||
minHeight: 200,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
errorText: {
|
||||
color: "#c62828",
|
||||
},
|
||||
successText: {
|
||||
color: "#2e7d32",
|
||||
},
|
||||
readOnlyText: {
|
||||
color: "#666",
|
||||
},
|
||||
actions: {
|
||||
gap: 12,
|
||||
},
|
||||
primaryButton: {
|
||||
borderRadius: 12,
|
||||
paddingVertical: 14,
|
||||
alignItems: "center",
|
||||
backgroundColor: "#111",
|
||||
},
|
||||
primaryButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
},
|
||||
deleteButton: {
|
||||
borderRadius: 12,
|
||||
paddingVertical: 14,
|
||||
alignItems: "center",
|
||||
backgroundColor: "#b71c1c",
|
||||
},
|
||||
deleteButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,49 +1,181 @@
|
||||
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 SignOutButton from '@/components/social-auth-buttons/sign-out-button'
|
||||
|
||||
type TabKey = "my-notes" | "work-notes"
|
||||
|
||||
export default function HomeScreen()
|
||||
{
|
||||
const { notes } = useNotes();
|
||||
const { claims } = useAuthContext();
|
||||
const { errorMessage, isLoading, notes } = useNotes();
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("my-notes");
|
||||
const insets = useSafeAreaInsets();
|
||||
const userId = claims?.sub;
|
||||
|
||||
const filteredNotes = useMemo(
|
||||
() =>
|
||||
notes.filter((note) =>
|
||||
activeTab === "my-notes" ? note.createdBy === userId : note.createdBy !== userId
|
||||
),
|
||||
[activeTab, notes, userId]
|
||||
);
|
||||
|
||||
const emptyText =
|
||||
activeTab === "my-notes"
|
||||
? "No personal notes yet. Create your first note."
|
||||
: "No work notes yet."
|
||||
|
||||
const formatTimestamp = (value: string) => {
|
||||
const parsed = new Date(value)
|
||||
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
return parsed.toLocaleString()
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.topBar, { paddingTop: insets.top + 8 }]}>
|
||||
<Text style={styles.screenTitle}>FastNotes</Text>
|
||||
<SignOutButton />
|
||||
</View>
|
||||
|
||||
<View style={styles.tabBar}>
|
||||
<Pressable
|
||||
onPress={() => setActiveTab("my-notes")}
|
||||
style={[
|
||||
styles.tabButton,
|
||||
activeTab === "my-notes" ? styles.tabButtonActive : null,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabButtonText,
|
||||
activeTab === "my-notes" ? styles.tabButtonTextActive : null,
|
||||
]}
|
||||
>
|
||||
My Notes
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => setActiveTab("work-notes")}
|
||||
style={[
|
||||
styles.tabButton,
|
||||
activeTab === "work-notes" ? styles.tabButtonActive : null,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabButtonText,
|
||||
activeTab === "work-notes" ? styles.tabButtonTextActive : null,
|
||||
]}
|
||||
>
|
||||
Work Notes
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||
|
||||
<FlatList
|
||||
data={notes}
|
||||
data={filteredNotes}
|
||||
keyExtractor={(n) => n.id}
|
||||
contentContainerStyle={[styles.list, { paddingBottom: 120 }]}
|
||||
ListEmptyComponent={
|
||||
<Text style={styles.emptyText}>
|
||||
{isLoading ? "Loading notes..." : emptyText}
|
||||
</Text>
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
style={styles.noteItem}
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: "/detail",
|
||||
params: { title: item.title, content: item.content },
|
||||
params: { id: item.id },
|
||||
})
|
||||
}
|
||||
>
|
||||
<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}>
|
||||
Last changed {formatTimestamp(item.lastChangedAt)}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
style={[styles.fab, { bottom: insets.bottom + 24, right: insets.right + 40 }]}
|
||||
onPress={() => router.push("/newNote")}
|
||||
>
|
||||
<Text style={styles.fabText}>+</Text>
|
||||
</Pressable>
|
||||
{activeTab === "my-notes" ? (
|
||||
<Pressable
|
||||
style={[styles.fab, { bottom: insets.bottom + 24, right: insets.right + 40 }]}
|
||||
onPress={() => router.push("/newNote")}
|
||||
>
|
||||
<Text style={styles.fabText}>+</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
list: { padding: 16, gap: 12, paddingTop: 20 },
|
||||
noteItem: { padding: 16, borderWidth: 1, borderRadius:12 },
|
||||
topBar: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
screenTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: "700",
|
||||
},
|
||||
tabBar: {
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
tabButton: {
|
||||
flex: 1,
|
||||
borderRadius: 999,
|
||||
paddingVertical: 10,
|
||||
alignItems: "center",
|
||||
backgroundColor: "#e6e6e6",
|
||||
},
|
||||
tabButtonActive: {
|
||||
backgroundColor: "#111",
|
||||
},
|
||||
tabButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#111",
|
||||
},
|
||||
tabButtonTextActive: {
|
||||
color: "#fff",
|
||||
},
|
||||
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" },
|
||||
emptyText: {
|
||||
textAlign: "center",
|
||||
paddingVertical: 32,
|
||||
color: "#666",
|
||||
},
|
||||
errorText: {
|
||||
color: "#c62828",
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
fab:
|
||||
{
|
||||
position: "absolute",
|
||||
|
||||
146
FastNotes/app/login.tsx
Normal file
146
FastNotes/app/login.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useState } from 'react'
|
||||
import { Link, Stack } from 'expo-router'
|
||||
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
|
||||
import { supabase } from '@/libs/supabase'
|
||||
|
||||
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 onLogin = async () => {
|
||||
if (!email.trim() || !password) {
|
||||
setErrorMessage('Log in with email and password')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email: email.trim(),
|
||||
password,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
setErrorMessage(error.message)
|
||||
}
|
||||
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Login' }} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Login</Text>
|
||||
|
||||
<Text style={styles.label}>E-mail</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
onChangeText={setEmail}
|
||||
placeholder="name@email.com"
|
||||
placeholderTextColor="#666"
|
||||
style={styles.input}
|
||||
value={email}
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Password</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="password"
|
||||
onChangeText={setPassword}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#666"
|
||||
secureTextEntry
|
||||
style={styles.input}
|
||||
value={password}
|
||||
/>
|
||||
|
||||
{errorMessage ? (
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
) : null}
|
||||
|
||||
<Pressable
|
||||
disabled={isSubmitting}
|
||||
onPress={onLogin}
|
||||
style={({ pressed }) => [
|
||||
styles.loginButton,
|
||||
pressed && !isSubmitting ? styles.loginButtonPressed : null,
|
||||
isSubmitting ? styles.loginButtonDisabled : null,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.loginButtonText}>
|
||||
{isSubmitting ? 'Logging in...' : 'Log in'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Link href="/signup" style={styles.link}>
|
||||
<Text style={styles.linkText}>Create a new account</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
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',
|
||||
fontSize: 14,
|
||||
},
|
||||
loginButton: {
|
||||
borderRadius: 12,
|
||||
paddingVertical: 14,
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#111',
|
||||
marginTop: 8,
|
||||
},
|
||||
loginButtonPressed: {
|
||||
opacity: 0.85,
|
||||
},
|
||||
loginButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
loginButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
link: {
|
||||
alignSelf: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
linkText: {
|
||||
color: '#0b57d0',
|
||||
fontSize: 16,
|
||||
},
|
||||
})
|
||||
@@ -13,20 +13,34 @@ import { useHeaderHeight } from "@react-navigation/elements";
|
||||
|
||||
export default function NewNoteScreen()
|
||||
{
|
||||
const { addNote } = useNotes();
|
||||
const[title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const insets = useSafeAreaInsets();
|
||||
const headerHeight = useHeaderHeight();
|
||||
const [contentHeight, setContentHeight] = useState(160);
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
const { addNote, errorMessage } = useNotes()
|
||||
const [title, setTitle] = useState("")
|
||||
const [content, setContent] = useState("")
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [localErrorMessage, setLocalErrorMessage] = useState<string | null>(null)
|
||||
const insets = useSafeAreaInsets()
|
||||
const headerHeight = useHeaderHeight()
|
||||
const [contentHeight, setContentHeight] = useState(160)
|
||||
const scrollRef = useRef<ScrollView>(null)
|
||||
|
||||
const onSave = () =>
|
||||
const onSave = async () =>
|
||||
{
|
||||
if(!title.trim() && !content.trim()) return;
|
||||
addNote(title, content);
|
||||
router.back();
|
||||
};
|
||||
if(!title.trim() || !content.trim()) {
|
||||
setLocalErrorMessage("Title and content are required.")
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
setLocalErrorMessage(null)
|
||||
|
||||
const wasSaved = await addNote(title, content)
|
||||
|
||||
setIsSaving(false)
|
||||
|
||||
if (wasSaved) {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={{ flex: 1}} behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
@@ -43,15 +57,25 @@ export default function NewNoteScreen()
|
||||
<TextInput value={content} onChangeText={setContent} placeholder="Write your note..."
|
||||
style={[styles.input, { height: Math.max(160, contentHeight) }]} multiline
|
||||
textAlignVertical="top"
|
||||
onContentSizeChange={(e) => {setContentHeight(e.nativeEvent.contentSize.height);
|
||||
onContentSizeChange={(e) => {setContentHeight(e.nativeEvent.contentSize.height);
|
||||
scrollRef.current?.scrollToEnd({ animated: true });
|
||||
}}/>
|
||||
|
||||
{localErrorMessage ? (
|
||||
<Text style={styles.errorText}>{localErrorMessage}</Text>
|
||||
) : null}
|
||||
|
||||
{!localErrorMessage && errorMessage ? (
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
) : null}
|
||||
|
||||
</ScrollView>
|
||||
<View>
|
||||
<Pressable onPress={onSave}
|
||||
<Pressable disabled={isSaving} onPress={onSave}
|
||||
style={[styles.saveFloating, { bottom: insets.bottom + 16 }]}>
|
||||
<Text style={styles.saveFloatingText}>Save</Text>
|
||||
<Text style={styles.saveFloatingText}>
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
@@ -101,6 +125,9 @@ const styles = StyleSheet.create(
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "700"
|
||||
}
|
||||
},
|
||||
errorText: {
|
||||
color: "#c62828"
|
||||
},
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
179
FastNotes/app/signup.tsx
Normal file
179
FastNotes/app/signup.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { supabase } from "@/libs/supabase";
|
||||
import { Link, Stack } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
|
||||
|
||||
export default function SignupScreen(){
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const onSignup = async () => {
|
||||
if(!email.trim() || !password){
|
||||
setErrorMessage('Sign up using email and password')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
setErrorMessage(null)
|
||||
setSuccessMessage(null)
|
||||
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email: email.trim(),
|
||||
password
|
||||
})
|
||||
|
||||
if(error){
|
||||
setErrorMessage(error.message)
|
||||
} else if (data.session) {
|
||||
const user = data.user
|
||||
|
||||
if (user) {
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.upsert({
|
||||
id: user.id,
|
||||
email: user.email ?? email.trim(),
|
||||
})
|
||||
|
||||
if (profileError) {
|
||||
setErrorMessage(profileError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!error && data.session) {
|
||||
setSuccessMessage('Account created. You are now signed in.')
|
||||
} else if (!error) {
|
||||
setSuccessMessage('Account created. Check your email to confirm the signup.')
|
||||
}
|
||||
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{title: "Signup"}}/>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Sign up</Text>
|
||||
|
||||
<Text style={styles.label}>E-mail</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
onChangeText={setEmail}
|
||||
placeholder="name@email.com"
|
||||
placeholderTextColor="#666"
|
||||
style={styles.input}
|
||||
value={email}
|
||||
/>
|
||||
|
||||
<Text style={styles.label} >Password</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="password"
|
||||
onChangeText={setPassword}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#666"
|
||||
secureTextEntry
|
||||
style={styles.input}
|
||||
value={password}
|
||||
/>
|
||||
|
||||
{errorMessage ? (
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
) : null}
|
||||
|
||||
{successMessage ? (
|
||||
<Text style={styles.successText}>{successMessage}</Text>
|
||||
) : null}
|
||||
|
||||
<Pressable
|
||||
disabled={isSubmitting}
|
||||
onPress={onSignup}
|
||||
style={({ pressed }) => [
|
||||
styles.actionButton,
|
||||
pressed && !isSubmitting ? styles.actionButtonPressed : null,
|
||||
isSubmitting ? styles.actionButtonDisabled : null,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.actionButtonText}>
|
||||
{isSubmitting ? 'Creating account...' : 'Sign up'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Link href="/login" style={styles.link}>
|
||||
<Text style={styles.linkText}>Already have an account? Log in</Text>
|
||||
</Link>
|
||||
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
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',
|
||||
fontSize: 14,
|
||||
},
|
||||
successText: {
|
||||
color: '#2e7d32',
|
||||
fontSize: 14,
|
||||
},
|
||||
actionButton: {
|
||||
borderRadius: 12,
|
||||
paddingVertical: 14,
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#111',
|
||||
marginTop: 8,
|
||||
},
|
||||
actionButtonPressed: {
|
||||
opacity: 0.85,
|
||||
},
|
||||
actionButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
actionButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
link: {
|
||||
alignSelf: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
linkText: {
|
||||
color: '#0b57d0',
|
||||
fontSize: 16,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user