feat: add color and icon pickers for lists
Replace inline list creation with a modal supporting color palette and icon grid selection. Long-press to edit existing lists. Display chosen icon/color in list rows, detail header, and task creation chips. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5fc1365ced
commit
2e86835e44
6 changed files with 320 additions and 60 deletions
|
|
@ -1,18 +1,38 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { View, Text, FlatList, Pressable, useColorScheme, TextInput, Alert } from 'react-native';
|
||||
import {
|
||||
View, Text, FlatList, Pressable, useColorScheme, TextInput, Alert,
|
||||
Modal, KeyboardAvoidingView, Platform, ScrollView,
|
||||
} from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Plus, ChevronRight, Trash2 } from 'lucide-react-native';
|
||||
import {
|
||||
Plus, ChevronRight, Trash2, Check,
|
||||
List, ShoppingCart, Briefcase, Home, Heart, Star, BookOpen,
|
||||
GraduationCap, Dumbbell, Utensils, Plane, Music, Code, Wrench,
|
||||
Gift, Camera, Palette, Dog, Leaf, Zap,
|
||||
} from 'lucide-react-native';
|
||||
import type { LucideIcon } from 'lucide-react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { colors } from '@/src/theme/colors';
|
||||
import { useSettingsStore } from '@/src/stores/useSettingsStore';
|
||||
import { getAllLists, createList, deleteList } from '@/src/db/repository/lists';
|
||||
import { getAllLists, createList, deleteList, updateList } from '@/src/db/repository/lists';
|
||||
import { getTasksByList } from '@/src/db/repository/tasks';
|
||||
|
||||
const LIST_COLORS = ['#4A90A4', '#C17767', '#8BA889', '#D4A574', '#7B68EE', '#E57373', '#4DB6AC'];
|
||||
|
||||
const ICON_MAP: Record<string, LucideIcon> = {
|
||||
List, ShoppingCart, Briefcase, Home, Heart, Star, BookOpen,
|
||||
GraduationCap, Dumbbell, Utensils, Plane, Music, Code, Wrench,
|
||||
Gift, Camera, Palette, Dog, Leaf, Zap,
|
||||
};
|
||||
|
||||
const ICON_NAMES = Object.keys(ICON_MAP);
|
||||
|
||||
type ListWithCount = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string | null;
|
||||
icon: string | null;
|
||||
isInbox: boolean;
|
||||
taskCount: number;
|
||||
};
|
||||
|
|
@ -21,12 +41,17 @@ export default function ListsScreen() {
|
|||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [lists, setLists] = useState<ListWithCount[]>([]);
|
||||
const [showNewInput, setShowNewInput] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const systemScheme = useColorScheme();
|
||||
const theme = useSettingsStore((s) => s.theme);
|
||||
const isDark = (theme === 'system' ? systemScheme : theme) === 'dark';
|
||||
|
||||
// Modal state
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingListId, setEditingListId] = useState<string | null>(null);
|
||||
const [modalName, setModalName] = useState('');
|
||||
const [modalColor, setModalColor] = useState(LIST_COLORS[0]);
|
||||
const [modalIcon, setModalIcon] = useState<string | null>(null);
|
||||
|
||||
const loadLists = useCallback(async () => {
|
||||
const allLists = await getAllLists();
|
||||
const withCounts = await Promise.all(
|
||||
|
|
@ -47,11 +72,34 @@ export default function ListsScreen() {
|
|||
return () => clearInterval(interval);
|
||||
}, [loadLists]);
|
||||
|
||||
const handleCreateList = async () => {
|
||||
if (!newName.trim()) return;
|
||||
await createList(newName.trim());
|
||||
setNewName('');
|
||||
setShowNewInput(false);
|
||||
const openNewList = () => {
|
||||
setEditingListId(null);
|
||||
setModalName('');
|
||||
setModalColor(LIST_COLORS[0]);
|
||||
setModalIcon(null);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const openEditList = (item: ListWithCount) => {
|
||||
setEditingListId(item.id);
|
||||
setModalName(item.name);
|
||||
setModalColor(item.color || LIST_COLORS[0]);
|
||||
setModalIcon(item.icon);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSaveList = async () => {
|
||||
if (!modalName.trim()) return;
|
||||
if (editingListId) {
|
||||
await updateList(editingListId, {
|
||||
name: modalName.trim(),
|
||||
color: modalColor,
|
||||
icon: modalIcon,
|
||||
});
|
||||
} else {
|
||||
await createList(modalName.trim(), modalColor, modalIcon ?? undefined);
|
||||
}
|
||||
setShowModal(false);
|
||||
loadLists();
|
||||
};
|
||||
|
||||
|
|
@ -69,6 +117,20 @@ export default function ListsScreen() {
|
|||
]);
|
||||
};
|
||||
|
||||
const renderListIcon = (item: ListWithCount, size: number = 20) => {
|
||||
const iconColor = item.color || colors.bleu.DEFAULT;
|
||||
if (item.icon && ICON_MAP[item.icon]) {
|
||||
const IconComponent = ICON_MAP[item.icon];
|
||||
return <IconComponent size={size} color={iconColor} />;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: iconColor }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className={`flex-1 ${isDark ? 'bg-[#1A1A1A]' : 'bg-creme'}`}>
|
||||
<FlatList
|
||||
|
|
@ -78,15 +140,15 @@ export default function ListsScreen() {
|
|||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
onPress={() => router.push(`/list/${item.id}` as any)}
|
||||
onLongPress={() => { if (!item.isInbox) openEditList(item); }}
|
||||
className={`flex-row items-center justify-between border-b px-4 py-4 ${
|
||||
isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'
|
||||
}`}
|
||||
>
|
||||
<View className="flex-1 flex-row items-center">
|
||||
<View
|
||||
className="mr-3 h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: item.color || colors.bleu.DEFAULT }}
|
||||
/>
|
||||
<View className="mr-3 w-5 items-center">
|
||||
{renderListIcon(item)}
|
||||
</View>
|
||||
<Text
|
||||
className={`text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_500Medium' }}
|
||||
|
|
@ -107,37 +169,132 @@ export default function ListsScreen() {
|
|||
</View>
|
||||
</Pressable>
|
||||
)}
|
||||
ListFooterComponent={
|
||||
showNewInput ? (
|
||||
<View className="flex-row items-center px-4 py-3">
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={newName}
|
||||
onChangeText={setNewName}
|
||||
onSubmitEditing={handleCreateList}
|
||||
onBlur={() => { setShowNewInput(false); setNewName(''); }}
|
||||
placeholder={t('list.namePlaceholder')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`flex-1 rounded-lg border px-3 py-2 text-base ${
|
||||
isDark
|
||||
? 'border-[#3A3A3A] bg-[#2A2A2A] text-[#F5F5F5]'
|
||||
: 'border-[#E5E7EB] bg-white text-[#1A1A1A]'
|
||||
}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{/* FAB */}
|
||||
<Pressable
|
||||
onPress={() => setShowNewInput(true)}
|
||||
onPress={openNewList}
|
||||
className="absolute bottom-6 right-6 h-14 w-14 items-center justify-center rounded-full bg-bleu"
|
||||
style={{ elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 4 }}
|
||||
>
|
||||
<Plus size={28} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<Modal visible={showModal} transparent animationType="fade">
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
>
|
||||
<Pressable onPress={() => setShowModal(false)} className="flex-1 justify-center bg-black/40 px-6">
|
||||
<Pressable
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
className={`rounded-2xl px-5 pb-5 pt-4 ${isDark ? 'bg-[#2A2A2A]' : 'bg-white'}`}
|
||||
>
|
||||
{/* Modal title */}
|
||||
<Text
|
||||
className={`mb-4 text-lg ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_600SemiBold' }}
|
||||
>
|
||||
{editingListId ? t('list.editList') : t('list.newList')}
|
||||
</Text>
|
||||
|
||||
{/* Name input */}
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={modalName}
|
||||
onChangeText={setModalName}
|
||||
onSubmitEditing={handleSaveList}
|
||||
placeholder={t('list.namePlaceholder')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`rounded-lg border px-3 py-2.5 text-base ${isDark ? 'border-[#3A3A3A] text-[#F5F5F5]' : 'border-[#E5E7EB] text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
|
||||
{/* Color picker */}
|
||||
<Text
|
||||
className={`mb-2 mt-4 text-xs uppercase tracking-wide ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`}
|
||||
style={{ fontFamily: 'Inter_600SemiBold' }}
|
||||
>
|
||||
{t('list.colorLabel')}
|
||||
</Text>
|
||||
<View className="flex-row justify-between">
|
||||
{LIST_COLORS.map((c) => (
|
||||
<Pressable
|
||||
key={c}
|
||||
onPress={() => setModalColor(c)}
|
||||
className="h-8 w-8 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: c, borderWidth: modalColor === c ? 3 : 0, borderColor: isDark ? '#F5F5F5' : '#1A1A1A' }}
|
||||
>
|
||||
{modalColor === c && (
|
||||
<Check size={14} color="#FFFFFF" strokeWidth={3} />
|
||||
)}
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Icon picker */}
|
||||
<Text
|
||||
className={`mb-2 mt-4 text-xs uppercase tracking-wide ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`}
|
||||
style={{ fontFamily: 'Inter_600SemiBold' }}
|
||||
>
|
||||
{t('list.iconLabel')}
|
||||
</Text>
|
||||
<ScrollView style={{ maxHeight: 160 }} showsVerticalScrollIndicator={false}>
|
||||
<View className="flex-row flex-wrap">
|
||||
{/* No icon option */}
|
||||
<Pressable
|
||||
onPress={() => setModalIcon(null)}
|
||||
className={`mb-2 mr-2 h-10 w-10 items-center justify-center rounded-lg ${
|
||||
modalIcon === null
|
||||
? isDark ? 'bg-[#4A4A4A]' : 'bg-[#E5E7EB]'
|
||||
: isDark ? 'bg-[#3A3A3A]' : 'bg-[#F3F4F6]'
|
||||
}`}
|
||||
style={modalIcon === null ? { borderWidth: 2, borderColor: modalColor } : undefined}
|
||||
>
|
||||
<View className="h-3 w-3 rounded-full" style={{ backgroundColor: modalColor }} />
|
||||
</Pressable>
|
||||
{ICON_NAMES.map((name) => {
|
||||
const Icon = ICON_MAP[name];
|
||||
const isSelected = modalIcon === name;
|
||||
return (
|
||||
<Pressable
|
||||
key={name}
|
||||
onPress={() => setModalIcon(name)}
|
||||
className={`mb-2 mr-2 h-10 w-10 items-center justify-center rounded-lg ${
|
||||
isSelected
|
||||
? isDark ? 'bg-[#4A4A4A]' : 'bg-[#E5E7EB]'
|
||||
: isDark ? 'bg-[#3A3A3A]' : 'bg-[#F3F4F6]'
|
||||
}`}
|
||||
style={isSelected ? { borderWidth: 2, borderColor: modalColor } : undefined}
|
||||
>
|
||||
<Icon size={20} color={isSelected ? modalColor : isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Buttons */}
|
||||
<View className="mt-4 flex-row justify-end">
|
||||
<Pressable onPress={() => setShowModal(false)} className="mr-3 px-4 py-2">
|
||||
<Text
|
||||
className={`text-sm ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`}
|
||||
style={{ fontFamily: 'Inter_500Medium' }}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={handleSaveList} className="rounded-lg bg-bleu px-4 py-2">
|
||||
<Text className="text-sm text-white" style={{ fontFamily: 'Inter_600SemiBold' }}>
|
||||
{t('common.save')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { View, Text, FlatList, Pressable, useColorScheme, Alert } from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { ArrowLeft, Plus, ArrowUpDown, Filter } from 'lucide-react-native';
|
||||
import {
|
||||
ArrowLeft, Plus, ArrowUpDown, Filter, Download,
|
||||
List, ShoppingCart, Briefcase, Home, Heart, Star, BookOpen,
|
||||
GraduationCap, Dumbbell, Utensils, Plane, Music, Code, Wrench,
|
||||
Gift, Camera, Palette, Dog, Leaf, Zap,
|
||||
} from 'lucide-react-native';
|
||||
import type { LucideIcon } from 'lucide-react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
|
|
@ -14,11 +20,19 @@ import { getTagsForTask } from '@/src/db/repository/tags';
|
|||
import TaskItem from '@/src/components/task/TaskItem';
|
||||
import SortMenu from '@/src/components/SortMenu';
|
||||
import FilterMenu from '@/src/components/FilterMenu';
|
||||
import { exportAndShareICS } from '@/src/services/icsExport';
|
||||
|
||||
const ICON_MAP: Record<string, LucideIcon> = {
|
||||
List, ShoppingCart, Briefcase, Home, Heart, Star, BookOpen,
|
||||
GraduationCap, Dumbbell, Utensils, Plane, Music, Code, Wrench,
|
||||
Gift, Camera, Palette, Dog, Leaf, Zap,
|
||||
};
|
||||
|
||||
type Tag = { id: string; name: string; color: string };
|
||||
type Task = {
|
||||
id: string;
|
||||
title: string;
|
||||
notes: string | null;
|
||||
completed: boolean;
|
||||
priority: number;
|
||||
dueDate: Date | null;
|
||||
|
|
@ -33,6 +47,8 @@ export default function ListDetailScreen() {
|
|||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [listName, setListName] = useState('');
|
||||
const [listColor, setListColor] = useState<string | null>(null);
|
||||
const [listIcon, setListIcon] = useState<string | null>(null);
|
||||
const [showSort, setShowSort] = useState(false);
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const systemScheme = useColorScheme();
|
||||
|
|
@ -58,6 +74,8 @@ export default function ListDetailScreen() {
|
|||
const list = lists.find((l) => l.id === id);
|
||||
if (list) {
|
||||
setListName(list.isInbox ? t('list.inbox') : list.name);
|
||||
setListColor(list.color);
|
||||
setListIcon(list.icon);
|
||||
}
|
||||
}, [id, t, sortBy, sortOrder, filterPriority, filterTag, filterCompleted, filterDueDate]);
|
||||
|
||||
|
|
@ -91,6 +109,18 @@ export default function ListDetailScreen() {
|
|||
]);
|
||||
};
|
||||
|
||||
const handleExportICS = async () => {
|
||||
if (tasks.length === 0) {
|
||||
Alert.alert(t('export.noTasks'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await exportAndShareICS(tasks, listName || t('list.inbox'));
|
||||
} catch {
|
||||
// User cancelled sharing
|
||||
}
|
||||
};
|
||||
|
||||
const filtersActive = hasActiveFilters();
|
||||
|
||||
return (
|
||||
|
|
@ -105,14 +135,22 @@ export default function ListDetailScreen() {
|
|||
<Pressable onPress={() => router.back()} className="mr-3 p-1">
|
||||
<ArrowLeft size={24} color={isDark ? '#F5F5F5' : '#1A1A1A'} />
|
||||
</Pressable>
|
||||
{listIcon && ICON_MAP[listIcon] ? (
|
||||
(() => { const Icon = ICON_MAP[listIcon]; return <Icon size={20} color={listColor || colors.bleu.DEFAULT} />; })()
|
||||
) : listColor ? (
|
||||
<View className="h-3 w-3 rounded-full" style={{ backgroundColor: listColor }} />
|
||||
) : null}
|
||||
<Text
|
||||
className={`text-lg ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
className={`text-lg ${listIcon || listColor ? 'ml-2' : ''} ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_600SemiBold' }}
|
||||
>
|
||||
{listName}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<Pressable onPress={handleExportICS} className="mr-3 p-1">
|
||||
<Download size={20} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
</Pressable>
|
||||
<Pressable onPress={() => setShowSort(true)} className="mr-3 p-1">
|
||||
<ArrowUpDown size={20} color={sortBy !== 'position' ? colors.bleu.DEFAULT : isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
</Pressable>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,13 @@ import {
|
|||
Platform,
|
||||
} from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { X, Calendar, Repeat } from 'lucide-react-native';
|
||||
import {
|
||||
X, Calendar, Repeat,
|
||||
List, ShoppingCart, Briefcase, Home, Heart, Star, BookOpen,
|
||||
GraduationCap, Dumbbell, Utensils, Plane, Music, Code, Wrench,
|
||||
Gift, Camera, Palette, Dog, Leaf, Zap,
|
||||
} from 'lucide-react-native';
|
||||
import type { LucideIcon } from 'lucide-react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
|
||||
|
||||
|
|
@ -22,6 +28,12 @@ import { getPriorityOptions } from '@/src/lib/priority';
|
|||
import { RECURRENCE_OPTIONS } from '@/src/lib/recurrence';
|
||||
import TagChip from '@/src/components/task/TagChip';
|
||||
|
||||
const ICON_MAP: Record<string, LucideIcon> = {
|
||||
List, ShoppingCart, Briefcase, Home, Heart, Star, BookOpen,
|
||||
GraduationCap, Dumbbell, Utensils, Plane, Music, Code, Wrench,
|
||||
Gift, Camera, Palette, Dog, Leaf, Zap,
|
||||
};
|
||||
|
||||
export default function NewTaskScreen() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
|
@ -36,7 +48,7 @@ export default function NewTaskScreen() {
|
|||
const [dueDate, setDueDate] = useState<Date | null>(null);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const [selectedListId, setSelectedListId] = useState(params.listId ?? getInboxId());
|
||||
const [lists, setLists] = useState<{ id: string; name: string; isInbox: boolean }[]>([]);
|
||||
const [lists, setLists] = useState<{ id: string; name: string; color: string | null; icon: string | null; isInbox: boolean }[]>([]);
|
||||
const [recurrence, setRecurrence] = useState<string | null>(null);
|
||||
const [availableTags, setAvailableTags] = useState<{ id: string; name: string; color: string }[]>([]);
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||
|
|
@ -261,25 +273,35 @@ export default function NewTaskScreen() {
|
|||
{t('nav.lists')}
|
||||
</Text>
|
||||
<View className="flex-row flex-wrap">
|
||||
{lists.map((list) => (
|
||||
<Pressable
|
||||
key={list.id}
|
||||
onPress={() => setSelectedListId(list.id)}
|
||||
className={`mb-2 mr-2 rounded-full border px-3 py-1.5 ${
|
||||
selectedListId === list.id ? 'border-bleu bg-bleu/10' : isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className="text-sm"
|
||||
style={{
|
||||
fontFamily: selectedListId === list.id ? 'Inter_600SemiBold' : 'Inter_400Regular',
|
||||
color: selectedListId === list.id ? colors.bleu.DEFAULT : isDark ? '#A0A0A0' : '#6B6B6B',
|
||||
}}
|
||||
{lists.map((list) => {
|
||||
const isSelected = selectedListId === list.id;
|
||||
const chipColor = isSelected ? colors.bleu.DEFAULT : isDark ? '#A0A0A0' : '#6B6B6B';
|
||||
const IconComp = list.icon && ICON_MAP[list.icon] ? ICON_MAP[list.icon] : null;
|
||||
return (
|
||||
<Pressable
|
||||
key={list.id}
|
||||
onPress={() => setSelectedListId(list.id)}
|
||||
className={`mb-2 mr-2 flex-row items-center rounded-full border px-3 py-1.5 ${
|
||||
isSelected ? 'border-bleu bg-bleu/10' : isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'
|
||||
}`}
|
||||
>
|
||||
{list.isInbox ? t('list.inbox') : list.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
{IconComp ? (
|
||||
<IconComp size={14} color={isSelected ? colors.bleu.DEFAULT : list.color || chipColor} />
|
||||
) : list.color ? (
|
||||
<View className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: isSelected ? colors.bleu.DEFAULT : list.color }} />
|
||||
) : null}
|
||||
<Text
|
||||
className={`text-sm ${IconComp || list.color ? 'ml-1.5' : ''}`}
|
||||
style={{
|
||||
fontFamily: isSelected ? 'Inter_600SemiBold' : 'Inter_400Regular',
|
||||
color: chipColor,
|
||||
}}
|
||||
>
|
||||
{list.isInbox ? t('list.inbox') : list.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export async function getAllLists() {
|
|||
return db.select().from(lists).orderBy(lists.position);
|
||||
}
|
||||
|
||||
export async function createList(name: string, color?: string) {
|
||||
export async function createList(name: string, color?: string, icon?: string) {
|
||||
const now = new Date();
|
||||
const id = randomUUID();
|
||||
const allLists = await getAllLists();
|
||||
|
|
@ -39,6 +39,7 @@ export async function createList(name: string, color?: string) {
|
|||
id,
|
||||
name,
|
||||
color: color ?? null,
|
||||
icon: icon ?? null,
|
||||
position: maxPosition + 1,
|
||||
isInbox: false,
|
||||
createdAt: now,
|
||||
|
|
@ -47,7 +48,7 @@ export async function createList(name: string, color?: string) {
|
|||
return id;
|
||||
}
|
||||
|
||||
export async function updateList(id: string, data: { name?: string; color?: string }) {
|
||||
export async function updateList(id: string, data: { name?: string; color?: string; icon?: string | null }) {
|
||||
await db
|
||||
.update(lists)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@
|
|||
"newList": "New list",
|
||||
"namePlaceholder": "List name...",
|
||||
"deleteConfirm": "Are you sure you want to delete this list?",
|
||||
"editList": "Edit list",
|
||||
"colorLabel": "Color",
|
||||
"iconLabel": "Icon",
|
||||
"taskCount_one": "{{count}} task",
|
||||
"taskCount_other": "{{count}} tasks"
|
||||
},
|
||||
|
|
@ -92,6 +95,24 @@
|
|||
"about": "About",
|
||||
"version": "Version"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"enabled": "Reminders enabled",
|
||||
"offset": "Remind before due time",
|
||||
"atTime": "At time",
|
||||
"hoursBefore": "{{count}}h before",
|
||||
"dayBefore": "1 day before"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Calendar",
|
||||
"syncEnabled": "Sync with calendar",
|
||||
"syncDescription": "Adds tasks with due dates to the system calendar"
|
||||
},
|
||||
"export": {
|
||||
"ics": "Export as ICS",
|
||||
"success": "File exported",
|
||||
"noTasks": "No tasks to export"
|
||||
},
|
||||
"empty": {
|
||||
"inbox": "No tasks yet.\nTap + to get started.",
|
||||
"list": "This list is empty."
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@
|
|||
"newList": "Nouvelle liste",
|
||||
"namePlaceholder": "Nom de la liste...",
|
||||
"deleteConfirm": "Voulez-vous vraiment supprimer cette liste ?",
|
||||
"editList": "Modifier la liste",
|
||||
"colorLabel": "Couleur",
|
||||
"iconLabel": "Icône",
|
||||
"taskCount_one": "{{count}} tâche",
|
||||
"taskCount_other": "{{count}} tâches"
|
||||
},
|
||||
|
|
@ -92,6 +95,24 @@
|
|||
"about": "À propos",
|
||||
"version": "Version"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"enabled": "Rappels activés",
|
||||
"offset": "Rappel avant l'échéance",
|
||||
"atTime": "À l'heure",
|
||||
"hoursBefore": "{{count}}h avant",
|
||||
"dayBefore": "1 jour avant"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Calendrier",
|
||||
"syncEnabled": "Synchroniser avec le calendrier",
|
||||
"syncDescription": "Ajoute les tâches avec échéance au calendrier système"
|
||||
},
|
||||
"export": {
|
||||
"ics": "Exporter en ICS",
|
||||
"success": "Fichier exporté",
|
||||
"noTasks": "Aucune tâche à exporter"
|
||||
},
|
||||
"empty": {
|
||||
"inbox": "Aucune tâche.\nAppuyez sur + pour commencer.",
|
||||
"list": "Cette liste est vide."
|
||||
|
|
|
|||
Loading…
Reference in a new issue