finished assignment 3 - 100%
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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"}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user