This commit is contained in:
2026-04-21 18:46:41 +02:00
parent 99ed8bae59
commit cb7555590f
6 changed files with 265 additions and 110 deletions

View File

@@ -7,6 +7,7 @@ import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import "react-native-reanimated"; import "react-native-reanimated";
import { BirthdaysProvider } from "@/context/birthdays-context";
import { useColorScheme } from "@/hooks/use-color-scheme"; import { useColorScheme } from "@/hooks/use-color-scheme";
export const unstable_settings = { export const unstable_settings = {
@@ -17,12 +18,14 @@ export default function RootLayout() {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
return ( return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}> <BirthdaysProvider>
<Stack> <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack.Screen name="index" options={{ headerShown: false }} /> <Stack>
<Stack.Screen name="add" options={{ headerShown: false }} /> <Stack.Screen name="index" options={{ headerShown: false }} />
</Stack> <Stack.Screen name="add" options={{ headerShown: false }} />
<StatusBar style="auto" /> </Stack>
</ThemeProvider> <StatusBar style="auto" />
</ThemeProvider>
</BirthdaysProvider>
); );
} }

View File

@@ -1,25 +1,33 @@
import DateTimePicker from "@react-native-community/datetimepicker"; import DateTimePicker, {
DateTimePickerEvent,
} from "@react-native-community/datetimepicker";
import { useBirthdays } from "@/context/birthdays-context";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
Button, Alert,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
TouchableOpacity,
View, View,
} from "react-native"; } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
export default function SimpleForm() { export default function SimpleForm() {
const router = useRouter(); const router = useRouter();
const { addBirthday } = useBirthdays();
const [name, setName] = useState(""); const [name, setName] = useState("");
const [date, setDate] = useState(new Date()); const [date, setDate] = useState(new Date());
const [showPicker, setShowPicker] = useState(false); const [showPicker, setShowPicker] = useState(false);
const onChangeDate = (event, selectedDate) => { const onChangeDate = (
event: DateTimePickerEvent,
selectedDate?: Date
) => {
setShowPicker(Platform.OS === "ios"); setShowPicker(Platform.OS === "ios");
if (selectedDate) { if (selectedDate) {
setDate(selectedDate); setDate(selectedDate);
@@ -27,8 +35,18 @@ export default function SimpleForm() {
}; };
const handleSubmit = () => { const handleSubmit = () => {
alert(`Name: ${name}\nDate: ${date.toDateString()}`); if (!name.trim()) {
router.push("/"); Alert.alert("Missing name", "Enter a name before saving.");
return;
}
addBirthday({
name,
date: formatBirthdayDate(date),
});
setName("");
setDate(new Date());
router.back();
}; };
return ( return (
@@ -37,36 +55,53 @@ export default function SimpleForm() {
style={styles.keyboardContainer} style={styles.keyboardContainer}
behavior={Platform.OS === "ios" ? "padding" : undefined} behavior={Platform.OS === "ios" ? "padding" : undefined}
> >
<ScrollView <View style={styles.content}>
contentContainerStyle={styles.scrollContent} <Text style={styles.title}>Add Birthday</Text>
keyboardShouldPersistTaps="handled" <ScrollView
> contentContainerStyle={styles.scrollContent}
<View style={styles.formCard}> keyboardShouldPersistTaps="handled"
<Text style={styles.label}>Name:</Text> >
<TextInput <View style={styles.formCard}>
style={styles.input} <Text style={styles.label}>Name:</Text>
placeholder="Enter your name" <TextInput
value={name} style={styles.input}
onChangeText={setName} placeholder="Enter name of the person"
/> value={name}
onChangeText={setName}
<Text style={styles.label}>Date:</Text>
<Button title="Select Date" onPress={() => setShowPicker(true)} />
<Text style={styles.dateText}>{date.toDateString()}</Text>
{showPicker && (
<DateTimePicker
value={date}
mode="date"
display="default"
onChange={onChangeDate}
/> />
)}
<Button title="Submit" onPress={handleSubmit} /> <Text style={styles.label}>Date:</Text>
</View> <TouchableOpacity
</ScrollView> onPress={() => {
setShowPicker(true);
}}
style={[{ backgroundColor: "#00adaa" }, styles.button]}
>
<Text style={styles.buttonText}>Select Date</Text>
</TouchableOpacity>
<Text style={styles.dateText}>
{"Selected date: " + date.toDateString()}
</Text>
{showPicker && (
<DateTimePicker
value={date}
mode="date"
display="default"
onChange={onChangeDate}
/>
)}
<TouchableOpacity
onPress={handleSubmit}
style={[{ backgroundColor: "#00984c" }, styles.button]}
>
<Text style={styles.buttonText}>Save</Text>
</TouchableOpacity>
</View>
</ScrollView>
</View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
); );
@@ -76,30 +111,63 @@ const styles = StyleSheet.create({
safeArea: { safeArea: {
flex: 1, flex: 1,
backgroundColor: "#fff", backgroundColor: "#fff",
justifyContent: "center",
marginBottom: 0,
}, },
keyboardContainer: { keyboardContainer: {
flex: 1, flex: 1,
}, },
content: {
flex: 1,
paddingHorizontal: 16,
paddingTop: 12,
},
title: {
fontSize: 24,
fontWeight: "bold",
paddingHorizontal: 5,
paddingBottom: 10,
},
scrollContent: { scrollContent: {
flexGrow: 1, flexGrow: 1,
justifyContent: "center", paddingHorizontal: 20,
padding: 20, paddingBottom: 20,
}, },
formCard: { formCard: {
gap: 12, gap: 12,
}, },
label: { label: {
paddingTop: 20,
fontSize: 16, fontSize: 16,
}, },
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: "#ccc", borderColor: "#ccc",
padding: 10, padding: 10,
marginBottom: 15,
borderRadius: 5, borderRadius: 5,
}, },
dateText: { dateText: {
fontSize: 16, fontSize: 16,
marginBottom: 8, marginBottom: 8,
}, },
button: {
borderRadius: 10,
paddingVertical: 10,
paddingHorizontal: 12,
elevation: 8
},
buttonText: {
fontSize: 18,
color: "#fff",
fontWeight: "bold",
alignSelf: "center"
}
}); });
function formatBirthdayDate(date: Date) {
const year = date.getFullYear();
const month = `${date.getMonth() + 1}`.padStart(2, "0");
const day = `${date.getDate()}`.padStart(2, "0");
return `${year}-${month}-${day}`;
}

View File

@@ -1,12 +1,25 @@
import { StyleSheet, View } from "react-native"; import { StyleSheet, Text, View, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
import { BirthdayList } from "@/components/birthdate-list"; import { BirthdayList } from "@/components/birthdate-list";
import { useRouter } from "expo-router";
export default function HomeScreen() { export default function HomeScreen() {
const router = useRouter();
return ( return (
<SafeAreaView style={styles.screen} edges={["top", "bottom"]}> <SafeAreaView style={styles.screen} edges={["top", "bottom"]}>
<View style={styles.content}> <View style={styles.content}>
<View style={styles.actionsContainer}>
<Text style={styles.title}>Upcoming Birthdays</Text>
<TouchableOpacity
onPress={() => router.push("/add")}
style={styles.addButton}
>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
<BirthdayList /> <BirthdayList />
</View> </View>
</SafeAreaView> </SafeAreaView>
@@ -23,4 +36,30 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
paddingTop: 12, paddingTop: 12,
}, },
title: {
fontSize: 24,
fontWeight: "bold",
paddingHorizontal: 5,
paddingBottom: 10,
},
addButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#007AFF",
justifyContent: "center",
alignItems: "center",
},
addButtonText: {
fontSize: 24,
color: "white",
fontWeight: "bold",
},
actionsContainer: {
flexDirection: "row",
justifyContent: "space-between",
gap: 12,
paddingHorizontal: 5,
paddingBottom: 10,
},
}); });

