finished assignment 3 - 100%

This commit is contained in:
Christopher Sanden
2026-03-16 17:42:22 +01:00
parent a4c2d55aea
commit ca915ec8e8
33 changed files with 2869 additions and 711 deletions

View File

@@ -3,8 +3,11 @@ import AuthProvider from '@/providers/auth-provider'
import { NotesProvider } from "@/src/notes/NotesContext"
import { AppThemeProvider, useAppTheme } from '@/src/theme/AppThemeProvider'
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
import Constants from "expo-constants"
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import { ComponentType, PropsWithChildren, useEffect, useState } from "react"
import { Platform } from "react-native"
import 'react-native-reanimated'
import { SafeAreaProvider } from 'react-native-safe-area-context'
@@ -71,9 +74,11 @@ function ThemedRootLayout() {
<SafeAreaProvider>
<ThemeProvider value={navigationTheme}>
<AuthProvider>
<NotesProvider>
<RootNavigator />
</NotesProvider>
<NotificationProviderGate>
<NotesProvider>
<RootNavigator />
</NotesProvider>
</NotificationProviderGate>
</AuthProvider>
<StatusBar
style={colorScheme === "dark" ? "light" : "dark"}
@@ -84,6 +89,44 @@ function ThemedRootLayout() {
)
}
function NotificationProviderGate({ children }: PropsWithChildren) {
const isAndroidExpoGo = Platform.OS === "android" && Constants.executionEnvironment === "storeClient"
const [provider, setProvider] = useState<ComponentType<PropsWithChildren> | null>(null)
useEffect(() => {
if (isAndroidExpoGo) {
return
}
let isMounted = true
const loadProvider = async () => {
try {
const module = await import("@/src/notifications/PushNotificationsProvider")
if (isMounted) {
setProvider(() => module.default)
}
} catch (error) {
console.error("Failed to load push notifications provider:", error)
}
}
void loadProvider()
return () => {
isMounted = false
}
}, [isAndroidExpoGo])
if (isAndroidExpoGo || !provider) {
return children
}
const PushNotificationsProvider = provider
return <PushNotificationsProvider>{children}</PushNotificationsProvider>
}
export default function RootLayout() {
return (
<AppThemeProvider>

View File

@@ -1,18 +1,30 @@
import { useEffect, useState } from "react"
import { Alert, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from "react-native"
import {
Alert,
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
Text,
TextInput,
View,
} from "react-native"
import { router, useLocalSearchParams } from "expo-router"
import { BlurView } from "expo-blur"
import { useHeaderHeight } from "@react-navigation/elements"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import NoteImagePanel from "@/components/note-image-panel"
import UploadProgressBar from "@/components/upload-progress-bar"
import { useAuthContext } from "@/hooks/use-auth-context"
import { NoteImageChange, useNotes } from "@/src/notes/NotesContext"
import { StagedNoteImage, validateStagedNoteImage } from "@/src/notes/image-utils"
import { pickImageFromCamera, pickImageFromLibrary } from "@/src/notes/native-image-picker"
import { detailScreenStyles as styles } from "@/src/styles/app-styles"
import { useAppTheme } from "@/src/theme/AppThemeProvider"
import { useAuthContext } from "@/hooks/use-auth-context"
import { useNotes } from "@/src/notes/NotesContext"
export default function DetailScreen()
{
const { id } = useLocalSearchParams<
{
export default function DetailScreen() {
const { id } = useLocalSearchParams<{
id?: string
}>()
const { claims } = useAuthContext()
@@ -21,8 +33,11 @@ export default function DetailScreen()
const canEdit = note?.createdBy === claims?.sub
const [title, setTitle] = useState(note?.title ?? "")
const [content, setContent] = useState(note?.content ?? "")
const [stagedImage, setStagedImage] = useState<StagedNoteImage | null>(null)
const [imageChange, setImageChange] = useState<NoteImageChange>({ type: "keep" })
const [isSaving, setIsSaving] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [uploadProgress, setUploadProgress] = useState<number | null>(null)
const [localErrorMessage, setLocalErrorMessage] = useState<string | null>(null)
const [statusMessage, setStatusMessage] = useState<string | null>(null)
const insets = useSafeAreaInsets()
@@ -42,7 +57,48 @@ export default function DetailScreen()
useEffect(() => {
setTitle(note?.title ?? "")
setContent(note?.content ?? "")
}, [note?.content, note?.title])
setStagedImage(null)
setImageChange({ type: "keep" })
}, [note?.content, note?.id, note?.title])
const attachFromCamera = async () => {
try {
const image = await pickImageFromCamera()
if (image) {
validateStagedNoteImage(image)
setStagedImage(image)
setImageChange({ type: "replace", image })
setLocalErrorMessage(null)
setStatusMessage(null)
}
} catch (error) {
setLocalErrorMessage(error instanceof Error ? error.message : "The camera could not be opened.")
}
}
const attachFromGallery = async () => {
try {
const image = await pickImageFromLibrary()
if (image) {
validateStagedNoteImage(image)
setStagedImage(image)
setImageChange({ type: "replace", image })
setLocalErrorMessage(null)
setStatusMessage(null)
}
} catch (error) {
setLocalErrorMessage(error instanceof Error ? error.message : "The gallery could not be opened.")
}
}
const clearImage = () => {
setStagedImage(null)
setStatusMessage(null)
setLocalErrorMessage(null)
setImageChange(note?.imageUrl ? { type: "remove" } : { type: "keep" })
}
const onSave = async () => {
if (!id) {
@@ -56,20 +112,27 @@ export default function DetailScreen()
}
setIsSaving(true)
setUploadProgress(null)
setLocalErrorMessage(null)
setStatusMessage(null)
const wasSaved = await updateNote(id, title, content)
const wasSaved = await updateNote(id, title, content, imageChange, {
onImageUploadProgress: (progress) => {
setUploadProgress(progress.progress)
},
})
setIsSaving(false)
setUploadProgress(null)
if (wasSaved) {
setStagedImage(null)
setImageChange({ type: "keep" })
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?",
@@ -117,7 +180,17 @@ export default function DetailScreen()
)
}
return(
const currentImageUrl = imageChange.type === "remove" ? null : note.imageUrl
const currentImageMimeType = imageChange.type === "remove" ? null : note.imageMimeType
const currentImageSizeBytes = imageChange.type === "remove" ? null : note.imageSizeBytes
const isUploading = uploadProgress !== null
const imageActionsDisabled = isSaving || isUploading
const saveDisabled = isSaving
const deleteDisabled = isDeleting || isUploading
const primaryButtonStyle = saveDisabled ? styles.disabledButton : styles.enabledButtonShadow
const deleteButtonStyle = deleteDisabled ? styles.disabledButton : styles.enabledButtonShadow
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={headerHeight}
@@ -131,7 +204,10 @@ export default function DetailScreen()
<TextInput
editable={canEdit}
onChangeText={setTitle}
style={[styles.titleInput, { color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]}
style={[
styles.titleInput,
{ color: palette.text, borderColor: palette.border, backgroundColor: palette.input },
]}
value={title}
placeholderTextColor={palette.mutedText}
/>
@@ -143,11 +219,40 @@ export default function DetailScreen()
editable={canEdit}
multiline
onChangeText={setContent}
style={[styles.contentInput, { color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]}
style={[
styles.contentInput,
{ color: palette.text, borderColor: palette.border, backgroundColor: palette.input },
]}
textAlignVertical="top"
value={content}
placeholderTextColor={palette.mutedText}
/>
<NoteImagePanel
canEdit={Boolean(canEdit)}
isBusy={imageActionsDisabled}
currentImageUrl={currentImageUrl}
currentImageMimeType={currentImageMimeType}
currentImageSizeBytes={currentImageSizeBytes}
stagedImage={stagedImage}
helperText={
canEdit
? "Replacing or removing the image only takes effect after you save changes."
: "This image is attached to the note and stored in Supabase."
}
palette={palette}
primaryTextColor={colorScheme === "dark" ? "#000" : "#fff"}
onTakePhoto={() => {
void attachFromCamera()
}}
onChooseFromLibrary={() => {
void attachFromGallery()
}}
onRemoveImage={clearImage}
/>
{uploadProgress !== null ? <UploadProgressBar progress={uploadProgress} palette={palette} /> : null}
{!canEdit ? (
<Text style={[styles.readOnlyText, { color: palette.mutedText }]}>
Only the creator of this note can update or delete it.
@@ -158,95 +263,37 @@ export default function DetailScreen()
{statusMessage ? <Text style={styles.successText}>{statusMessage}</Text> : null}
</ScrollView>
{canEdit ? (
<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, { backgroundColor: palette.destructive }]}>
<Text style={styles.deleteButtonText}>
{isDeleting ? "Deleting..." : "Delete note"}
</Text>
</Pressable>
<View style={[styles.actions, { paddingBottom: insets.bottom + 16 }]}>
<BlurView
intensity={22}
tint={colorScheme}
style={styles.actionsBlur}
/>
<View style={[styles.actionsContent, { borderColor: palette.border }]}>
<Pressable
disabled={saveDisabled}
onPress={() => {
void onSave()
}}
style={[styles.primaryButton, primaryButtonStyle, { backgroundColor: palette.accent }]}
>
<Text style={[styles.primaryButtonText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>
{isSaving ? "Saving..." : "Save changes"}
</Text>
</Pressable>
<Pressable
disabled={deleteDisabled}
onPress={confirmDelete}
style={[styles.deleteButton, deleteButtonStyle, { backgroundColor: palette.destructive }]}
>
<Text style={styles.deleteButtonText}>
{isDeleting ? "Deleting..." : "Delete note"}
</Text>
</Pressable>
</View>
</View>
) : null}
</View>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create(
{
keyboardAvoider: { flex: 1 },
container: { flex: 1 },
formContent: { padding: 16, gap: 12 },
title: { fontSize: 22, fontWeight:"700" },
content: { fontSize: 16 },
titleInput: {
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 22,
fontWeight: "700",
},
signature: {
fontSize: 12,
color: "#666",
},
contentInput: {
minHeight: 200,
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
},
errorText: {
color: "#c62828",
},
successText: {
color: "#2e7d32",
},
readOnlyText: {
color: "#666",
},
actions: {
position: "absolute",
left: 16,
right: 16,
flexDirection: "row",
gap: 12,
},
primaryButton: {
flex: 1,
borderRadius: 12,
paddingVertical: 14,
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.18,
shadowOffset: { width: 0, height: 8 },
shadowRadius: 16,
elevation: 8,
},
primaryButtonText: {
fontSize: 16,
fontWeight: "700",
},
deleteButton: {
flex: 1,
borderRadius: 12,
paddingVertical: 14,
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.18,
shadowOffset: { width: 0, height: 8 },
shadowRadius: 16,
elevation: 8,
},
deleteButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "700",
},
})

View File

@@ -1,11 +1,13 @@
import { useMemo, useState } from "react"
import { Appearance, FlatList, Pressable, StyleSheet, Text, View } from "react-native"
import { FlatList, Pressable, Text, View } from "react-native"
import { router } from "expo-router"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { Image } from "expo-image"
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"
import { homeScreenStyles as styles } from "@/src/styles/app-styles"
type TabKey = "my-notes" | "work-notes"
@@ -109,12 +111,23 @@ export default function HomeScreen()
})
}
>
<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>
<View style={styles.noteCardRow}>
<View style={styles.noteBody}>
<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>
</View>
{item.imageUrl ? (
<View style={styles.noteThumbnailFrame}>
<Image source={{ uri: item.imageUrl }} style={styles.noteThumbnail} contentFit="contain" />
</View>
) : null}
</View>
</Pressable>
)}
/>
@@ -130,70 +143,3 @@ export default function HomeScreen()
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1 },
topBar: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
paddingBottom: 8,
borderBottomWidth: 1,
},
screenTitle: {
fontSize: 24,
fontWeight: "700",
},
tabBar: {
flexDirection: "row",
gap: 8,
paddingHorizontal: 16,
paddingBottom: 8,
paddingTop: 12
},
tabButton: {
flex: 1,
borderWidth: 1,
borderRadius: 999,
paddingVertical: 10,
alignItems: "center",
},
tabButtonActive: {
backgroundColor: Appearance.getColorScheme() === "light" ? "#000000":"#8a8888",
},
tabButtonText: {
fontSize: 14,
fontWeight: "600",
},
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 },
noteMeta: { fontSize: 12 },
emptyText: {
textAlign: "center",
paddingVertical: 32,
color: "#666",
},
errorText: {
color: "#c62828",
paddingHorizontal: 16,
paddingBottom: 8,
},
fab:
{
position: "absolute",
width: 56, height: 56, borderRadius: 28,
alignItems: "center", justifyContent: "center",
shadowColor: "#000",
shadowOpacity: 0.18,
shadowOffset: { width: 0, height: 8 },
shadowRadius: 16,
elevation: 8,
},
fabText: { fontSize: 28, lineHeight: 28, fontWeight: "700"}
})

