diff --git a/src/db/migrations/0003_sharp_radioactive_man.sql b/src/db/migrations/0003_sharp_radioactive_man.sql new file mode 100644 index 0000000..10b3df6 --- /dev/null +++ b/src/db/migrations/0003_sharp_radioactive_man.sql @@ -0,0 +1 @@ +ALTER TABLE `tags` ADD `updated_at` integer; \ No newline at end of file diff --git a/src/db/migrations/meta/0003_snapshot.json b/src/db/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..dcd096a --- /dev/null +++ b/src/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,316 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d3023632-946c-4fe9-b543-61cdf8af873c", + "prevId": "3b2c3545-d1aa-4879-9654-4c6b58c73dc2", + "tables": { + "lists": { + "name": "lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_inbox": { + "name": "is_inbox", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#4A90A4'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_tags": { + "name": "task_tags", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "task_tags_task_id_tasks_id_fk": { + "name": "task_tags_task_id_tasks_id_fk", + "tableFrom": "task_tags", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_tags_tag_id_tags_id_fk": { + "name": "task_tags_tag_id_tags_id_fk", + "tableFrom": "task_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_tags_task_id_tag_id_pk": { + "columns": [ + "task_id", + "tag_id" + ], + "name": "task_tags_task_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed": { + "name": "completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "due_date": { + "name": "due_date", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "calendar_event_id": { + "name": "calendar_event_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_list_id_lists_id_fk": { + "name": "tasks_list_id_lists_id_fk", + "tableFrom": "tasks", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 51d5b07..738281c 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1771639773448, "tag": "0002_majestic_wendell_rand", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1775486221676, + "tag": "0003_sharp_radioactive_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/migrations/migrations.js b/src/db/migrations/migrations.js index 771ed0e..a924b11 100644 --- a/src/db/migrations/migrations.js +++ b/src/db/migrations/migrations.js @@ -4,13 +4,15 @@ import journal from './meta/_journal.json'; import m0000 from './0000_bitter_phalanx.sql'; import m0001 from './0001_sticky_arachne.sql'; import m0002 from './0002_majestic_wendell_rand.sql'; +import m0003 from './0003_sharp_radioactive_man.sql'; export default { journal, migrations: { m0000, m0001, -m0002 +m0002, +m0003 } } \ No newline at end of file diff --git a/src/db/repository/tags.ts b/src/db/repository/tags.ts index 71a015e..6ccc4ca 100644 --- a/src/db/repository/tags.ts +++ b/src/db/repository/tags.ts @@ -10,17 +10,19 @@ export async function getAllTags() { export async function createTag(name: string, color: string) { const id = randomUUID(); + const now = new Date(); await db.insert(tags).values({ id, name: truncate(name, 100), color, - createdAt: new Date(), + createdAt: now, + updatedAt: now, }); return id; } export async function updateTag(id: string, name: string, color: string) { - await db.update(tags).set({ name: truncate(name, 100), color }).where(eq(tags.id, id)); + await db.update(tags).set({ name: truncate(name, 100), color, updatedAt: new Date() }).where(eq(tags.id, id)); } export async function deleteTag(id: string) { diff --git a/src/db/repository/tasks.ts b/src/db/repository/tasks.ts index d2668c4..89308b2 100644 --- a/src/db/repository/tasks.ts +++ b/src/db/repository/tasks.ts @@ -4,7 +4,7 @@ import { tasks, taskTags } from '../schema'; import { randomUUID } from '@/src/lib/uuid'; import { getNextOccurrence, type RecurrenceType } from '@/src/lib/recurrence'; import { startOfDay, endOfDay, endOfWeek, startOfWeek } from 'date-fns'; -import type { SortBy, SortOrder, FilterCompleted, FilterDueDate } from '@/src/stores/useTaskStore'; +import type { TaskFilters, SortBy, SortOrder } from '@/src/shared/types'; import { scheduleTaskReminder, cancelTaskReminder } from '@/src/services/notifications'; import { addTaskToCalendar, updateCalendarEvent, removeCalendarEvent } from '@/src/services/calendar'; import { useSettingsStore } from '@/src/stores/useSettingsStore'; @@ -12,14 +12,7 @@ import { syncWidgetData } from '@/src/services/widgetSync'; import { clamp, truncate } from '@/src/lib/validation'; import { RECURRENCE_OPTIONS } from '@/src/lib/recurrence'; -export interface TaskFilters { - sortBy?: SortBy; - sortOrder?: SortOrder; - filterPriority?: number | null; - filterTag?: string | null; - filterCompleted?: FilterCompleted; - filterDueDate?: FilterDueDate; -} +export type { TaskFilters } from '@/src/shared/types'; export async function getTasksByList(listId: string, filters?: TaskFilters) { const conditions = [eq(tasks.listId, listId), isNull(tasks.parentId)]; diff --git a/src/db/schema.ts b/src/db/schema.ts index ae6144a..a146c37 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -33,6 +33,7 @@ export const tags = sqliteTable('tags', { name: text('name').notNull(), color: text('color').notNull().default('#4A90A4'), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp' }), }); export const taskTags = sqliteTable( diff --git a/src/lib/priority.ts b/src/lib/priority.ts index eeda797..0945988 100644 --- a/src/lib/priority.ts +++ b/src/lib/priority.ts @@ -1,29 +1 @@ -import { colors } from '@/src/theme/colors'; - -const lightColors = [ - colors.priority.none, - colors.priority.low, - colors.priority.medium, - colors.priority.high, -]; - -const darkColors = [ - colors.priority.noneLight, - colors.priority.lowLight, - colors.priority.mediumLight, - colors.priority.highLight, -]; - -export function getPriorityColor(priority: number, isDark: boolean): string { - const palette = isDark ? darkColors : lightColors; - return palette[priority] ?? palette[0]; -} - -export function getPriorityOptions(isDark: boolean) { - return [ - { value: 0, labelKey: 'priority.none', color: getPriorityColor(0, isDark) }, - { value: 1, labelKey: 'priority.low', color: getPriorityColor(1, isDark) }, - { value: 2, labelKey: 'priority.medium', color: getPriorityColor(2, isDark) }, - { value: 3, labelKey: 'priority.high', color: getPriorityColor(3, isDark) }, - ]; -} +export { getPriorityColor, getPriorityOptions } from '@/src/shared/priority'; diff --git a/src/lib/recurrence.ts b/src/lib/recurrence.ts index a60b26b..069b9ba 100644 --- a/src/lib/recurrence.ts +++ b/src/lib/recurrence.ts @@ -1,18 +1,2 @@ -import { addDays, addWeeks, addMonths, addYears } from 'date-fns'; - -export type RecurrenceType = 'daily' | 'weekly' | 'monthly' | 'yearly'; - -export const RECURRENCE_OPTIONS: RecurrenceType[] = ['daily', 'weekly', 'monthly', 'yearly']; - -export function getNextOccurrence(dueDate: Date, recurrence: RecurrenceType): Date { - switch (recurrence) { - case 'daily': - return addDays(dueDate, 1); - case 'weekly': - return addWeeks(dueDate, 1); - case 'monthly': - return addMonths(dueDate, 1); - case 'yearly': - return addYears(dueDate, 1); - } -} +export { getNextOccurrence, RECURRENCE_OPTIONS } from '@/src/shared/recurrence'; +export type { RecurrenceType } from '@/src/shared/recurrence'; diff --git a/src/shared/colors.ts b/src/shared/colors.ts new file mode 100644 index 0000000..08687ff --- /dev/null +++ b/src/shared/colors.ts @@ -0,0 +1,40 @@ +export const colors = { + bleu: { + DEFAULT: '#4A90A4', + light: '#6BAEC2', + dark: '#3A7389', + }, + creme: { + DEFAULT: '#FFF8F0', + dark: '#F5EDE3', + }, + terracotta: { + DEFAULT: '#C17767', + light: '#D49585', + dark: '#A45F50', + }, + priority: { + high: '#C17767', + medium: '#4A90A4', + low: '#8BA889', + none: '#9CA3AF', + highLight: '#E8A090', + mediumLight: '#7CC0D6', + lowLight: '#B0D4A8', + noneLight: '#C0C7CF', + }, + light: { + background: '#FFF8F0', + surface: '#FFFFFF', + text: '#1A1A1A', + textSecondary: '#6B6B6B', + border: '#E5E7EB', + }, + dark: { + background: '#1A1A1A', + surface: '#2A2A2A', + text: '#F5F5F5', + textSecondary: '#A0A0A0', + border: '#3A3A3A', + }, +} as const; diff --git a/src/shared/priority.ts b/src/shared/priority.ts new file mode 100644 index 0000000..a48fbc5 --- /dev/null +++ b/src/shared/priority.ts @@ -0,0 +1,29 @@ +import { colors } from './colors'; + +const lightColors = [ + colors.priority.none, + colors.priority.low, + colors.priority.medium, + colors.priority.high, +]; + +const darkColors = [ + colors.priority.noneLight, + colors.priority.lowLight, + colors.priority.mediumLight, + colors.priority.highLight, +]; + +export function getPriorityColor(priority: number, isDark: boolean): string { + const palette = isDark ? darkColors : lightColors; + return palette[priority] ?? palette[0]; +} + +export function getPriorityOptions(isDark: boolean) { + return [ + { value: 0, labelKey: 'priority.none', color: getPriorityColor(0, isDark) }, + { value: 1, labelKey: 'priority.low', color: getPriorityColor(1, isDark) }, + { value: 2, labelKey: 'priority.medium', color: getPriorityColor(2, isDark) }, + { value: 3, labelKey: 'priority.high', color: getPriorityColor(3, isDark) }, + ]; +} diff --git a/src/shared/recurrence.ts b/src/shared/recurrence.ts new file mode 100644 index 0000000..a60b26b --- /dev/null +++ b/src/shared/recurrence.ts @@ -0,0 +1,18 @@ +import { addDays, addWeeks, addMonths, addYears } from 'date-fns'; + +export type RecurrenceType = 'daily' | 'weekly' | 'monthly' | 'yearly'; + +export const RECURRENCE_OPTIONS: RecurrenceType[] = ['daily', 'weekly', 'monthly', 'yearly']; + +export function getNextOccurrence(dueDate: Date, recurrence: RecurrenceType): Date { + switch (recurrence) { + case 'daily': + return addDays(dueDate, 1); + case 'weekly': + return addWeeks(dueDate, 1); + case 'monthly': + return addMonths(dueDate, 1); + case 'yearly': + return addYears(dueDate, 1); + } +} diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 0000000..f188076 --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,17 @@ +export type { RecurrenceType } from './recurrence'; + +export type Priority = 0 | 1 | 2 | 3; + +export type SortBy = 'position' | 'priority' | 'dueDate' | 'title' | 'createdAt'; +export type SortOrder = 'asc' | 'desc'; +export type FilterCompleted = 'all' | 'active' | 'completed'; +export type FilterDueDate = 'all' | 'today' | 'week' | 'overdue' | 'noDate'; + +export interface TaskFilters { + sortBy?: SortBy; + sortOrder?: SortOrder; + filterPriority?: number | null; + filterTag?: string | null; + filterCompleted?: FilterCompleted; + filterDueDate?: FilterDueDate; +} diff --git a/src/stores/useTaskStore.ts b/src/stores/useTaskStore.ts index b1305eb..e130d29 100644 --- a/src/stores/useTaskStore.ts +++ b/src/stores/useTaskStore.ts @@ -1,11 +1,9 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import type { SortBy, SortOrder, FilterCompleted, FilterDueDate } from '@/src/shared/types'; -export type SortBy = 'position' | 'priority' | 'dueDate' | 'title' | 'createdAt'; -export type SortOrder = 'asc' | 'desc'; -export type FilterCompleted = 'all' | 'active' | 'completed'; -export type FilterDueDate = 'all' | 'today' | 'week' | 'overdue' | 'noDate'; +export type { SortBy, SortOrder, FilterCompleted, FilterDueDate } from '@/src/shared/types'; interface TaskStoreState { sortBy: SortBy; diff --git a/src/theme/colors.ts b/src/theme/colors.ts index 08687ff..58689a6 100644 --- a/src/theme/colors.ts +++ b/src/theme/colors.ts @@ -1,40 +1 @@ -export const colors = { - bleu: { - DEFAULT: '#4A90A4', - light: '#6BAEC2', - dark: '#3A7389', - }, - creme: { - DEFAULT: '#FFF8F0', - dark: '#F5EDE3', - }, - terracotta: { - DEFAULT: '#C17767', - light: '#D49585', - dark: '#A45F50', - }, - priority: { - high: '#C17767', - medium: '#4A90A4', - low: '#8BA889', - none: '#9CA3AF', - highLight: '#E8A090', - mediumLight: '#7CC0D6', - lowLight: '#B0D4A8', - noneLight: '#C0C7CF', - }, - light: { - background: '#FFF8F0', - surface: '#FFFFFF', - text: '#1A1A1A', - textSecondary: '#6B6B6B', - border: '#E5E7EB', - }, - dark: { - background: '#1A1A1A', - surface: '#2A2A2A', - text: '#F5F5F5', - textSecondary: '#A0A0A0', - border: '#3A3A3A', - }, -} as const; +export { colors } from '@/src/shared/colors';