View File

@@ -21,7 +21,7 @@ const styles = StyleSheet.create({
padding: 15, padding: 15,
borderRadius: 8, borderRadius: 8,
backgroundColor: "#f0f0f0", backgroundColor: "#f0f0f0",
boxShadow: "0 1px 4px rgba(0,0,0,0.3)", elevation: 2,
marginBottom: 10, marginBottom: 10,
}, },
name: { name: {

View File

@@ -1,60 +1,23 @@
// birthdate-list.tsx
import { useRouter } from "expo-router";
import { import {
SectionList, SectionList,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity,
View, View,
} from "react-native"; } from "react-native";
import { BirthdayEntry, useBirthdays } from "@/context/birthdays-context";
import { BirthdayItem } from "./birthdate-item"; import { BirthdayItem } from "./birthdate-item";
const DATA = [
{ id: "1", name: "John Doe", date: "2024-01-15" },
{ id: "2", name: "Jane Smith", date: "2024-01-15" },
{ id: "3", name: "Bob Johnson", date: "2024-02-10" },
{ id: "4", name: "Alice Brown", date: "2024-02-10" },
{ id: "5", name: "Charlie Wilson", date: "2024-01-20" },
{ id: "6", name: "Bob Johnson", date: "2024-02-10" },
{ id: "7", name: "Alice Brown", date: "2024-02-10" },
{ id: "8", name: "Charlie Wilson", date: "2024-01-20" },
{ id: "9", name: "Bob Johnson", date: "2024-02-10" },
{ id: "10", name: "Alice Brown", date: "2024-02-10" },
{ id: "11", name: "Charlie Wilson", date: "2024-01-20" },
{ id: "12", name: "Charlie Wilson", date: "2024-01-20" },
{ id: "13", name: "Charlie Wilson", date: "2024-01-20" },
{ id: "14", name: "Charlie Wilson", date: "2024-01-20" },
];
interface BirthdayItemData {
id: string;
name: string;
date: string;
}
interface SectionData { interface SectionData {
title: string; title: string;
data: BirthdayItemData[]; data: BirthdayEntry[];
} }
export function BirthdayList() { export function BirthdayList() {
const router = useRouter(); const { birthdays } = useBirthdays();
const groupedData = groupBirthdaysByDate(DATA); const groupedData = groupBirthdaysByDate(birthdays);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.titleContainer}>
<Text style={{ fontSize: 24, fontWeight: "bold" }}>
Upcoming Birthdays
</Text>
<TouchableOpacity
onPress={() => router.push("/add")}
style={styles.addButton}
>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
<SectionList <SectionList
sections={groupedData} sections={groupedData}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
@@ -64,9 +27,17 @@ export function BirthdayList() {
style={styles.list} style={styles.list}
contentContainerStyle={styles.listContent} contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text style={styles.emptyTitle}>No birthdays yet</Text>
<Text style={styles.emptyText}>
Add one from the form and it will appear here.
</Text>
</View>
}
renderSectionHeader={({ section: { title } }) => ( renderSectionHeader={({ section: { title } }) => (
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<Text style={styles.sectionHeader}>{title}</Text> <Text style={styles.sectionHeader}>{formatDisplayDate(title)}</Text>
</View> </View>
)} )}
/> />
@@ -75,10 +46,10 @@ export function BirthdayList() {
} }
// Helper function to group and sort // Helper function to group and sort
function groupBirthdaysByDate(data: BirthdayItemData[]): SectionData[] { function groupBirthdaysByDate(data: BirthdayEntry[]): SectionData[] {
// Sort by date // Sort by date
const sorted = [...data].sort((a, b) => { const sorted = [...data].sort((a, b) => {
return new Date(a.date).getTime() - new Date(b.date).getTime(); return parseBirthdayDate(a.date).getTime() - parseBirthdayDate(b.date).getTime();
}); });
// Group by date // Group by date
@@ -97,37 +68,28 @@ function groupBirthdaysByDate(data: BirthdayItemData[]): SectionData[] {
return grouped; return grouped;
} }
function parseBirthdayDate(date: string) {
return new Date(`${date}T00:00:00`);
}
function formatDisplayDate(date: string) {
return parseBirthdayDate(date).toLocaleDateString(undefined, {
month: "long",
day: "numeric",
year: "numeric",
});
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
titleContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
gap: 12,
paddingHorizontal: 5,
paddingVertical: 10,
},
list: { list: {
flex: 1, flex: 1,
}, },
listContent: { listContent: {
paddingBottom: 20, paddingBottom: 20,
}, },
addButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#007AFF",
justifyContent: "center",
alignItems: "center",
},
addButtonText: {
fontSize: 24,
color: "white",
fontWeight: "bold",
},
headerContainer: { headerContainer: {
paddingVertical: 10, paddingVertical: 10,
paddingHorizontal: 5, paddingHorizontal: 5,
@@ -136,4 +98,17 @@ const styles = StyleSheet.create({
fontSize: 18, fontSize: 18,
fontWeight: "bold", fontWeight: "bold",
}, },
emptyState: {
paddingHorizontal: 5,
paddingTop: 24,
gap: 6,
},
emptyTitle: {
fontSize: 18,
fontWeight: "bold",
},
emptyText: {
color: "#666",
fontSize: 15,
},
}); });