View File

@@ -1,8 +1,9 @@
import { useState } from 'react'
import { Link, Stack } from 'expo-router'
import { Appearance, Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
import { Pressable, Text, TextInput, View } from 'react-native'
import { supabase } from '@/libs/supabase'
import { useAppTheme } from '@/src/theme/AppThemeProvider'
import { loginScreenStyles as styles } from '@/src/styles/app-styles'
export default function LoginScreen() {
const [email, setEmail] = useState('')
@@ -87,56 +88,3 @@ export default function LoginScreen() {
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 24,
gap: 12,
},
title: {
fontSize: 28,
fontWeight: '700',
},
label: {
fontSize: 14,
fontWeight: '600',
},
input: {
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
},
errorText: {
color: '#c62828',
fontSize: 14,
},
loginButton: {
borderRadius: 12,
paddingVertical: 14,
alignItems: 'center',
backgroundColor: Appearance.getColorScheme() === "light" ? '#000000':'#696969',
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,
},
})

View File

@@ -1,23 +1,33 @@
import
{
StyleSheet, Text, View, KeyboardAvoidingView,
Platform, Pressable, TextInput, ScrollView
import { useRef, useState } from "react"
import {
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
Text,
TextInput,
View,
} 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 { router } from "expo-router"
import { BlurView } from "expo-blur"
import { useHeaderHeight } from "@react-navigation/elements"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import NoteImagePanel from "@/components/note-image-panel"
import UploadProgressBar from "@/components/upload-progress-bar"
import { useNotes } from "@/src/notes/NotesContext"
import { StagedNoteImage, validateStagedNoteImage } from "@/src/notes/image-utils"
import { newNoteScreenStyles as styles } from "@/src/styles/app-styles"
import { pickImageFromCamera, pickImageFromLibrary } from "@/src/notes/native-image-picker"
import { useAppTheme } from "@/src/theme/AppThemeProvider"
export default function NewNoteScreen()
{
export default function NewNoteScreen() {
const { addNote, errorMessage } = useNotes()
const [title, setTitle] = useState("")
const [content, setContent] = useState("")
const [stagedImage, setStagedImage] = useState<StagedNoteImage | null>(null)
const [isSaving, setIsSaving] = useState(false)
const [uploadProgress, setUploadProgress] = useState<number | null>(null)
const [localErrorMessage, setLocalErrorMessage] = useState<string | null>(null)
const insets = useSafeAreaInsets()
const headerHeight = useHeaderHeight()
@@ -25,114 +35,163 @@ export default function NewNoteScreen()
const [contentHeight, setContentHeight] = useState(160)
const scrollRef = useRef<ScrollView>(null)
const onSave = async () =>
{
if(!title.trim() || !content.trim()) {
const attachFromCamera = async () => {
try {
const image = await pickImageFromCamera()
if (image) {
validateStagedNoteImage(image)
setStagedImage(image)
setLocalErrorMessage(null)
}
} catch (error) {
setLocalErrorMessage(error instanceof Error ? error.message : "The camera could not be opened.")
}
}
const attachFromGallery = async () => {
try {
const image = await pickImageFromLibrary()
if (image) {
validateStagedNoteImage(image)
setStagedImage(image)
setLocalErrorMessage(null)
}
} catch (error) {
setLocalErrorMessage(error instanceof Error ? error.message : "The gallery could not be opened.")
}
}
const onSave = async () => {
if (!title.trim() || !content.trim()) {
setLocalErrorMessage("Title and content are required.")
return
}
setIsSaving(true)
setUploadProgress(null)
setLocalErrorMessage(null)
const wasSaved = await addNote(title, content)
const wasSaved = await addNote(title, content, stagedImage, {
onImageUploadProgress: (progress) => {
setUploadProgress(progress.progress)
},
})
setIsSaving(false)
setUploadProgress(null)
if (wasSaved) {
router.back()
if (router.canGoBack()) {
router.back()
return
}
router.replace("/index")
}
}
const saveDisabled = isSaving
const imageActionsDisabled = isSaving
const saveButtonStyle = saveDisabled ? styles.disabledButton : styles.enabledButtonShadow
return (
<KeyboardAvoidingView
style={styles.keyboardAvoider}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={headerHeight}>
<View style={[styles.container, { backgroundColor: palette.background }]}>
<ScrollView ref={scrollRef} contentContainerStyle={[styles.formContent, { paddingBottom: insets.bottom + 112 }]}
keyboardShouldPersistTaps="handled">
<TextInput value={title} onChangeText={setTitle}
placeholder="Give it a title..." style={[styles.titleInput, { color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]}
placeholderTextColor={palette.mutedText}
returnKeyType="next"/>
<KeyboardAvoidingView
style={styles.keyboardAvoider}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={headerHeight}
>
<View style={[styles.container, { backgroundColor: palette.background }]}>
<ScrollView
ref={scrollRef}
contentContainerStyle={[styles.formContent, { paddingBottom: insets.bottom + 112 }]}
keyboardShouldPersistTaps="handled"
>
<TextInput
value={title}
onChangeText={setTitle}
placeholder="Give it a title..."
style={[
styles.titleInput,
{ color: palette.text, borderColor: palette.border, backgroundColor: palette.input },
]}
placeholderTextColor={palette.mutedText}
returnKeyType="next"
/>
<TextInput value={content} onChangeText={setContent} placeholder="Write your note..."
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 })
}}/>
<TextInput
value={content}
onChangeText={setContent}
placeholder="Write your note..."
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 })
}}
/>
{localErrorMessage ? (
<Text style={styles.errorText}>{localErrorMessage}</Text>
) : null}
<NoteImagePanel
canEdit
isBusy={imageActionsDisabled}
stagedImage={stagedImage}
helperText="Images upload when you save the note. Allowed formats: PNG, JPG, WEBP. Max 15 MB after compression."
palette={palette}
primaryTextColor={colorScheme === "dark" ? "#000" : "#fff"}
onTakePhoto={() => {
void attachFromCamera()
}}
onChooseFromLibrary={() => {
void attachFromGallery()
}}
onRemoveImage={() => {
setStagedImage(null)
setLocalErrorMessage(null)
}}
/>
{!localErrorMessage && errorMessage ? (
<Text style={styles.errorText}>{errorMessage}</Text>
) : null}
{uploadProgress !== null ? <UploadProgressBar progress={uploadProgress} palette={palette} /> : null}
</ScrollView>
<View style={[styles.actions, { bottom: insets.bottom + 16 }]}>
<Pressable disabled={isSaving} onPress={onSave}
style={[styles.saveButton, { backgroundColor: palette.accent }]}>
<Text style={[styles.saveFloatingText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>
{isSaving ? "Saving..." : "Save note"}
</Text>
</Pressable>
{localErrorMessage ? (
<Text style={styles.errorText}>{localErrorMessage}</Text>
) : null}
{!localErrorMessage && errorMessage ? (
<Text style={styles.errorText}>{errorMessage}</Text>
) : null}
</ScrollView>
<View style={[styles.actions, { paddingBottom: insets.bottom + 16 }]}>
<BlurView
intensity={22}
tint={colorScheme}
style={styles.actionsBlur}
/>
<View style={[styles.actionsContent, { borderColor: palette.border }]}>
<Pressable
disabled={saveDisabled}
onPress={() => {
void onSave()
}}
style={[styles.saveButton, saveButtonStyle, { backgroundColor: palette.accent }]}
>
<Text style={[styles.saveFloatingText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>
{isSaving ? "Saving..." : "Save note"}
</Text>
</Pressable>
</View>
</View>
</View>
</View>
</KeyboardAvoidingView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create(
{
keyboardAvoider: { flex: 1 },
container: { flex: 1 },
formContent: { padding: 16, gap: 12 },
titleInput:
{
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 22,
fontWeight: "700",
},
contentInput:
{
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
},
actions: {
position: "absolute",
left: 16,
right: 16,
flexDirection: "row",
gap: 12,
},
saveButton:
{
flex: 1,
borderRadius: 12,
paddingVertical: 14,
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.18,
shadowOffset: { width: 0, height: 8 },
shadowRadius: 16,
elevation: 8,
},
saveFloatingText:
{
fontSize: 16,
fontWeight: "700"
},
errorText: {
color: "#c62828"
},
}
)

View File

@@ -1,8 +1,9 @@
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 { Pressable, Text, TextInput, View } from 'react-native'
import { useAppTheme } from "@/src/theme/AppThemeProvider"
import { signupScreenStyles as styles } from "@/src/styles/app-styles"
export default function SignupScreen(){
const [email, setEmail] = useState('')
@@ -115,61 +116,3 @@ export default function SignupScreen(){
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 24,
gap: 12,
},
title: {
fontSize: 28,
fontWeight: '700',
},
label: {
fontSize: 14,
fontWeight: '600',
},
input: {
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
},
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,
},
})