Polished up some UI and added dark/light mode

This commit is contained in:
Christopher Sanden
2026-03-06 17:20:41 +01:00
parent 45ab15ff40
commit 3e81e46b1a
25 changed files with 472 additions and 309 deletions

View File

@@ -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,91 +82,106 @@ export default function DetailScreen()
text: "Delete",
style: "destructive",
onPress: () => {
void onDelete();
void onDelete()
},
},
]
);
};
)
}
const onDelete = async () => {
if (!id) {
setLocalErrorMessage("This note could not be found.");
return;
setLocalErrorMessage("This note could not be found.")
return
}
setIsDeleting(true);
setLocalErrorMessage(null);
setStatusMessage(null);
setIsDeleting(true)
setLocalErrorMessage(null)
setStatusMessage(null)
const wasDeleted = await deleteNote(id);
const wasDeleted = await deleteNote(id)
setIsDeleting(false);
setIsDeleting(false)
if (wasDeleted) {
router.replace("/");
router.replace("/")
}
};
}
if (!note) {
return (
<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}>
<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"}
return(
<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, { color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]}
value={title}
placeholderTextColor={palette.mutedText}
/>
<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, { color: palette.text, borderColor: palette.border, backgroundColor: palette.input }]}
textAlignVertical="top"
value={content}
placeholderTextColor={palette.mutedText}
/>
{!canEdit ? (
<Text style={[styles.readOnlyText, { color: palette.mutedText }]}>
Only the creator of this note can update or delete it.
</Text>
</Pressable>
<Pressable disabled={isDeleting} onPress={confirmDelete} style={styles.deleteButton}>
<Text style={styles.deleteButtonText}>
{isDeleting ? "Deleting..." : "Delete note"}
</Text>
</Pressable>
</View>
) : null}
</View>
);
) : 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, { 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>
) : 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",
},
});
})