feat: extract shared types, colors, priority and recurrence (#34) #41
15 changed files with 444 additions and 103 deletions
1
src/db/migrations/0003_sharp_radioactive_man.sql
Normal file
1
src/db/migrations/0003_sharp_radioactive_man.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `tags` ADD `updated_at` integer;
|
||||
316
src/db/migrations/meta/0003_snapshot.json
Normal file
316
src/db/migrations/meta/0003_snapshot.json
Normal file
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)];
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
40
src/shared/colors.ts
Normal file
40
src/shared/colors.ts
Normal file
|
|
@ -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;
|
||||
29
src/shared/priority.ts
Normal file
29
src/shared/priority.ts
Normal file
|
|
@ -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) },
|
||||
];
|
||||
}
|
||||
18
src/shared/recurrence.ts
Normal file
18
src/shared/recurrence.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
17
src/shared/types.ts
Normal file
17
src/shared/types.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export type RecurrenceType = 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue