feat(frontend): setup frontend foundation

- Install dependencies: date-fns, @vite-pwa/sveltekit, workbox-window
- Configure Vite with PWA support and manifest
- Update SvelteKit config for static adapter with fallback
- Create environment files (.env, .env.example, .env.production)
- Create directory structure for lib, components, routes
- Add JSDoc type definitions for Task, User, AuthTokens, etc.
- Add utility functions: storage (localStorage wrapper), uuid, dates, sync-queue
- Configure PWA with icons, theme colors, and caching strategies
This commit is contained in:
2026-01-06 15:43:39 +01:00
parent 4eb18388db
commit 41795d1827
10 changed files with 1071 additions and 3 deletions
+81
View File
@@ -0,0 +1,81 @@
/**
* Task status enumeration
* @typedef {'P'|'C'|'D'|'R'} TaskStatus
*/
/**
* Task priority enumeration
* @typedef {0|1|2|3} TaskPriority
*/
/**
* @typedef {Object} Task
* @property {string} uuid
* @property {number} id
* @property {TaskStatus} status
* @property {string} description
* @property {string|null} project
* @property {TaskPriority} priority
* @property {number} created
* @property {number} modified
* @property {number|null} start
* @property {number|null} end
* @property {number|null} due
* @property {number|null} scheduled
* @property {number|null} wait
* @property {number|null} until
* @property {number|null} recurrence_duration
* @property {string|null} parent_uuid
* @property {string[]} tags
*/
/**
* @typedef {Object} APIResponse
* @template T
* @property {boolean} success
* @property {T} [data]
* @property {string} [error]
*/
/**
* @typedef {Object} AuthTokens
* @property {string} access_token
* @property {string} refresh_token
* @property {number} expires_at
* @property {string} token_type
*/
/**
* @typedef {Object} User
* @property {number} id
* @property {string} username
* @property {string|null} email
*/
/**
* @typedef {Object} SyncResult
* @property {number} pulled
* @property {number} pushed
* @property {number} conflicts_resolved
* @property {number} queued_offline
* @property {string[]} errors
*/
/**
* @typedef {Object} QueuedChange
* @property {string} id
* @property {number} timestamp
* @property {string} task_uuid
* @property {'create'|'update'|'delete'} type
* @property {Partial<Task>} data
*/
/**
* @typedef {Object} TaskFilters
* @property {TaskStatus} [status]
* @property {string} [project]
* @property {string} [priority]
* @property {string[]} [tags]
*/
export {};
+55
View File
@@ -0,0 +1,55 @@
import { format, formatDistance, isToday, isTomorrow, isPast } from 'date-fns';
/**
* Format Unix timestamp to readable date
* @param {number|null} timestamp - Unix timestamp (seconds)
* @param {string} formatStr - date-fns format string
* @returns {string}
*/
export function formatDate(timestamp, formatStr = 'MMM d, yyyy') {
if (!timestamp) return '';
return format(new Date(timestamp * 1000), formatStr);
}
/**
* Format Unix timestamp to relative time
* @param {number|null} timestamp
* @returns {string}
*/
export function formatRelative(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
if (isToday(date)) return 'Today';
if (isTomorrow(date)) return 'Tomorrow';
return formatDistance(date, new Date(), { addSuffix: true });
}
/**
* Check if timestamp is overdue
* @param {number|null} timestamp
* @returns {boolean}
*/
export function isOverdue(timestamp) {
if (!timestamp) return false;
return isPast(new Date(timestamp * 1000));
}
/**
* Convert Date object to Unix timestamp
* @param {Date} date
* @returns {number}
*/
export function toUnix(date) {
return Math.floor(date.getTime() / 1000);
}
/**
* Convert Unix timestamp to Date object
* @param {number} timestamp
* @returns {Date}
*/
export function fromUnix(timestamp) {
return new Date(timestamp * 1000);
}
+60
View File
@@ -0,0 +1,60 @@
import { browser } from '$app/environment';
/**
* Get item from localStorage
* @param {string} key
* @returns {any}
*/
export function getItem(key) {
if (!browser) return null;
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error(`Failed to get item ${key}:`, error);
return null;
}
}
/**
* Set item in localStorage
* @param {string} key
* @param {any} value
*/
export function setItem(key, value) {
if (!browser) return;
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Failed to set item ${key}:`, error);
}
}
/**
* Remove item from localStorage
* @param {string} key
*/
export function removeItem(key) {
if (!browser) return;
try {
localStorage.removeItem(key);
} catch (error) {
console.error(`Failed to remove item ${key}:`, error);
}
}
/**
* Clear all items
*/
export function clear() {
if (!browser) return;
try {
localStorage.clear();
} catch (error) {
console.error('Failed to clear storage:', error);
}
}
+56
View File
@@ -0,0 +1,56 @@
import { getItem, setItem } from './storage.js';
import { generateUUID } from './uuid.js';
/**
* @typedef {import('$lib/api/types.js').QueuedChange} QueuedChange
*/
const QUEUE_KEY = 'opal_sync_queue';
/**
* Add change to sync queue
* @param {Omit<QueuedChange, 'id'|'timestamp'>} change
*/
export function queueChange(change) {
const queue = getQueue();
queue.push({
id: generateUUID(),
timestamp: Math.floor(Date.now() / 1000),
...change
});
setItem(QUEUE_KEY, queue);
}
/**
* Get all queued changes
* @returns {QueuedChange[]}
*/
export function getQueue() {
return getItem(QUEUE_KEY) || [];
}
/**
* Clear sync queue
*/
export function clearQueue() {
setItem(QUEUE_KEY, []);
}
/**
* Get queue size
* @returns {number}
*/
export function getQueueSize() {
return getQueue().length;
}
/**
* Remove specific change from queue
* @param {string} id
*/
export function removeFromQueue(id) {
const queue = getQueue().filter((change) => change.id !== id);
setItem(QUEUE_KEY, queue);
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Generate UUID v4
* @returns {string}
*/
export function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}