This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity } from 'react-native';
|
||||
import { Header } from '@/components/Header';
|
||||
import { ListItem } from '@/components/ListItem';
|
||||
import { OptionPickerModal } from '@/components/OptionPickerModal';
|
||||
import { CategoryEditorModal } from '@/components/CategoryEditorModal';
|
||||
import { SyncModal } from '@/components/SyncModal';
|
||||
import { FriendsModal } from '@/components/FriendsModal';
|
||||
import { LegalModal } from '@/components/LegalModal';
|
||||
import SyncStatus from '@/components/SyncStatus';
|
||||
import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS } from '@/theme';
|
||||
import { useCategories } from '@/hooks/useDatabase';
|
||||
import { getAuthUser, getAuthToken } from '@/services/auth';
|
||||
import { getLastSyncTime } from '@/database/sync';
|
||||
import Category from '@/models/Category';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference } = useSettings();
|
||||
const categories = useCategories();
|
||||
const [picker, setPicker] = useState<null | 'category' | 'sort' | 'reminder'>(null);
|
||||
const [editingCategory, setEditingCategory] = useState<Category | null | 'new'>(null);
|
||||
const [syncVisible, setSyncVisible] = useState(false);
|
||||
const [friendsVisible, setFriendsVisible] = useState(false);
|
||||
const [legalVisible, setLegalVisible] = useState<null | 'privacy' | 'terms'>(null);
|
||||
const [syncSubtitle, setSyncSubtitle] = useState('Checking...');
|
||||
|
||||
const refreshSyncStatus = async () => {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
setSyncSubtitle('Not signed in');
|
||||
return;
|
||||
}
|
||||
const user = await getAuthUser();
|
||||
const lastSync = await getLastSyncTime();
|
||||
if (user) {
|
||||
setSyncSubtitle(
|
||||
lastSync ? `Signed in · synced ${formatSyncTime(lastSync)}` : `Signed in as ${user.username}`
|
||||
);
|
||||
} else {
|
||||
setSyncSubtitle('Not signed in');
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refreshSyncStatus();
|
||||
}, []);
|
||||
|
||||
const defaultCategory = categories.find((c) => c.id === defaultCategoryId);
|
||||
const defaultCategoryLabel = defaultCategory?.name ?? (categories.length > 0 ? categories[0].name : 'None');
|
||||
const reminderLabel = REMINDER_OPTIONS.find((o) => o.value === reminderPreference)?.label ?? 'No reminder';
|
||||
|
||||
const handleDefaultCategory = (value: string) => {
|
||||
setDefaultCategoryId(value);
|
||||
};
|
||||
|
||||
const handleReminderPreference = (value: string) => {
|
||||
setReminderPreference(value as typeof reminderPreference);
|
||||
};
|
||||
|
||||
const editorVisible = editingCategory !== null;
|
||||
const editorCategory = editingCategory === 'new' ? null : editingCategory;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Header title="Settings" showLogo={false} />
|
||||
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Categories</Text>
|
||||
{categories.map((category) => (
|
||||
<ListItem
|
||||
key={category.id}
|
||||
title={category.name}
|
||||
subtitle={category.id === defaultCategoryId ? 'Default category' : undefined}
|
||||
leftElement={
|
||||
<View style={[styles.categoryDot, { backgroundColor: category.color }]} />
|
||||
}
|
||||
onPress={() => setEditingCategory(category)}
|
||||
showChevron
|
||||
/>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
style={[styles.addCategoryRow, { borderColor: theme.borderStrong }]}
|
||||
onPress={() => setEditingCategory('new')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.addIcon, { backgroundColor: theme.accentSoft }]}>
|
||||
<Svg width={16} height={16} viewBox="0 0 24 24">
|
||||
<Path d="M12 5v14M5 12h14" stroke={theme.accent} strokeWidth={2} strokeLinecap="round" />
|
||||
</Svg>
|
||||
</View>
|
||||
<Text style={[styles.addCategoryText, { color: theme.accent }]}>Add Category</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Notifications</Text>
|
||||
<ListItem
|
||||
title="Push Notifications"
|
||||
subtitle="Receive task reminders"
|
||||
rightElement={
|
||||
<Switch
|
||||
value={notifications}
|
||||
onValueChange={setNotifications}
|
||||
thumbColor="#FFFFFF"
|
||||
trackColor={{ false: theme.borderStrong, true: theme.accent }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
title="Reminder Preferences"
|
||||
subtitle={notifications ? reminderLabel : 'Enable notifications first'}
|
||||
onPress={() => notifications && setPicker('reminder')}
|
||||
showChevron
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Tasks</Text>
|
||||
<ListItem
|
||||
title="Default Category"
|
||||
subtitle={defaultCategoryLabel}
|
||||
onPress={() => setPicker('category')}
|
||||
showChevron
|
||||
/>
|
||||
<ListItem
|
||||
title="Sort Tasks By"
|
||||
subtitle={SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Date'}
|
||||
onPress={() => setPicker('sort')}
|
||||
showChevron
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Data</Text>
|
||||
<SyncStatus />
|
||||
<ListItem
|
||||
title="Backup & Sync"
|
||||
subtitle={syncSubtitle}
|
||||
onPress={() => setSyncVisible(true)}
|
||||
showChevron
|
||||
/>
|
||||
<ListItem
|
||||
title="Friends"
|
||||
subtitle="Manage friends and requests"
|
||||
onPress={() => setFriendsVisible(true)}
|
||||
showChevron
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>About</Text>
|
||||
<ListItem
|
||||
title="Version"
|
||||
subtitle="1.0.0"
|
||||
/>
|
||||
<ListItem
|
||||
title="Privacy Policy"
|
||||
onPress={() => setLegalVisible('privacy')}
|
||||
showChevron
|
||||
/>
|
||||
<ListItem
|
||||
title="Terms of Service"
|
||||
onPress={() => setLegalVisible('terms')}
|
||||
showChevron
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
<OptionPickerModal
|
||||
visible={picker === 'category'}
|
||||
title="Default Category"
|
||||
options={categories.map((c) => ({ value: c.id, label: c.name, color: c.color }))}
|
||||
selectedValue={defaultCategoryId || categories[0]?.id}
|
||||
onSelect={handleDefaultCategory}
|
||||
onClose={() => setPicker(null)}
|
||||
/>
|
||||
|
||||
<OptionPickerModal
|
||||
visible={picker === 'sort'}
|
||||
title="Sort Tasks By"
|
||||
options={SORT_OPTIONS}
|
||||
selectedValue={sortBy}
|
||||
onSelect={(value) => setSortBy(value as typeof sortBy)}
|
||||
onClose={() => setPicker(null)}
|
||||
/>
|
||||
|
||||
<OptionPickerModal
|
||||
visible={picker === 'reminder'}
|
||||
title="Reminder Preferences"
|
||||
options={REMINDER_OPTIONS}
|
||||
selectedValue={reminderPreference}
|
||||
onSelect={handleReminderPreference}
|
||||
onClose={() => setPicker(null)}
|
||||
/>
|
||||
|
||||
<CategoryEditorModal
|
||||
visible={editorVisible}
|
||||
category={editorCategory}
|
||||
categoryCount={categories.length}
|
||||
onClose={() => setEditingCategory(null)}
|
||||
/>
|
||||
|
||||
<SyncModal visible={syncVisible} onClose={() => { setSyncVisible(false); refreshSyncStatus(); }} />
|
||||
|
||||
<FriendsModal visible={friendsVisible} onClose={() => setFriendsVisible(false)} />
|
||||
|
||||
<LegalModal
|
||||
visible={legalVisible !== null}
|
||||
type={legalVisible}
|
||||
onClose={() => setLegalVisible(null)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSyncTime(timestamp: number): string {
|
||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
||||
if (seconds < 60) return 'just now';
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)} min ago`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)} h ago`;
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
marginTop: 24,
|
||||
marginBottom: 8,
|
||||
marginLeft: 4,
|
||||
},
|
||||
categoryDot: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
},
|
||||
addCategoryRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'dashed',
|
||||
marginBottom: 8,
|
||||
},
|
||||
addIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
addCategoryText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user