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:
@@ -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 {};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user