View File

@@ -0,0 +1,70 @@
import {
createContext,
ReactNode,
useContext,
useMemo,
useState,
} from "react";
export interface BirthdayEntry {
id: string;
name: string;
date: string;
}
interface AddBirthdayInput {
name: string;
date: string;
}
interface BirthdaysContextValue {
birthdays: BirthdayEntry[];
addBirthday: (input: AddBirthdayInput) => void;
}
const BirthdaysContext = createContext<BirthdaysContextValue | undefined>(
undefined
);
export function BirthdaysProvider({ children }: { children: ReactNode }) {
const [birthdays, setBirthdays] = useState<BirthdayEntry[]>([]);
const value = useMemo(
() => ({
birthdays,
addBirthday: ({ name, date }: AddBirthdayInput) => {
const trimmedName = name.trim();
if (!trimmedName) {
return;
}
setBirthdays((currentBirthdays) => [
...currentBirthdays,
{
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: trimmedName,
date,
},
]);
},
}),
[birthdays]
);
return (
<BirthdaysContext.Provider value={value}>
{children}
</BirthdaysContext.Provider>
);
}
export function useBirthdays() {
const context = useContext(BirthdaysContext);
if (!context) {
throw new Error("useBirthdays must be used within a BirthdaysProvider");
}
return context;
}