feat: extract shared types, colors, priority and recurrence (#34) #41

Merged
maximus merged 2 commits from issue-34-shared-types into master 2026-04-06 16:57:30 +00:00
15 changed files with 444 additions and 103 deletions

View file

@ -0,0 +1 @@
ALTER TABLE `tags` ADD `updated_at` integer;

View 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": {}
}
}

View file

@ -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
}
]
}

View file

@ -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
}
}

View file

@ -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) {

View file

@ -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)];

View file

@ -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(

View file

@ -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';

View file

@ -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
View 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
View 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
View 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
View file

@ -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;
}

View file

@ -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;

View file

@ -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';