diff --git a/apps/calc/apps/web-archived/Dockerfile b/apps/calc/apps/web-archived/Dockerfile deleted file mode 100644 index 2b5b06923..000000000 --- a/apps/calc/apps/web-archived/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM sveltekit-base:local AS builder - -ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001 -ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL - -COPY apps/calc/apps/web ./apps/calc/apps/web -COPY apps/calc/packages ./apps/calc/packages - -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ - pnpm install --no-frozen-lockfile --ignore-scripts - -WORKDIR /app/apps/calc/apps/web -RUN pnpm exec svelte-kit sync -RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build - -FROM node:20-alpine AS production -WORKDIR /app/apps/calc/apps/web -COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm -COPY --from=builder /app/apps/calc/apps/web/node_modules ./node_modules -COPY --from=builder /app/apps/calc/apps/web/build ./build -COPY --from=builder /app/apps/calc/apps/web/package.json ./ - -EXPOSE 5026 -ENV NODE_ENV=production PORT=5026 HOST=0.0.0.0 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:5026/health || exit 1 - -CMD ["node", "build"] diff --git a/apps/calc/apps/web-archived/package.json b/apps/calc/apps/web-archived/package.json deleted file mode 100644 index 5f6e6a84b..000000000 --- a/apps/calc/apps/web-archived/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "@calc/web", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "eslint .", - "format": "prettier --write .", - "type-check": "svelte-kit sync && svelte-check --threshold error" - }, - "devDependencies": { - "@manacore/shared-pwa": "workspace:*", - "@manacore/shared-vite-config": "workspace:*", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.47.1", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@tailwindcss/vite": "^4.1.7", - "@types/node": "^20.0.0", - "@vite-pwa/sveltekit": "^1.1.0", - "prettier": "^3.1.1", - "prettier-plugin-svelte": "^3.1.2", - "svelte": "^5.41.0", - "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.7", - "tslib": "^2.4.1", - "typescript": "^5.9.3", - "vite": "^6.0.0" - }, - "dependencies": { - "@calc/shared": "workspace:*", - "@manacore/local-store": "workspace:*", - "@manacore/shared-app-onboarding": "workspace:*", - "@manacore/shared-auth": "workspace:*", - "@manacore/shared-auth-stores": "workspace:*", - "@manacore/shared-auth-ui": "workspace:*", - "@manacore/shared-branding": "workspace:*", - "@manacore/shared-error-tracking": "workspace:*", - "@manacore/feedback": "workspace:*", - "@manacore/shared-i18n": "workspace:*", - "@manacore/help": "workspace:*", - "@manacore/shared-icons": "workspace:*", - "@manacore/shared-profile-ui": "workspace:*", - "@manacore/shared-stores": "workspace:*", - "@manacore/shared-tags": "workspace:*", - "@manacore/subscriptions": "workspace:*", - "@manacore/shared-tailwind": "workspace:*", - "@manacore/shared-theme": "workspace:*", - "@manacore/shared-theme-ui": "workspace:*", - "@manacore/shared-ui": "workspace:*", - "@manacore/shared-utils": "workspace:*", - "svelte-i18n": "^4.0.1" - }, - "type": "module" -} diff --git a/apps/calc/apps/web-archived/src/app.css b/apps/calc/apps/web-archived/src/app.css deleted file mode 100644 index c29749613..000000000 --- a/apps/calc/apps/web-archived/src/app.css +++ /dev/null @@ -1,10 +0,0 @@ -@import "tailwindcss"; -@import "@manacore/shared-tailwind/themes.css"; - -/* Scan shared packages for Tailwind classes */ -@source "../../../../packages/shared-ui/src"; -@source "../../../../packages/shared-auth-ui/src"; -@source "../../../../packages/shared-branding/src"; -@source "../../../../packages/shared-theme-ui/src"; -@source "../../../../packages/shared-theme-ui/src/components"; -@source "../../../../packages/shared-theme-ui/src/pages"; diff --git a/apps/calc/apps/web-archived/src/app.d.ts b/apps/calc/apps/web-archived/src/app.d.ts deleted file mode 100644 index c269fca6f..000000000 --- a/apps/calc/apps/web-archived/src/app.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const __BUILD_HASH__: string; -declare const __BUILD_TIME__: string; diff --git a/apps/calc/apps/web-archived/src/app.html b/apps/calc/apps/web-archived/src/app.html deleted file mode 100644 index e31c3501d..000000000 --- a/apps/calc/apps/web-archived/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/calc/apps/web-archived/src/hooks.client.ts b/apps/calc/apps/web-archived/src/hooks.client.ts deleted file mode 100644 index 7e39e1d79..000000000 --- a/apps/calc/apps/web-archived/src/hooks.client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { initErrorTracking, handleSvelteError } from '@manacore/shared-error-tracking/browser'; -import type { HandleClientError } from '@sveltejs/kit'; - -initErrorTracking({ - serviceName: 'calc-web', - dsn: (window as any).__PUBLIC_GLITCHTIP_DSN__, - environment: import.meta.env.MODE, -}); - -export const handleError: HandleClientError = ({ error }) => { - handleSvelteError(error); -}; diff --git a/apps/calc/apps/web-archived/src/hooks.server.ts b/apps/calc/apps/web-archived/src/hooks.server.ts deleted file mode 100644 index 461480e2b..000000000 --- a/apps/calc/apps/web-archived/src/hooks.server.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; -import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; - -const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = - process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; -const PUBLIC_BACKEND_URL_CLIENT = - process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; -const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; - -export const handle: Handle = async ({ event, resolve }) => { - const response = await resolve(event, { - transformPageChunk: ({ html }) => { - const envScript = ``; - return html.replace('', `${envScript}`); - }, - }); - - setSecurityHeaders(response, { - connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], - }); - - return response; -}; diff --git a/apps/calc/apps/web-archived/src/lib/components/skeletons/AppLoadingSkeleton.svelte b/apps/calc/apps/web-archived/src/lib/components/skeletons/AppLoadingSkeleton.svelte deleted file mode 100644 index daf44b28c..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skeletons/AppLoadingSkeleton.svelte +++ /dev/null @@ -1,84 +0,0 @@ - - -
-
- -
- - -
- -
- -
-
- -
- {#each Array(16) as _} - - {/each} -
-
-
-
- - diff --git a/apps/calc/apps/web-archived/src/lib/components/skeletons/index.ts b/apps/calc/apps/web-archived/src/lib/components/skeletons/index.ts deleted file mode 100644 index f09e744bb..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skeletons/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte'; diff --git a/apps/calc/apps/web-archived/src/lib/components/skins/CasioSkin.svelte b/apps/calc/apps/web-archived/src/lib/components/skins/CasioSkin.svelte deleted file mode 100644 index fd6209bcc..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skins/CasioSkin.svelte +++ /dev/null @@ -1,284 +0,0 @@ - - -
-
- -
- CASIO - fx-82 -
- - -
- {#each Array(8) as _} -
- {/each} -
- - -
-
{expression || ' '}
-
-
- {error || display} -
- {#if display !== '0' && !error} - - {/if} -
-
- - -
- {#each buttons as row} - {#each row as btn} - - {/each} - {/each} -
- - - - - -
-
- - diff --git a/apps/calc/apps/web-archived/src/lib/components/skins/HP35Skin.svelte b/apps/calc/apps/web-archived/src/lib/components/skins/HP35Skin.svelte deleted file mode 100644 index 7ab74a669..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skins/HP35Skin.svelte +++ /dev/null @@ -1,263 +0,0 @@ - - -
- -
- - - - -
-
{expression || ' '}
-
-
- {error || display} -
- {#if display !== '0' && !error} - - {/if} -
-
- - -
- {#each buttons as row} - {#each row as btn} - - {/each} - {/each} -
- - - - - - -
-
- - diff --git a/apps/calc/apps/web-archived/src/lib/components/skins/MinimalSkin.svelte b/apps/calc/apps/web-archived/src/lib/components/skins/MinimalSkin.svelte deleted file mode 100644 index 2a597fb6e..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skins/MinimalSkin.svelte +++ /dev/null @@ -1,178 +0,0 @@ - - -
- -
-
{expression || ' '}
-
-
- {error || display} -
- {#if display !== '0' && !error} - - {/if} -
-
- - -
- {#each buttons as row} - {#each row as btn} - - {/each} - {/each} -
- - -
- - diff --git a/apps/calc/apps/web-archived/src/lib/components/skins/ModernSkin.svelte b/apps/calc/apps/web-archived/src/lib/components/skins/ModernSkin.svelte deleted file mode 100644 index d1fb096d6..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skins/ModernSkin.svelte +++ /dev/null @@ -1,84 +0,0 @@ - - -
-
-
- {expression || ' '} -
-
-
- {error || display} -
- {#if display !== '0' && !error} - - {/if} -
-
- -
- {#each buttons as row} - {#each row as btn} - - {/each} - {/each} -
- - -
diff --git a/apps/calc/apps/web-archived/src/lib/components/skins/TI84Skin.svelte b/apps/calc/apps/web-archived/src/lib/components/skins/TI84Skin.svelte deleted file mode 100644 index 8ec007fce..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skins/TI84Skin.svelte +++ /dev/null @@ -1,281 +0,0 @@ - - -
-
- -
- TEXAS INSTRUMENTS - TI-84 Plus -
- - -
-
-
{expression || ' '}
-
-
- {error || display} -
- {#if display !== '0' && !error} - - {/if} -
-
-
- - -
- - -
- - -
- {#each buttons as row} - {#each row as btn} - - {/each} - {/each} -
- - -
-
- - diff --git a/apps/calc/apps/web-archived/src/lib/components/skins/index.ts b/apps/calc/apps/web-archived/src/lib/components/skins/index.ts deleted file mode 100644 index 35d7614d2..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skins/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { default as ModernSkin } from './ModernSkin.svelte'; -export { default as HP35Skin } from './HP35Skin.svelte'; -export { default as CasioSkin } from './CasioSkin.svelte'; -export { default as TI84Skin } from './TI84Skin.svelte'; -export { default as MinimalSkin } from './MinimalSkin.svelte'; -export type { CalcSkinProps } from './types'; diff --git a/apps/calc/apps/web-archived/src/lib/components/skins/types.ts b/apps/calc/apps/web-archived/src/lib/components/skins/types.ts deleted file mode 100644 index 770f0fa9f..000000000 --- a/apps/calc/apps/web-archived/src/lib/components/skins/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Shared interface for all calculator skin components. - */ -export interface CalcSkinProps { - expression: string; - display: string; - error: string; - copied: boolean; - onButton: (btn: string) => void; - onClear: () => void; - onBackspace: () => void; - onEquals: () => void; - onCopy: () => void; -} diff --git a/apps/calc/apps/web-archived/src/lib/data/guest-seed.ts b/apps/calc/apps/web-archived/src/lib/data/guest-seed.ts deleted file mode 100644 index 6478a275d..000000000 --- a/apps/calc/apps/web-archived/src/lib/data/guest-seed.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Guest seed data for the Calc app. - * - * Sample calculations loaded on first guest visit. - */ - -import type { LocalCalculation } from './local-store'; - -export const guestCalculations: LocalCalculation[] = [ - { - id: 'calc-demo-1', - mode: 'standard', - expression: '42 * 23', - result: '966', - }, - { - id: 'calc-demo-2', - mode: 'scientific', - expression: 'sin(π/4)', - result: '0.7071067812', - }, - { - id: 'calc-demo-3', - mode: 'standard', - expression: '1024 / 8', - result: '128', - }, -]; diff --git a/apps/calc/apps/web-archived/src/lib/data/local-store.ts b/apps/calc/apps/web-archived/src/lib/data/local-store.ts deleted file mode 100644 index 8830202ce..000000000 --- a/apps/calc/apps/web-archived/src/lib/data/local-store.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Calc App — Local-First Data Layer - * - * Defines the IndexedDB database, collections, and guest seed data. - * This is the single source of truth for all Calc data. - */ - -import { createLocalStore, type BaseRecord } from '@manacore/local-store'; -import { guestCalculations } from './guest-seed'; -import type { CalculatorMode, CalculatorSkin } from '@calc/shared'; - -// ─── Types ────────────────────────────────────────────────── - -export interface LocalCalculation extends BaseRecord { - mode: CalculatorMode; - expression: string; - result: string; - skin?: CalculatorSkin; -} - -export interface LocalSavedFormula extends BaseRecord { - name: string; - expression: string; - description: string | null; - mode: CalculatorMode; -} - -// ─── Store ────────────────────────────────────────────────── - -const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050'; - -export const calcStore = createLocalStore({ - appId: 'calc', - collections: [ - { - name: 'calculations', - indexes: ['mode'], - guestSeed: guestCalculations, - }, - { - name: 'savedFormulas', - indexes: ['mode', 'name'], - }, - ], - sync: { - serverUrl: SYNC_SERVER_URL, - }, -}); - -// Typed collection accessors -export const calculationCollection = calcStore.collection('calculations'); -export const savedFormulaCollection = calcStore.collection('savedFormulas'); diff --git a/apps/calc/apps/web-archived/src/lib/data/queries.ts b/apps/calc/apps/web-archived/src/lib/data/queries.ts deleted file mode 100644 index c22b9eef7..000000000 --- a/apps/calc/apps/web-archived/src/lib/data/queries.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Reactive Queries for Calc - * - * Uses Dexie liveQuery to automatically re-render when IndexedDB changes. - */ - -import { useLiveQueryWithDefault } from '@manacore/local-store/svelte'; -import { - calculationCollection, - savedFormulaCollection, - type LocalCalculation, - type LocalSavedFormula, -} from './local-store'; -import type { Calculation, SavedFormula } from '@calc/shared'; - -// ─── Type Converters ─────────────────────────────────────── - -export function toCalculation(local: LocalCalculation): Calculation { - return { - id: local.id, - userId: 'local', - mode: local.mode, - expression: local.expression, - result: local.result, - skin: local.skin, - createdAt: local.createdAt ?? new Date().toISOString(), - }; -} - -export function toSavedFormula(local: LocalSavedFormula): SavedFormula { - return { - id: local.id, - userId: 'local', - name: local.name, - expression: local.expression, - description: local.description ?? undefined, - mode: local.mode, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: local.updatedAt ?? new Date().toISOString(), - }; -} - -// ─── Live Query Hooks ────────────────────────────────────── - -/** All calculations (history), newest first. */ -export function useAllCalculations() { - return useLiveQueryWithDefault(async () => { - const locals = await calculationCollection.getAll(); - return locals.map(toCalculation).reverse(); - }, [] as Calculation[]); -} - -/** All saved formulas. */ -export function useAllSavedFormulas() { - return useLiveQueryWithDefault(async () => { - const locals = await savedFormulaCollection.getAll(); - return locals.map(toSavedFormula); - }, [] as SavedFormula[]); -} diff --git a/apps/calc/apps/web-archived/src/lib/engine/evaluate.ts b/apps/calc/apps/web-archived/src/lib/engine/evaluate.ts deleted file mode 100644 index 761e1b6e9..000000000 --- a/apps/calc/apps/web-archived/src/lib/engine/evaluate.ts +++ /dev/null @@ -1,261 +0,0 @@ -/** - * Safe math expression evaluator. - * - * Supports: +, -, *, /, %, ^, parentheses, and scientific functions. - * Does NOT use eval() — parses manually for safety. - */ - -const FUNCTIONS: Record number> = { - sin: Math.sin, - cos: Math.cos, - tan: Math.tan, - asin: Math.asin, - acos: Math.acos, - atan: Math.atan, - sinh: Math.sinh, - cosh: Math.cosh, - tanh: Math.tanh, - log: Math.log10, - ln: Math.log, - sqrt: Math.sqrt, - cbrt: Math.cbrt, - abs: Math.abs, - ceil: Math.ceil, - floor: Math.floor, - round: Math.round, - exp: Math.exp, -}; - -const CONSTANTS: Record = { - pi: Math.PI, - PI: Math.PI, - π: Math.PI, - e: Math.E, - E: Math.E, - φ: 1.6180339887, - phi: 1.6180339887, -}; - -type Token = - | { type: 'number'; value: number } - | { type: 'op'; value: string } - | { type: 'func'; value: string } - | { type: 'paren'; value: '(' | ')' }; - -function tokenize(expr: string): Token[] { - const tokens: Token[] = []; - let i = 0; - const s = expr.replace(/\s+/g, ''); - - while (i < s.length) { - // Numbers (including decimals) - if (/[0-9.]/.test(s[i])) { - let num = ''; - while (i < s.length && /[0-9.eE]/.test(s[i])) { - num += s[i++]; - // Handle scientific notation sign - if ((s[i] === '+' || s[i] === '-') && /[eE]/.test(s[i - 1])) { - num += s[i++]; - } - } - tokens.push({ type: 'number', value: parseFloat(num) }); - continue; - } - - // Parentheses - if (s[i] === '(' || s[i] === ')') { - tokens.push({ type: 'paren', value: s[i] as '(' | ')' }); - i++; - continue; - } - - // Operators - if ('+-*/%^'.includes(s[i])) { - // Handle unary minus - if ( - s[i] === '-' && - (tokens.length === 0 || - tokens[tokens.length - 1].type === 'op' || - (tokens[tokens.length - 1].type === 'paren' && tokens[tokens.length - 1].value === '(')) - ) { - let num = '-'; - i++; - while (i < s.length && /[0-9.eE]/.test(s[i])) { - num += s[i++]; - } - if (num.length > 1) { - tokens.push({ type: 'number', value: parseFloat(num) }); - continue; - } - // It's just a minus, push as operator - tokens.push({ type: 'op', value: '-' }); - continue; - } - tokens.push({ type: 'op', value: s[i] }); - i++; - continue; - } - - // Special characters (π, etc.) - if (s[i] === 'π' || s[i] === 'φ') { - tokens.push({ type: 'number', value: CONSTANTS[s[i]] }); - i++; - continue; - } - - // Functions and constants (letters) - if (/[a-zA-Z_]/.test(s[i])) { - let name = ''; - while (i < s.length && /[a-zA-Z_0-9]/.test(s[i])) { - name += s[i++]; - } - if (CONSTANTS[name] !== undefined) { - tokens.push({ type: 'number', value: CONSTANTS[name] }); - } else if (FUNCTIONS[name]) { - tokens.push({ type: 'func', value: name }); - } else { - throw new Error(`Unknown: ${name}`); - } - continue; - } - - // Factorial - if (s[i] === '!') { - tokens.push({ type: 'op', value: '!' }); - i++; - continue; - } - - throw new Error(`Unexpected character: ${s[i]}`); - } - - return tokens; -} - -function precedence(op: string): number { - if (op === '+' || op === '-') return 1; - if (op === '*' || op === '/' || op === '%') return 2; - if (op === '^') return 3; - return 0; -} - -function factorial(n: number): number { - if (n < 0 || !Number.isInteger(n)) throw new Error('Factorial of non-integer'); - if (n > 170) return Infinity; - let result = 1; - for (let i = 2; i <= n; i++) result *= i; - return result; -} - -function applyOp(op: string, a: number, b: number): number { - switch (op) { - case '+': - return a + b; - case '-': - return a - b; - case '*': - return a * b; - case '/': - if (b === 0) throw new Error('Division by zero'); - return a / b; - case '%': - return a % b; - case '^': - return Math.pow(a, b); - default: - throw new Error(`Unknown op: ${op}`); - } -} - -/** - * Evaluate a mathematical expression string. - * Returns the numeric result or throws on error. - */ -export function evaluate(expression: string): number { - const tokens = tokenize(expression); - const output: number[] = []; - const ops: Token[] = []; - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - - if (token.type === 'number') { - output.push(token.value); - } else if (token.type === 'func') { - ops.push(token); - } else if (token.type === 'op') { - if (token.value === '!') { - const val = output.pop(); - if (val === undefined) throw new Error('Missing operand'); - output.push(factorial(val)); - } else { - while ( - ops.length > 0 && - ops[ops.length - 1].type === 'op' && - precedence(ops[ops.length - 1].value as string) >= precedence(token.value) - ) { - const op = ops.pop()!; - const b = output.pop()!; - const a = output.pop()!; - output.push(applyOp(op.value as string, a, b)); - } - ops.push(token); - } - } else if (token.type === 'paren' && token.value === '(') { - ops.push(token); - } else if (token.type === 'paren' && token.value === ')') { - while ( - ops.length > 0 && - !(ops[ops.length - 1].type === 'paren' && ops[ops.length - 1].value === '(') - ) { - const op = ops.pop()!; - const b = output.pop()!; - const a = output.pop()!; - output.push(applyOp(op.value as string, a, b)); - } - ops.pop(); // remove '(' - // If there's a function on the stack, apply it - if (ops.length > 0 && ops[ops.length - 1].type === 'func') { - const func = ops.pop()!; - const val = output.pop()!; - output.push(FUNCTIONS[func.value as string](val)); - } - } - } - - while (ops.length > 0) { - const op = ops.pop()!; - const b = output.pop()!; - const a = output.pop()!; - output.push(applyOp(op.value as string, a, b)); - } - - if (output.length !== 1) throw new Error('Invalid expression'); - return output[0]; -} - -/** - * Format a number for display — removes trailing zeros, handles very large/small numbers. - */ -export function formatResult(value: number, precision: number = 10): string { - if (!isFinite(value)) return value > 0 ? '∞' : '-∞'; - if (isNaN(value)) return 'NaN'; - - // Use scientific notation for very large/small numbers - if (Math.abs(value) > 1e15 || (Math.abs(value) < 1e-10 && value !== 0)) { - return value.toExponential(precision - 1); - } - - // Round to precision and strip trailing zeros - const result = parseFloat(value.toPrecision(precision)); - return String(result); -} - -/** - * Convert between number bases. - */ -export function convertBase(value: string, fromBase: number, toBase: number): string { - const decimal = parseInt(value, fromBase); - if (isNaN(decimal)) throw new Error('Invalid number'); - return decimal.toString(toBase).toUpperCase(); -} diff --git a/apps/calc/apps/web-archived/src/lib/i18n/index.ts b/apps/calc/apps/web-archived/src/lib/i18n/index.ts deleted file mode 100644 index 0e2127891..000000000 --- a/apps/calc/apps/web-archived/src/lib/i18n/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { browser } from '$app/environment'; -import { init, register, locale, waitLocale } from 'svelte-i18n'; - -export const supportedLocales = ['de', 'en'] as const; -export type SupportedLocale = (typeof supportedLocales)[number]; - -const defaultLocale = 'de'; - -register('de', () => import('./locales/de.json')); -register('en', () => import('./locales/en.json')); - -function getInitialLocale(): SupportedLocale { - if (browser) { - const stored = localStorage.getItem('calc_locale'); - if (stored && supportedLocales.includes(stored as SupportedLocale)) { - return stored as SupportedLocale; - } - const browserLang = navigator.language.split('-')[0]; - if (supportedLocales.includes(browserLang as SupportedLocale)) { - return browserLang as SupportedLocale; - } - } - return defaultLocale; -} - -init({ - fallbackLocale: defaultLocale, - initialLocale: getInitialLocale(), -}); - -export function setLocale(newLocale: SupportedLocale) { - locale.set(newLocale); - if (browser) { - localStorage.setItem('calc_locale', newLocale); - } -} - -export { waitLocale }; diff --git a/apps/calc/apps/web-archived/src/lib/i18n/locales/de.json b/apps/calc/apps/web-archived/src/lib/i18n/locales/de.json deleted file mode 100644 index dadb38a50..000000000 --- a/apps/calc/apps/web-archived/src/lib/i18n/locales/de.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "app": { - "name": "Calc", - "tagline": "Dein Taschenrechner-Hub" - }, - "nav": { - "overview": "Übersicht", - "standard": "Standard", - "scientific": "Wissenschaftlich", - "programmer": "Programmierer", - "converter": "Einheiten", - "currency": "Währung", - "finance": "Finanzen", - "date": "Datum", - "percentage": "Prozent", - "settings": "Einstellungen" - }, - "calc": { - "result": "Ergebnis", - "history": "Verlauf", - "clearHistory": "Verlauf löschen", - "noHistory": "Noch keine Berechnungen", - "error": "Fehler", - "copied": "Kopiert!", - "clear": "Löschen", - "equals": "Gleich" - }, - "converter": { - "from": "Von", - "to": "Nach", - "swap": "Tauschen" - }, - "finance": { - "principal": "Anfangskapital", - "rate": "Zinssatz", - "years": "Laufzeit (Jahre)", - "result": "Ergebnis", - "monthlyPayment": "Monatliche Rate", - "totalInterest": "Gesamtzinsen", - "totalAmount": "Gesamtbetrag" - } -} diff --git a/apps/calc/apps/web-archived/src/lib/i18n/locales/en.json b/apps/calc/apps/web-archived/src/lib/i18n/locales/en.json deleted file mode 100644 index 58e9a7ce0..000000000 --- a/apps/calc/apps/web-archived/src/lib/i18n/locales/en.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "app": { - "name": "Calc", - "tagline": "Your Calculator Hub" - }, - "nav": { - "overview": "Overview", - "standard": "Standard", - "scientific": "Scientific", - "programmer": "Programmer", - "converter": "Units", - "currency": "Currency", - "finance": "Finance", - "date": "Date", - "percentage": "Percent", - "settings": "Settings" - }, - "calc": { - "result": "Result", - "history": "History", - "clearHistory": "Clear History", - "noHistory": "No calculations yet", - "error": "Error", - "copied": "Copied!", - "clear": "Clear", - "equals": "Equals" - }, - "converter": { - "from": "From", - "to": "To", - "swap": "Swap" - }, - "finance": { - "principal": "Principal", - "rate": "Interest Rate", - "years": "Term (Years)", - "result": "Result", - "monthlyPayment": "Monthly Payment", - "totalInterest": "Total Interest", - "totalAmount": "Total Amount" - } -} diff --git a/apps/calc/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts b/apps/calc/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts deleted file mode 100644 index 6baa589c8..000000000 --- a/apps/calc/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createAppOnboardingStore, type AppOnboardingStep } from '@manacore/shared-app-onboarding'; -import { userSettings } from './user-settings.svelte'; - -const calcOnboardingSteps: AppOnboardingStep[] = [ - { - id: 'features', - type: 'info', - question: 'Willkommen bei Calc!', - description: 'Das kann Calc:', - emoji: '🧮', - gradient: { from: 'pink-500', to: 'pink-700' }, - bullets: [ - 'Standard, Wissenschaftlich & Programmierer', - 'Einheiten- & Währungsrechner', - 'Finanzrechner (Zins, Kredit, Sparplan)', - 'Historische Taschenrechner-Skins', - ], - }, - { - id: 'defaultMode', - type: 'select', - question: 'Welchen Modus nutzt du am häufigsten?', - description: 'Du kannst jederzeit wechseln.', - emoji: '🔢', - gradient: { from: 'pink-500', to: 'pink-700' }, - options: [ - { id: 'standard', label: 'Standard', description: 'Grundrechenarten', emoji: '➕' }, - { - id: 'scientific', - label: 'Wissenschaftlich', - description: 'sin, cos, log & mehr', - emoji: '🔬', - }, - { id: 'programmer', label: 'Programmierer', description: 'HEX, BIN, OCT', emoji: '💻' }, - { id: 'converter', label: 'Einheiten', description: 'Umrechnen leicht gemacht', emoji: '📏' }, - ], - defaultValue: 'standard', - }, - { - id: 'welcome', - type: 'info', - question: 'Dein Rechner ist bereit!', - description: 'Tipps:', - emoji: '🎉', - gradient: { from: 'primary', to: 'primary/70' }, - bullets: [ - 'Tastatur-Eingabe funktioniert überall', - 'Verlauf speichert alle Berechnungen', - 'Wechsle Skins für verschiedene Looks', - 'Drücke Cmd/Ctrl+K für Schnellzugriff', - ], - }, -]; - -export const calcOnboarding = createAppOnboardingStore({ - appId: 'calc', - steps: calcOnboardingSteps, - userSettings, - onComplete: async () => {}, - onSkip: async () => {}, -}); diff --git a/apps/calc/apps/web-archived/src/lib/stores/auth.svelte.ts b/apps/calc/apps/web-archived/src/lib/stores/auth.svelte.ts deleted file mode 100644 index 5f0483975..000000000 --- a/apps/calc/apps/web-archived/src/lib/stores/auth.svelte.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Auth Store — uses centralized Mana auth factory. - */ - -import { createManaAuthStore } from '@manacore/shared-auth-stores'; - -export const authStore = createManaAuthStore({ - devBackendPort: 3017, -}); diff --git a/apps/calc/apps/web-archived/src/lib/stores/calc-settings.svelte.ts b/apps/calc/apps/web-archived/src/lib/stores/calc-settings.svelte.ts deleted file mode 100644 index 933031a74..000000000 --- a/apps/calc/apps/web-archived/src/lib/stores/calc-settings.svelte.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Calc-specific settings — persisted to localStorage. - */ - -import { browser } from '$app/environment'; -import type { CalculatorMode, CalculatorSkin } from '@calc/shared'; - -const STORAGE_KEY = 'calc-settings'; - -interface CalcSettings { - defaultMode: CalculatorMode; - defaultSkin: CalculatorSkin; - decimalPlaces: number; - thousandsSeparator: boolean; - angleMode: 'deg' | 'rad'; - historySize: number; - showKeyboardHints: boolean; -} - -const DEFAULTS: CalcSettings = { - defaultMode: 'standard', - defaultSkin: 'modern', - decimalPlaces: 10, - thousandsSeparator: false, - angleMode: 'rad', - historySize: 50, - showKeyboardHints: true, -}; - -function load(): CalcSettings { - if (!browser) return { ...DEFAULTS }; - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (raw) return { ...DEFAULTS, ...JSON.parse(raw) }; - } catch {} - return { ...DEFAULTS }; -} - -function save(settings: CalcSettings) { - if (!browser) return; - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); -} - -// Reactive settings store using Svelte 5 runes -let current = $state(load()); - -export const calcSettings = { - get value() { - return current; - }, - - update(partial: Partial) { - current = { ...current, ...partial }; - save(current); - }, - - reset() { - current = { ...DEFAULTS }; - save(current); - }, - - get defaults() { - return DEFAULTS; - }, -}; diff --git a/apps/calc/apps/web-archived/src/lib/stores/calculations.svelte.ts b/apps/calc/apps/web-archived/src/lib/stores/calculations.svelte.ts deleted file mode 100644 index 1bad46fc0..000000000 --- a/apps/calc/apps/web-archived/src/lib/stores/calculations.svelte.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Calculation mutation store — write operations only. - * Reads come from live query hooks in queries.ts. - */ - -import { calculationCollection, type LocalCalculation } from '$lib/data/local-store'; -import type { CreateCalculationInput } from '@calc/shared'; - -export const calculationsStore = { - async addCalculation(input: CreateCalculationInput) { - await calculationCollection.insert({ - mode: input.mode, - expression: input.expression, - result: input.result, - skin: input.skin, - } as Omit); - }, - - async deleteCalculation(id: string) { - await calculationCollection.delete(id); - }, - - async clearHistory() { - const all = await calculationCollection.getAll(); - for (const item of all) { - await calculationCollection.delete(item.id); - } - }, -}; diff --git a/apps/calc/apps/web-archived/src/lib/stores/navigation.ts b/apps/calc/apps/web-archived/src/lib/stores/navigation.ts deleted file mode 100644 index 34b7e2036..000000000 --- a/apps/calc/apps/web-archived/src/lib/stores/navigation.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createSimpleNavigationStores } from '@manacore/shared-stores'; - -export const { isNavCollapsed } = createSimpleNavigationStores({ - storageKey: 'calc', -}); diff --git a/apps/calc/apps/web-archived/src/lib/stores/saved-formulas.svelte.ts b/apps/calc/apps/web-archived/src/lib/stores/saved-formulas.svelte.ts deleted file mode 100644 index 77462d4d9..000000000 --- a/apps/calc/apps/web-archived/src/lib/stores/saved-formulas.svelte.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Saved formula mutation store — write operations only. - */ - -import { savedFormulaCollection, type LocalSavedFormula } from '$lib/data/local-store'; -import type { CreateFormulaInput, UpdateFormulaInput } from '@calc/shared'; - -export const savedFormulasStore = { - async saveFormula(input: CreateFormulaInput) { - await savedFormulaCollection.insert({ - name: input.name, - expression: input.expression, - description: input.description ?? null, - mode: input.mode, - } as Omit); - }, - - async updateFormula(id: string, input: UpdateFormulaInput) { - await savedFormulaCollection.update(id, input); - }, - - async deleteFormula(id: string) { - await savedFormulaCollection.delete(id); - }, -}; diff --git a/apps/calc/apps/web-archived/src/lib/stores/theme.svelte.ts b/apps/calc/apps/web-archived/src/lib/stores/theme.svelte.ts deleted file mode 100644 index defa5d01b..000000000 --- a/apps/calc/apps/web-archived/src/lib/stores/theme.svelte.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createThemeStore } from '@manacore/shared-theme'; - -export const theme = createThemeStore({ - appId: 'calc', - defaultVariant: 'lume', -}); diff --git a/apps/calc/apps/web-archived/src/lib/stores/user-settings.svelte.ts b/apps/calc/apps/web-archived/src/lib/stores/user-settings.svelte.ts deleted file mode 100644 index 1f3b69f1d..000000000 --- a/apps/calc/apps/web-archived/src/lib/stores/user-settings.svelte.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { browser } from '$app/environment'; -import { createUserSettingsStore } from '@manacore/shared-theme'; -import { authStore } from './auth.svelte'; - -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - if (injectedUrl) return injectedUrl; - } - return import.meta.env.DEV ? 'http://localhost:3001' : ''; -} - -export const userSettings = createUserSettingsStore({ - appId: 'calc', - authUrl: getAuthUrl, - getAccessToken: () => authStore.getAccessToken(), -}); diff --git a/apps/calc/apps/web-archived/src/routes/(app)/+layout.svelte b/apps/calc/apps/web-archived/src/routes/(app)/+layout.svelte deleted file mode 100644 index 3a82a5991..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/+layout.svelte +++ /dev/null @@ -1,428 +0,0 @@ - - - - - -
- - - {#if isTagStripVisible} - ({ - id: t.id, - name: t.name, - color: t.color || '#3b82f6', - }))} - selectedIds={[]} - onToggle={() => {}} - onClear={() => {}} - managementHref="/tags" - /> - {/if} - -
-
- {@render children()} -
-
- - (commandBarOpen = false)} - onSearch={handleCommandBarSearch} - onSelect={handleCommandBarSelect} - quickActions={commandBarQuickActions} - placeholder="Schnellzugriff..." - emptyText="Keine Ergebnisse" - searchingText="Suche..." - /> -
- - {#if calcOnboarding.shouldShow} - - {/if} - - (showGuestWelcome = false)} - onLogin={() => goto('/login')} - onRegister={() => goto('/register')} - locale={($locale || 'de') === 'de' ? 'de' : 'en'} - /> - - {#if authStore.isAuthenticated} - - {/if} - -
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/+page.svelte deleted file mode 100644 index 6b4663906..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/+page.svelte +++ /dev/null @@ -1,137 +0,0 @@ - - - - Calc - Dashboard - - -{#if isLoading} - -{:else} -
-
-

Calc

-

Dein Taschenrechner-Hub

-
- - -
-
-
- -
-
-
0
-
Wähle einen Rechner-Modus
-
-
-
- - -
- {#each quickLinks as link} - -
-
- -
-
-
{link.label}
-
{link.description}
-
-
-
- {/each} -
-
-{/if} - - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/converter/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/converter/+page.svelte deleted file mode 100644 index 17f94cc07..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/converter/+page.svelte +++ /dev/null @@ -1,131 +0,0 @@ - - - - Calc - Einheiten - - -
- -
- {#each UNIT_CATEGORIES.filter((c) => UNITS_BY_CATEGORY[c.id]) as cat} - - {/each} -
- - -
- -
- -
- - -
-
- - -
- -
- - -
- -
-
- {result()} -
- -
-
-
- - - {#if fromValue && result()} -
- {fromValue} - {units.find((u: UnitDefinition) => u.id === fromUnit)?.symbol} = {result()} - {units.find((u: UnitDefinition) => u.id === toUnit)?.symbol} -
- {/if} -
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/currency/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/currency/+page.svelte deleted file mode 100644 index fbf18b206..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/currency/+page.svelte +++ /dev/null @@ -1,184 +0,0 @@ - - - - Calc - Währung - - -
-
-

Währungsrechner

- -
- - -
- -
- - - -
- - {#if loading} -
Kurse laden...
- {:else if result() !== null} -
-
- {fmt(result()!)} - {toCurrency} -
-
- 1 {fromCurrency} = {(rates[toCurrency] || 0).toFixed(4)} - {toCurrency} -
-
- {/if} - - {#if lastUpdated} -
{lastUpdated}
- {/if} -
- - - {#if Object.keys(rates).length > 0} -
-

Schnellübersicht

-
- {#each currencies.filter((c) => c.code !== fromCurrency).slice(0, 8) as c} -
- {c.code} - {fmt(amount * (rates[c.code] || 0))} -
- {/each} -
-
- {/if} -
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/date/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/date/+page.svelte deleted file mode 100644 index f7c02b937..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/date/+page.svelte +++ /dev/null @@ -1,117 +0,0 @@ - - - - Calc - Datum - - -
- -
-

Tage zwischen Daten

-
- - -
- {#if daysBetween()} -
-
- {daysBetween()?.days} Tage -
-
- = {daysBetween()?.weeks} Wochen = ~{daysBetween()?.months} Monate -
-
- {/if} -
- - -
-

Tage addieren/subtrahieren

- - -
- {#each [7, 14, 30, 90, 365] as days} - - {/each} -
- {#if addedDate()} -
-
{formatDate(addedDate()!)}
-
- {addedDate()!.toISOString().split('T')[0]} -
-
- {/if} -
-
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/feedback/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/feedback/+page.svelte deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/calc/apps/web-archived/src/routes/(app)/finance/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/finance/+page.svelte deleted file mode 100644 index 6113d91f3..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/finance/+page.svelte +++ /dev/null @@ -1,312 +0,0 @@ - - - - Calc - Finanzen - - -
- -
- {#each modes as m} - - {/each} -
- -
- {#if mode === 'compound-interest'} -

Zinseszinsrechner

- - - - -
-
- Endkapital{fmt(compoundResult().total)} € -
-
- Zinsen{fmt(compoundResult().interest)} € -
-
- {:else if mode === 'loan'} -

Kreditrechner

- - - -
-
- Monatliche Rate{fmt(loanResult().monthly)} € -
-
- Gesamtkosten{fmt(loanResult().total)} € -
-
- Zinskosten{fmt(loanResult().interest)} € -
-
- {:else if mode === 'savings'} -

Sparplanrechner

- - - - -
-
- Endkapital{fmt(savingsResult().total)} € -
-
- Eingezahlt{fmt(savingsResult().deposited)} € -
-
- Zinsen{fmt(savingsResult().interest)} € -
-
- {:else if mode === 'tip'} -

Trinkgeld & Split

- - - -
-
- Trinkgeld{fmt(tipResult().tip)} € -
-
- Gesamt{fmt(tipResult().total)} € -
- {#if splitCount > 1} -
- Pro Person{fmt(tipResult().perPerson)} € -
- {/if} -
- {/if} -
-
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/help/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/help/+page.svelte deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/calc/apps/web-archived/src/routes/(app)/mana/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/mana/+page.svelte deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/calc/apps/web-archived/src/routes/(app)/percentage/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/percentage/+page.svelte deleted file mode 100644 index 66a040067..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/percentage/+page.svelte +++ /dev/null @@ -1,176 +0,0 @@ - - - - Calc - Prozent - - -
-
- {#each modes as m} - - {/each} -
- -
- {#if mode === 'of'} -

X% von Y

-
- - % von - -
-
-
{fmt(percentOfResult)}
-
- {percentValue}% von {baseValue} = {fmt(percentOfResult)} -
-
- {:else if mode === 'change'} -

Prozentuale Änderung

- - -
-
- {changeResult().percent >= 0 ? '+' : ''}{fmt(changeResult().percent)}% -
-
Differenz: {fmt(changeResult().diff)}
-
- {:else if mode === 'markup'} -

Preisaufschlag

- - -
-
{fmt(markupResult)} €
-
- {fmt(price)} + {percentChange}% = {fmt(markupResult)} € -
-
- {:else if mode === 'discount'} -

Rabatt

- - -
- {#each [10, 15, 20, 25, 50] as pct} - - {/each} -
-
-
{fmt(discountResult)} €
-
Ersparnis: {fmt(price - discountResult)} €
-
- {/if} -
-
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/profile/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/profile/+page.svelte deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/calc/apps/web-archived/src/routes/(app)/programmer/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/programmer/+page.svelte deleted file mode 100644 index 78571017e..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/programmer/+page.svelte +++ /dev/null @@ -1,171 +0,0 @@ - - - - Calc - Programmierer - - -
- -
- {#each bases as base} - - {/each} -
- - -
- {#each bases as base} -
- {base.label} - - {activeBase === base.id ? inputValue : getConverted(base.id)} - -
- {/each} - {#if error} -
{error}
- {/if} -
- - -
- {#each hexDigits as digit} - - {/each} -
- -
- - -
- - - {#if activeBase === 'dec' && inputValue !== '0'} - {@const num = parseInt(inputValue, 10)} - {#if !isNaN(num)} -
-
- {num} = 0x{num.toString(16).toUpperCase()} = 0b{num.toString(2)} -
-
- Bits: {num.toString(2).length} | Bytes: {Math.ceil(num.toString(2).length / 8)} -
-
- {/if} - {/if} -
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/scientific/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/scientific/+page.svelte deleted file mode 100644 index e854f38f5..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/scientific/+page.svelte +++ /dev/null @@ -1,274 +0,0 @@ - - - - Calc - Wissenschaftlich - - -
-
- -
- -
- - -
-
- - {#if showSkinPicker} -
-
- {#each CALCULATOR_SKINS as skin} - - {/each} -
-
- {/if} - - - {#if showExtraKeys} -
- -
- {#each SCIENTIFIC_CONSTANTS.slice(0, 6) as constant} - - {/each} -
- - -
- {#each sciExtraButtons as row} - {#each row as btn} - - {/each} - {/each} -
-
- {/if} - - - {#if activeSkin === 'modern'} - - {:else if activeSkin === 'hp35'} - - {:else if activeSkin === 'casio-fx'} - - {:else if activeSkin === 'ti84'} - - {:else if activeSkin === 'minimal'} - - {/if} -
- - -
-

Verlauf

- {#if recentHistory.length === 0} -

Noch keine Berechnungen

- {:else} -
- {#each recentHistory as calc} - - {/each} -
- {/if} -
-
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/settings/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/settings/+page.svelte deleted file mode 100644 index 338d3c670..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/settings/+page.svelte +++ /dev/null @@ -1,300 +0,0 @@ - - - - Calc - Einstellungen - - -
-
-

Einstellungen

-

Passe Calc an deine Bedürfnisse an

-
- - -
-

Allgemein

- - -
-
-
Standard-Modus
-
Wird beim Öffnen der App gezeigt
-
- -
- - -
-
-
Standard-Skin
-
Look des Rechners
-
- -
-
- - -
-

Berechnung

- - -
-
-
Dezimalstellen
-
Genauigkeit der Ergebnisse (1–15)
-
-
- - {settings.decimalPlaces} -
-
- - -
-
-
Tausendertrennzeichen
-
1.000.000 statt 1000000
-
- -
- - -
-
-
Winkel-Modus
-
Für sin, cos, tan im wissenschaftlichen Rechner
-
- -
-
- - -
-

Anzeige

- - -
-
-
Verlauf-Größe
-
Maximale Anzahl gespeicherter Berechnungen
-
-
- -
-
- - -
-
-
Tastatur-Hinweise
-
Zeige Keyboard-Shortcuts in der UI
-
- -
-
- - -
-

Tastaturkürzel

-
- {#each [['0–9, .', 'Ziffern eingeben'], ['+ - * /', 'Operatoren'], ['( )', 'Klammern'], ['Enter / =', 'Berechnen'], ['Backspace', 'Letzte Stelle löschen'], ['Esc / C', 'Alles löschen'], ['Cmd+K', 'Schnellzugriff'], ['Cmd+1–9', 'Navigation']] as [key, desc]} -
- {key} - {desc} -
- {/each} -
-
- - -
- -
-
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/skins/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/skins/+page.svelte deleted file mode 100644 index 999469e64..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/skins/+page.svelte +++ /dev/null @@ -1,153 +0,0 @@ - - - - Calc - Skins - - -
-
-

Rechner-Skins

-

Wähle das Aussehen deines Taschenrechners

-
- - -
- {#each CALCULATOR_SKINS as skin} -
selectSkin(skin.id)} - role="button" - tabindex="0" - > - -
-
- {#if skin.id === 'modern'} - - {:else if skin.id === 'hp35'} - - {:else if skin.id === 'casio-fx'} - - {:else if skin.id === 'ti84'} - - {:else if skin.id === 'minimal'} - - {/if} -
-
- - -
-
-
-

{skin.label}

- {#if skin.year} - {skin.year} - {/if} -
- {#if previewSkin === skin.id} - Aktiv - {/if} -
-

{skin.description.de}

-
-
- {/each} -
- - -
-

Geschichte des Taschenrechners

-
-
-
- 🏛️ -
-

HP-35 (1972)

-

- Der HP-35 war der weltweit erste wissenschaftliche Taschenrechner. Entwickelt von - Hewlett-Packard, machte er den Rechenschieber über Nacht obsolet. Sein Name kam daher, - dass er 35 Tasten hatte. Preis: $395 (heute ~$2.800). -

-
-
-
- -
-
- 🎒 -
-

Casio fx-82 (1985)

-

- Die Casio fx-Serie wurde zum Synonym für Schulrechner weltweit. Mit Solarzelle und dem - charakteristischen grün-grauen LCD-Display war er in fast jeder Schultasche zu finden. - Über 100 Millionen Stück verkauft. -

-
-
-
- -
-
- 📊 -
-

TI-84 Plus (2004)

-

- Der TI-84 Plus von Texas Instruments wurde zum Standard-Grafikrechner an - amerikanischen High Schools und Universitäten. Er konnte Funktionen plotten, Programme - ausführen und wurde trotz Smartphones nie abgelöst. -

-
-
-
-
-
-
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/standard/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/standard/+page.svelte deleted file mode 100644 index 2fb16792b..000000000 --- a/apps/calc/apps/web-archived/src/routes/(app)/standard/+page.svelte +++ /dev/null @@ -1,264 +0,0 @@ - - - - Calc - Standard - - - - -
-
- -
- -
- - - {#if showSkinPicker} -
-
- {#each CALCULATOR_SKINS as skin} - - {/each} -
-
- {/if} - - - {#if activeSkin === 'modern'} - - {:else if activeSkin === 'hp35'} - - {:else if activeSkin === 'casio-fx'} - - {:else if activeSkin === 'ti84'} - - {:else if activeSkin === 'minimal'} - - {/if} -
- - -
-

Verlauf

- {#if recentHistory.length === 0} -

Noch keine Berechnungen

- {:else} -
- {#each recentHistory as calc} - - {/each} -
- - {/if} -
-
- - diff --git a/apps/calc/apps/web-archived/src/routes/(app)/tags/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/tags/+page.svelte deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/calc/apps/web-archived/src/routes/(app)/themes/+page.svelte b/apps/calc/apps/web-archived/src/routes/(app)/themes/+page.svelte deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/calc/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte b/apps/calc/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte deleted file mode 100644 index 65410e2fa..000000000 --- a/apps/calc/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - Calc - forgot-password - - -
-

Auth: forgot-password

-
diff --git a/apps/calc/apps/web-archived/src/routes/(auth)/login/+page.svelte b/apps/calc/apps/web-archived/src/routes/(auth)/login/+page.svelte deleted file mode 100644 index df6938e26..000000000 --- a/apps/calc/apps/web-archived/src/routes/(auth)/login/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - Calc - login - - -
-

Auth: login

-
diff --git a/apps/calc/apps/web-archived/src/routes/(auth)/register/+page.svelte b/apps/calc/apps/web-archived/src/routes/(auth)/register/+page.svelte deleted file mode 100644 index 59c438198..000000000 --- a/apps/calc/apps/web-archived/src/routes/(auth)/register/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - Calc - register - - -
-

Auth: register

-
diff --git a/apps/calc/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte b/apps/calc/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte deleted file mode 100644 index 2d1342b1d..000000000 --- a/apps/calc/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - Calc - reset-password - - -
-

Auth: reset-password

-
diff --git a/apps/calc/apps/web-archived/src/routes/+layout.svelte b/apps/calc/apps/web-archived/src/routes/+layout.svelte deleted file mode 100644 index f599b48aa..000000000 --- a/apps/calc/apps/web-archived/src/routes/+layout.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - - - -{#if $isLocaleLoading || loading} - -{:else} -
- {@render children()} -
-{/if} diff --git a/apps/calc/apps/web-archived/src/routes/+layout.ts b/apps/calc/apps/web-archived/src/routes/+layout.ts deleted file mode 100644 index ad6cddb06..000000000 --- a/apps/calc/apps/web-archived/src/routes/+layout.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Disable SSR — all data is local-first (IndexedDB + mana-sync) -export const ssr = false; diff --git a/apps/calc/apps/web-archived/src/routes/health/+server.ts b/apps/calc/apps/web-archived/src/routes/health/+server.ts deleted file mode 100644 index ce73ee6a3..000000000 --- a/apps/calc/apps/web-archived/src/routes/health/+server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; - -export const GET: RequestHandler = async () => { - return json({ status: 'ok', app: 'calc-web' }); -}; diff --git a/apps/calc/apps/web-archived/src/routes/offline/+page.svelte b/apps/calc/apps/web-archived/src/routes/offline/+page.svelte deleted file mode 100644 index 3f73c2261..000000000 --- a/apps/calc/apps/web-archived/src/routes/offline/+page.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - Calc - Offline - - -
-
-
🧮
-

Offline

-

- Calc funktioniert auch offline. Deine Daten sind lokal gespeichert. -

-
-
diff --git a/apps/calc/apps/web-archived/svelte.config.js b/apps/calc/apps/web-archived/svelte.config.js deleted file mode 100644 index f290ef5a6..000000000 --- a/apps/calc/apps/web-archived/svelte.config.js +++ /dev/null @@ -1,21 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - - kit: { - adapter: adapter({ - out: 'build', - }), - prerender: { - handleHttpError: ({ path, message }) => { - if (path === '/favicon.png') return; - throw new Error(message); - }, - }, - }, -}; - -export default config; diff --git a/apps/calc/apps/web-archived/tsconfig.json b/apps/calc/apps/web-archived/tsconfig.json deleted file mode 100644 index a8f10c8e3..000000000 --- a/apps/calc/apps/web-archived/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } -} diff --git a/apps/calc/apps/web-archived/vite.config.ts b/apps/calc/apps/web-archived/vite.config.ts deleted file mode 100644 index 8058e2bf4..000000000 --- a/apps/calc/apps/web-archived/vite.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; -import { SvelteKitPWA } from '@vite-pwa/sveltekit'; -import { createPWAConfig } from '@manacore/shared-pwa'; -import { MANACORE_SHARED_PACKAGES, getBuildDefines } from '@manacore/shared-vite-config'; - -export default defineConfig({ - plugins: [ - sveltekit(), - SvelteKitPWA( - createPWAConfig({ - name: 'Calc - Taschenrechner', - shortName: 'Calc', - description: 'Taschenrechner, Einheiten & Finanzen', - themeColor: '#ec4899', - preset: 'minimal', - }) - ), - ], - server: { - port: 5198, - strictPort: true, - }, - ssr: { - noExternal: [...MANACORE_SHARED_PACKAGES], - }, - optimizeDeps: { - exclude: [...MANACORE_SHARED_PACKAGES], - }, - define: { - ...getBuildDefines(), - }, -}); diff --git a/apps/calendar/apps/web-archived/Dockerfile b/apps/calendar/apps/web-archived/Dockerfile deleted file mode 100644 index c85e2fa72..000000000 --- a/apps/calendar/apps/web-archived/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM sveltekit-base:local AS builder - -ARG PUBLIC_BACKEND_URL=http://calendar-server -ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001 -ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL -ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL - -COPY apps/calendar/packages/shared ./apps/calendar/packages/shared -COPY apps/calendar/apps/web ./apps/calendar/apps/web - -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ - pnpm install --no-frozen-lockfile --ignore-scripts - -WORKDIR /app/apps/calendar/apps/web -RUN pnpm exec svelte-kit sync -RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build - -FROM node:20-alpine AS production -WORKDIR /app/apps/calendar/apps/web -COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm -COPY --from=builder /app/apps/calendar/apps/web/node_modules ./node_modules -COPY --from=builder /app/apps/calendar/apps/web/build ./build -COPY --from=builder /app/apps/calendar/apps/web/package.json ./ - -EXPOSE 5012 -ENV NODE_ENV=production PORT=5012 HOST=0.0.0.0 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:5012/health || exit 1 - -CMD ["node", "build"] diff --git a/apps/calendar/apps/web-archived/e2e/auth.spec.ts b/apps/calendar/apps/web-archived/e2e/auth.spec.ts deleted file mode 100644 index 96b92c060..000000000 --- a/apps/calendar/apps/web-archived/e2e/auth.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { test, expect } from '@playwright/test'; - -// Auth tests run WITHOUT storageState (unauthenticated) - -// Helper: wait for the app to finish loading (skeleton disappears) -async function waitForAppReady(page: import('@playwright/test').Page) { - // The root layout shows AppLoadingSkeleton until auth initializes - // Wait for it to disappear and the actual page content to render - await page.waitForFunction( - () => { - // Check if loading skeleton is gone - const skeleton = document.querySelector('.app-loading-skeleton, [data-skeleton]'); - return !skeleton || skeleton.children.length === 0; - }, - { timeout: 30000 } - ); - // Give Svelte time to render - await page.waitForTimeout(500); -} - -test.describe('Authentication', () => { - test('login page renders with email and password fields', async ({ page }) => { - await page.goto('/login'); - await waitForAppReady(page); - - // LoginPage uses id="email" and id="password" (from shared-auth-ui) - const emailInput = page.locator('input[type="email"], input[name="email"], #email'); - const passwordInput = page.locator('input[type="password"], input[name="password"], #password'); - - await expect(emailInput.first()).toBeVisible({ timeout: 10000 }); - await expect(passwordInput.first()).toBeVisible({ timeout: 5000 }); - await expect(page.locator('button[type="submit"]')).toBeVisible(); - }); - - test('invalid credentials show error message', async ({ page }) => { - await page.goto('/login'); - await waitForAppReady(page); - - const emailInput = page.locator('input[type="email"], input[name="email"], #email').first(); - const passwordInput = page - .locator('input[type="password"], input[name="password"], #password') - .first(); - - await emailInput.fill('nonexistent@test.local'); - await passwordInput.fill('WrongPassword123!'); - await page.locator('button[type="submit"]').click(); - - // Error alert should appear - const errorAlert = page.locator('#form-error, [role="alert"]'); - await expect(errorAlert.first()).toBeVisible({ timeout: 10000 }); - }); - - test('successful login redirects to calendar', async ({ page }) => { - const email = process.env.E2E_TEST_EMAIL || 'e2e-calendar@test.local'; - const password = process.env.E2E_TEST_PASSWORD || 'TestPassword123'; - - // Listen for console errors and network failures - const errors: string[] = []; - page.on('console', (msg) => { - if (msg.type() === 'error') errors.push(msg.text()); - }); - page.on('requestfailed', (req) => { - errors.push(`Request failed: ${req.url()} - ${req.failure()?.errorText}`); - }); - - await page.goto('/login'); - await waitForAppReady(page); - - const emailInput = page.locator('input[type="email"], input[name="email"], #email').first(); - const passwordInput = page - .locator('input[type="password"], input[name="password"], #password') - .first(); - - await emailInput.fill(email); - await passwordInput.fill(password); - await page.locator('button[type="submit"]').click(); - - // Wait for either redirect or error - try { - await page.waitForURL('/', { timeout: 20000 }); - } catch { - // Log any errors for debugging - console.log('Login errors:', errors); - const authUrl = await page.evaluate( - () => (window as any).__PUBLIC_MANA_CORE_AUTH_URL__ || 'NOT SET' - ); - console.log('Auth URL on page:', authUrl); - throw new Error(`Login did not redirect. Auth URL: ${authUrl}. Errors: ${errors.join('; ')}`); - } - await expect(page.locator('main[aria-label="Kalender"]')).toBeVisible({ timeout: 10000 }); - }); - - test('unauthenticated access to / redirects to /login', async ({ page }) => { - await page.goto('/'); - - // The app layout's onMount redirects unauthenticated users to /login - await page.waitForURL(/\/login/, { timeout: 30000 }); - }); -}); diff --git a/apps/calendar/apps/web-archived/e2e/calendar-views.spec.ts b/apps/calendar/apps/web-archived/e2e/calendar-views.spec.ts deleted file mode 100644 index ef5213ffd..000000000 --- a/apps/calendar/apps/web-archived/e2e/calendar-views.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { test, expect, dismissOnboarding } from './fixtures/auth'; - -test.describe('Calendar Views', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await dismissOnboarding(page); - // Wait for calendar to be fully loaded - await expect(page.locator('main[aria-label="Kalender"]')).toBeVisible({ timeout: 10000 }); - }); - - test('week view loads as default with day columns', async ({ page }) => { - // ViewsBar "7" button should be active (week view) - const weekButton = page.locator('button[title="Wochenansicht"]'); - await expect(weekButton).toBeVisible(); - await expect(weekButton).toHaveClass(/active/); - - // Week view grid should show day columns with hour rows - const calendarContent = page.locator('.calendar-content'); - await expect(calendarContent).toBeVisible(); - }); - - test('switch to month view via header button', async ({ page }) => { - const monthButton = page.locator('button[title="Monatsansicht"]'); - await expect(monthButton).toBeVisible(); - await monthButton.click(); - - // Month button should now be active - await expect(monthButton).toHaveClass(/active/); - - // Week button should no longer be active - const weekButton = page.locator('button[title="Wochenansicht"]'); - await expect(weekButton).not.toHaveClass(/active/); - }); - - test('switch to agenda view', async ({ page }) => { - const agendaButton = page.locator('button[title="Agenda"]'); - await expect(agendaButton).toBeVisible(); - await agendaButton.click(); - - // Agenda button should now be active - await expect(agendaButton).toHaveClass(/active/); - }); - - test('navigate forward and backward with arrow keys', async ({ page }) => { - // Click on the day-header area (non-interactive) to ensure body focus - await page.locator('body').click({ position: { x: 10, y: 10 } }); - // Dismiss any overlay that might have opened - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - - // Get all day-header aria-labels to identify the current week - const dayHeaders = page.locator('.day-header[aria-label]'); - const initialLabel = await dayHeaders.first().getAttribute('aria-label'); - - // Navigate forward one week with ArrowRight - await page.keyboard.press('ArrowRight'); - await page.waitForTimeout(1000); - - const afterForwardLabel = await dayHeaders.first().getAttribute('aria-label'); - // The first day header should show a different date after navigating - expect(afterForwardLabel).not.toBe(initialLabel); - - // Navigate backward with ArrowLeft - await page.keyboard.press('ArrowLeft'); - await page.waitForTimeout(1000); - - const afterBackLabel = await dayHeaders.first().getAttribute('aria-label'); - // After going forward then back, we should be at the same date - expect(afterBackLabel).toBe(initialLabel); - }); - - test('today button returns to current date after navigation', async ({ page }) => { - // Click on the day-header area and dismiss any overlay - await page.locator('body').click({ position: { x: 10, y: 10 } }); - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - - // Get today's day header - const todayHeader = page.locator('.day-header.today'); - await expect(todayHeader).toBeVisible(); - - // Navigate away from today - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); - await page.waitForTimeout(1000); - - // Today should no longer be visible (navigated 2 weeks ahead) - await expect(todayHeader).not.toBeVisible(); - - // Click the "Heute" (Today) button - find by its title attribute - const todayButton = page.locator( - '.today-button, button[title*="heute" i], button[title*="today" i]' - ); - await expect(todayButton.first()).toBeVisible({ timeout: 5000 }); - await todayButton.first().click(); - await page.waitForTimeout(1000); - - // Today header should be visible again - await expect(todayHeader).toBeVisible(); - }); -}); diff --git a/apps/calendar/apps/web-archived/e2e/calendars.spec.ts b/apps/calendar/apps/web-archived/e2e/calendars.spec.ts deleted file mode 100644 index fe1644121..000000000 --- a/apps/calendar/apps/web-archived/e2e/calendars.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { test, expect } from './fixtures/auth'; - -const BACKEND_URL = process.env.PUBLIC_BACKEND_URL || 'http://localhost:3014'; - -test.describe('Calendar Management', () => { - test.beforeAll(async () => { - // Skip all calendar management tests if the backend is not running - try { - const res = await fetch(`${BACKEND_URL}/api/v1/health`, { - signal: AbortSignal.timeout(3000), - }); - if (!res.ok) test.skip(true, 'Calendar backend is not running'); - } catch { - test.skip(true, 'Calendar backend is not reachable'); - } - }); - - test('default calendar exists on first load', async ({ page }) => { - await page.goto('/settings'); - await expect(page.getByRole('heading', { name: 'Einstellungen', exact: true })).toBeVisible(); - - // The calendar list should have at least one calendar with "Standard" badge - const defaultBadge = page.locator('.badge-primary', { hasText: 'Standard' }); - await expect(defaultBadge).toBeVisible({ timeout: 10000 }); - }); - - test('create new calendar with name and color', async ({ page }) => { - const calendarName = `E2E Calendar ${Date.now()}`; - - await page.goto('/settings'); - await expect(page.getByRole('heading', { name: 'Einstellungen', exact: true })).toBeVisible(); - - // Click "Neuer Kalender" button - const newCalButton = page.getByRole('button', { name: /neuer kalender/i }); - await expect(newCalButton).toBeVisible({ timeout: 10000 }); - await newCalButton.click(); - - // Fill in the calendar name - const nameInput = page.locator('.new-calendar-form input[type="text"]'); - await expect(nameInput).toBeVisible(); - await nameInput.fill(calendarName); - - // Submit the form - const createButton = page.getByRole('button', { name: /erstellen/i }); - await createButton.click(); - - // Verify the new calendar appears in the list - const calendarCard = page.locator('.calendar-card', { hasText: calendarName }); - await expect(calendarCard).toBeVisible({ timeout: 5000 }); - - // Cleanup: delete the calendar - page.on('dialog', (dialog) => dialog.accept()); - const deleteButton = calendarCard.getByRole('button', { name: /löschen/i }); - await deleteButton.click(); - await expect(calendarCard).not.toBeVisible({ timeout: 5000 }); - }); - - test('toggle calendar visibility in sidebar', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('main[aria-label="Kalender"]')).toBeVisible({ timeout: 10000 }); - - const calendarSelector = page.locator('.pill-calendar-selector, .calendar-selector'); - - if (await calendarSelector.isVisible({ timeout: 3000 }).catch(() => false)) { - const toggles = calendarSelector.locator('button, input[type="checkbox"]'); - const count = await toggles.count(); - - if (count > 0) { - await toggles.first().click(); - await page.waitForTimeout(500); - await toggles.first().click(); - } - } - }); - - test('delete non-default calendar from settings', async ({ page }) => { - const calendarName = `E2E Delete Test ${Date.now()}`; - - await page.goto('/settings'); - await expect(page.getByRole('heading', { name: 'Einstellungen', exact: true })).toBeVisible(); - - // Create a calendar first - await page.getByRole('button', { name: /neuer kalender/i }).click(); - await page.locator('.new-calendar-form input[type="text"]').fill(calendarName); - await page.getByRole('button', { name: /erstellen/i }).click(); - - const calendarCard = page.locator('.calendar-card', { hasText: calendarName }); - await expect(calendarCard).toBeVisible({ timeout: 5000 }); - - page.on('dialog', (dialog) => dialog.accept()); - - const deleteButton = calendarCard.getByRole('button', { name: /löschen/i }); - await expect(deleteButton).toBeVisible(); - await deleteButton.click(); - - await expect(calendarCard).not.toBeVisible({ timeout: 5000 }); - }); -}); diff --git a/apps/calendar/apps/web-archived/e2e/error-page.spec.ts b/apps/calendar/apps/web-archived/e2e/error-page.spec.ts deleted file mode 100644 index 8840f3b2d..000000000 --- a/apps/calendar/apps/web-archived/e2e/error-page.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Error Page', () => { - test('visiting a nonexistent route shows error page with status code', async ({ page }) => { - const response = await page.goto('/nonexistent-route-that-does-not-exist'); - - // SvelteKit should return a 404 status - expect(response?.status()).toBe(404); - - // The error page should display the status code - const statusHeading = page.locator('h1'); - await expect(statusHeading).toBeVisible({ timeout: 10000 }); - await expect(statusHeading).toContainText('404'); - - // Should show a "back to home" link - const backLink = page.locator('a[href="/"]'); - await expect(backLink).toBeVisible(); - }); -}); diff --git a/apps/calendar/apps/web-archived/e2e/events.spec.ts b/apps/calendar/apps/web-archived/e2e/events.spec.ts deleted file mode 100644 index 649b7e799..000000000 --- a/apps/calendar/apps/web-archived/e2e/events.spec.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { test, expect } from './fixtures/auth'; - -const BACKEND_URL = process.env.PUBLIC_BACKEND_URL || 'http://localhost:3014'; - -test.describe('Event CRUD', () => { - test.beforeAll(async () => { - // Skip all event tests if the backend is not running - try { - const res = await fetch(`${BACKEND_URL}/api/v1/health`, { - signal: AbortSignal.timeout(3000), - }); - if (!res.ok) test.skip(true, 'Calendar backend is not running'); - } catch { - test.skip(true, 'Calendar backend is not reachable'); - } - }); - - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await expect(page.locator('main[aria-label="Kalender"]')).toBeVisible({ timeout: 10000 }); - }); - - test('create event via quick overlay, see it in view, then delete it', async ({ page }) => { - const uniqueTitle = `E2E Test Event ${Date.now()}`; - - // Click on a time slot in the week view to trigger quick create - const weekGrid = page.locator('.week-grid, .carousel-page.current .week-grid'); - if (await weekGrid.first().isVisible()) { - const box = await weekGrid.first().boundingBox(); - if (box) { - await weekGrid.first().click({ - position: { x: box.width * 0.5, y: box.height * 0.3 }, - }); - } - } - - // Wait for the quick event overlay to appear - const overlay = page.locator('.quick-event-overlay'); - await expect(overlay).toBeVisible({ timeout: 5000 }); - - // Type the event title (the title input is auto-focused) - await page.keyboard.type(uniqueTitle); - - // Click "Speichern" (Save) - await overlay.getByRole('button', { name: /speichern/i }).click(); - await expect(overlay).not.toBeVisible({ timeout: 5000 }); - - // Verify the event appears in the calendar view - const eventCard = page.locator('.event-card, .event-block').filter({ hasText: uniqueTitle }); - await expect(eventCard).toBeVisible({ timeout: 5000 }); - - // Click the event to open it - await eventCard.click(); - - // The quick event overlay should open with event details - const editOverlay = page.locator('.quick-event-overlay'); - await expect(editOverlay).toBeVisible({ timeout: 5000 }); - - // Delete the event - const deleteButton = editOverlay.getByRole('button', { name: /löschen/i }); - if (await deleteButton.isVisible()) { - await deleteButton.click(); - - const confirmButton = page.getByRole('button', { name: /löschen|ja|bestätigen/i }); - if (await confirmButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await confirmButton.click(); - } - - await expect(eventCard).not.toBeVisible({ timeout: 5000 }); - } - }); - - test('edit event title and verify update', async ({ page }) => { - const originalTitle = `E2E Edit Test ${Date.now()}`; - const updatedTitle = `${originalTitle} Updated`; - - // Create an event first via the grid - const weekGrid = page.locator('.week-grid, .carousel-page.current .week-grid'); - if (await weekGrid.first().isVisible()) { - const box = await weekGrid.first().boundingBox(); - if (box) { - await weekGrid.first().click({ - position: { x: box.width * 0.5, y: box.height * 0.4 }, - }); - } - } - - const overlay = page.locator('.quick-event-overlay'); - await expect(overlay).toBeVisible({ timeout: 5000 }); - await page.keyboard.type(originalTitle); - await overlay.getByRole('button', { name: /speichern/i }).click(); - await expect(overlay).not.toBeVisible({ timeout: 5000 }); - - // Find and click the created event - const eventCard = page.locator('.event-card, .event-block').filter({ hasText: originalTitle }); - await expect(eventCard).toBeVisible({ timeout: 5000 }); - await eventCard.click(); - - // Edit the title - const editOverlay = page.locator('.quick-event-overlay'); - await expect(editOverlay).toBeVisible({ timeout: 5000 }); - - const titleInput = editOverlay.locator('input[type="text"]').first(); - await expect(titleInput).toHaveValue(originalTitle); - - await titleInput.clear(); - await titleInput.fill(updatedTitle); - - await editOverlay.getByRole('button', { name: /speichern/i }).click(); - await expect(editOverlay).not.toBeVisible({ timeout: 5000 }); - - // Verify updated title is visible - const updatedCard = page.locator('.event-card, .event-block').filter({ hasText: updatedTitle }); - await expect(updatedCard).toBeVisible({ timeout: 5000 }); - - // Cleanup: delete the event - await updatedCard.click(); - const cleanupOverlay = page.locator('.quick-event-overlay'); - await expect(cleanupOverlay).toBeVisible({ timeout: 5000 }); - const deleteBtn = cleanupOverlay.getByRole('button', { name: /löschen/i }); - if (await deleteBtn.isVisible()) { - await deleteBtn.click(); - const confirmBtn = page.getByRole('button', { name: /löschen|ja|bestätigen/i }); - if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) { - await confirmBtn.click(); - } - } - }); - - test('click event to open detail overlay', async ({ page }) => { - const title = `E2E Detail Test ${Date.now()}`; - - // Create an event - const weekGrid = page.locator('.week-grid, .carousel-page.current .week-grid'); - if (await weekGrid.first().isVisible()) { - const box = await weekGrid.first().boundingBox(); - if (box) { - await weekGrid.first().click({ - position: { x: box.width * 0.5, y: box.height * 0.5 }, - }); - } - } - - const overlay = page.locator('.quick-event-overlay'); - await expect(overlay).toBeVisible({ timeout: 5000 }); - await page.keyboard.type(title); - await overlay.getByRole('button', { name: /speichern/i }).click(); - await expect(overlay).not.toBeVisible({ timeout: 5000 }); - - // Click the event to see details - const eventCard = page.locator('.event-card, .event-block').filter({ hasText: title }); - await expect(eventCard).toBeVisible({ timeout: 5000 }); - await eventCard.click(); - - const detailOverlay = page.locator('.quick-event-overlay'); - await expect(detailOverlay).toBeVisible({ timeout: 5000 }); - const titleInput = detailOverlay.locator('input[type="text"]').first(); - await expect(titleInput).toHaveValue(title); - - // Close and cleanup - await detailOverlay.getByRole('button', { name: /abbrechen/i }).click(); - await expect(detailOverlay).not.toBeVisible({ timeout: 5000 }); - - await eventCard.click(); - const cleanupOverlay = page.locator('.quick-event-overlay'); - const deleteBtn = cleanupOverlay.getByRole('button', { name: /löschen/i }); - if (await deleteBtn.isVisible()) { - await deleteBtn.click(); - const confirmBtn = page.getByRole('button', { name: /löschen|ja|bestätigen/i }); - if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) { - await confirmBtn.click(); - } - } - }); -}); diff --git a/apps/calendar/apps/web-archived/e2e/fixtures/auth.ts b/apps/calendar/apps/web-archived/e2e/fixtures/auth.ts deleted file mode 100644 index d625b3596..000000000 --- a/apps/calendar/apps/web-archived/e2e/fixtures/auth.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { test as base, expect, type Page, type BrowserContext } from '@playwright/test'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const TEST_EMAIL = process.env.E2E_TEST_EMAIL || 'e2e-calendar@test.local'; -const TEST_PASSWORD = process.env.E2E_TEST_PASSWORD || 'TestPassword123'; -const AUTH_URL = process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const STORAGE_STATE_PATH = path.join(__dirname, '..', '.auth-state.json'); - -/** - * Ensures a test user exists via the auth API. - */ -async function ensureTestUser(): Promise { - try { - const res = await fetch(`${AUTH_URL}/api/v1/auth/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: TEST_EMAIL, password: TEST_PASSWORD, name: 'E2E Test User' }), - }); - if (!res.ok && res.status !== 409 && res.status !== 422) { - const body = await res.text(); - console.warn(`Register returned ${res.status}: ${body}`); - } - } catch { - // User may already exist - } - - try { - await fetch(`${AUTH_URL}/api/v1/auth/verify-email-dev`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: TEST_EMAIL }), - }); - } catch { - // Verification endpoint may not exist - } -} - -async function waitForAppReady(page: Page): Promise { - await page.waitForFunction( - () => document.querySelector('main, form, input[type="email"], #email') !== null, - { timeout: 30000 } - ); -} - -/** - * Dismiss the onboarding modal by clicking "Überspringen". - * Waits briefly for it to appear, then dismisses it. - */ -async function dismissOnboarding(page: Page): Promise { - try { - const skipButton = page.getByText('Überspringen', { exact: true }); - await skipButton.waitFor({ state: 'visible', timeout: 3000 }); - await skipButton.click(); - // Wait for modal to close - await page.locator('.fixed.inset-0.z-50').waitFor({ state: 'hidden', timeout: 5000 }); - } catch { - // No onboarding modal — that's fine - } -} - -function hasValidStorageState(): boolean { - try { - const stat = fs.statSync(STORAGE_STATE_PATH); - const ageMs = Date.now() - stat.mtimeMs; - if (ageMs > 60 * 60 * 1000) return false; - const content = JSON.parse(fs.readFileSync(STORAGE_STATE_PATH, 'utf-8')); - return content.origins?.length > 0; - } catch { - return false; - } -} - -async function loginViaUI(page: Page): Promise { - await page.goto('/login'); - await waitForAppReady(page); - - const emailInput = page.locator('input[type="email"], input[name="email"], #email').first(); - const passwordInput = page - .locator('input[type="password"], input[name="password"], #password') - .first(); - - await emailInput.fill(TEST_EMAIL); - await passwordInput.fill(TEST_PASSWORD); - await page.locator('button[type="submit"]').click(); - - await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 30000 }); - await expect(page.locator('main').first()).toBeVisible({ timeout: 15000 }); - - // Dismiss onboarding wizard if it appears - await dismissOnboarding(page); -} - -/** - * Extended test fixture that provides an authenticated page. - */ -export const test = base.extend({ - workerStorageState: [ - async ({ browser }, use) => { - if (hasValidStorageState()) { - await use(STORAGE_STATE_PATH); - return; - } - - await ensureTestUser(); - - const context = await browser.newContext(); - const page = await context.newPage(); - await loginViaUI(page); - - await context.storageState({ path: STORAGE_STATE_PATH }); - await page.close(); - await context.close(); - - await use(STORAGE_STATE_PATH); - }, - { scope: 'worker' }, - ], - - context: async ({ browser, workerStorageState }, use) => { - const context = await browser.newContext({ storageState: workerStorageState }); - await use(context); - await context.close(); - }, - - page: async ({ context }, use) => { - const page = await context.newPage(); - await use(page); - }, -}); - -export { expect, dismissOnboarding }; diff --git a/apps/calendar/apps/web-archived/e2e/settings.spec.ts b/apps/calendar/apps/web-archived/e2e/settings.spec.ts deleted file mode 100644 index e0bb5d83c..000000000 --- a/apps/calendar/apps/web-archived/e2e/settings.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { test, expect, dismissOnboarding } from './fixtures/auth'; - -test.describe('Settings', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/settings'); - await dismissOnboarding(page); - await expect(page.getByRole('heading', { name: 'Einstellungen', exact: true })).toBeVisible({ - timeout: 10000, - }); - }); - - test('settings page renders all sections', async ({ page }) => { - // Check that the main setting sections are visible (use headings to avoid ambiguity) - await expect(page.getByText('Meine Kalender', { exact: true })).toBeVisible(); - await expect(page.getByText('Kalender-Ansicht', { exact: true })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Termine' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Konto' })).toBeVisible(); - }); - - test('change time format between 24h and 12h', async ({ page }) => { - // Find the time format buttons - const button24h = page.getByRole('button', { name: '24h (14:00)' }); - const button12h = page.getByRole('button', { name: '12h (2:00 PM)' }); - - await expect(button24h).toBeVisible(); - await expect(button12h).toBeVisible(); - - // Switch to 12h - await button12h.click(); - await expect(button12h).toHaveClass(/active/); - await expect(button24h).not.toHaveClass(/active/); - - // Switch back to 24h - await button24h.click(); - await expect(button24h).toHaveClass(/active/); - await expect(button12h).not.toHaveClass(/active/); - }); - - test('toggle show week numbers', async ({ page }) => { - // Find the "Wochennummern anzeigen" checkbox - const weekNumbersLabel = page.getByText('Wochennummern anzeigen'); - await expect(weekNumbersLabel).toBeVisible(); - - // The checkbox is inside a label with this text - const checkbox = page - .locator('label') - .filter({ hasText: 'Wochennummern anzeigen' }) - .locator('input[type="checkbox"]'); - const wasChecked = await checkbox.isChecked(); - - // Toggle it - await checkbox.click(); - await expect(checkbox).toBeChecked({ checked: !wasChecked }); - - // Toggle it back - await checkbox.click(); - await expect(checkbox).toBeChecked({ checked: wasChecked }); - }); - - test('toggle show only weekdays', async ({ page }) => { - const checkbox = page - .locator('label') - .filter({ hasText: 'Nur Werktage anzeigen' }) - .locator('input[type="checkbox"]'); - await expect(checkbox).toBeVisible(); - - const wasChecked = await checkbox.isChecked(); - await checkbox.click(); - await expect(checkbox).toBeChecked({ checked: !wasChecked }); - - // Restore original state - await checkbox.click(); - await expect(checkbox).toBeChecked({ checked: wasChecked }); - }); - - test('settings persist after page reload', async ({ page }) => { - // Switch to 12h format - const button12h = page.getByRole('button', { name: '12h (2:00 PM)' }); - await button12h.click(); - await expect(button12h).toHaveClass(/active/); - - // Reload - await page.reload(); - await expect(page.getByRole('heading', { name: 'Einstellungen', exact: true })).toBeVisible({ - timeout: 10000, - }); - - // Verify 12h is still active - const button12hAfterReload = page.getByRole('button', { name: '12h (2:00 PM)' }); - await expect(button12hAfterReload).toHaveClass(/active/); - - // Restore to 24h - const button24h = page.getByRole('button', { name: '24h (14:00)' }); - await button24h.click(); - await expect(button24h).toHaveClass(/active/); - }); - - test('user email is displayed in account section', async ({ page }) => { - const testEmail = process.env.E2E_TEST_EMAIL || 'e2e-calendar@test.local'; - - // The account section shows the user's email - const emailDisplay = page.locator('.setting-value'); - await expect(emailDisplay.first()).toContainText(testEmail); - }); -}); diff --git a/apps/calendar/apps/web-archived/e2e/week-view-interactions.spec.ts b/apps/calendar/apps/web-archived/e2e/week-view-interactions.spec.ts deleted file mode 100644 index ae111294d..000000000 --- a/apps/calendar/apps/web-archived/e2e/week-view-interactions.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { test, expect, dismissOnboarding } from './fixtures/auth'; - -const BACKEND_URL = process.env.PUBLIC_BACKEND_URL || 'http://localhost:3014'; - -test.describe('WeekView Interactions', () => { - test.beforeAll(async () => { - try { - const res = await fetch(`${BACKEND_URL}/api/v1/health`, { - signal: AbortSignal.timeout(3000), - }); - if (!res.ok) test.skip(true, 'Calendar backend is not running'); - } catch { - test.skip(true, 'Calendar backend is not reachable'); - } - }); - - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await dismissOnboarding(page); - await expect(page.locator('main[aria-label="Kalender"]')).toBeVisible({ timeout: 10000 }); - }); - - test('drag-to-create: clicking on empty time slot opens quick create overlay', async ({ - page, - }) => { - // Find a day column in the week view - const dayColumn = page.locator('.day-column').first(); - await expect(dayColumn).toBeVisible(); - - const box = await dayColumn.boundingBox(); - if (!box) return; - - // Click in the middle of the day column (should open quick create) - await dayColumn.click({ position: { x: box.width / 2, y: box.height * 0.4 } }); - - // Quick event overlay should appear - const overlay = page.locator('.quick-event-overlay'); - await expect(overlay).toBeVisible({ timeout: 5000 }); - - // Close it - await page.keyboard.press('Escape'); - }); - - test('drag-to-create: drag creates event with correct time range', async ({ page }) => { - const dayColumn = page.locator('.day-column').first(); - await expect(dayColumn).toBeVisible(); - - const box = await dayColumn.boundingBox(); - if (!box) return; - - // Drag from ~10am to ~12pm area - const startY = box.y + box.height * 0.35; - const endY = box.y + box.height * 0.5; - const centerX = box.x + box.width / 2; - - await page.mouse.move(centerX, startY); - await page.mouse.down(); - await page.mouse.move(centerX, endY, { steps: 5 }); - await page.mouse.up(); - - // Quick event overlay should appear - const overlay = page.locator('.quick-event-overlay'); - await expect(overlay).toBeVisible({ timeout: 5000 }); - - // Type a title and save - const uniqueTitle = `Drag Create ${Date.now()}`; - await page.keyboard.type(uniqueTitle); - await overlay.getByRole('button', { name: /speichern/i }).click(); - await expect(overlay).not.toBeVisible({ timeout: 5000 }); - - // Event should appear in the grid - const eventCard = page.locator('.event-card').filter({ hasText: uniqueTitle }); - await expect(eventCard).toBeVisible({ timeout: 5000 }); - - // Cleanup: delete the event - await eventCard.click(); - const editOverlay = page.locator('.quick-event-overlay'); - await expect(editOverlay).toBeVisible({ timeout: 5000 }); - const deleteBtn = editOverlay.getByRole('button', { name: /löschen/i }); - if (await deleteBtn.isVisible()) { - await deleteBtn.click(); - const confirmBtn = page.getByRole('button', { name: /löschen|ja|bestätigen/i }); - if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) { - await confirmBtn.click(); - } - } - }); - - test('escape cancels drag-to-create', async ({ page }) => { - const dayColumn = page.locator('.day-column').first(); - await expect(dayColumn).toBeVisible(); - - const box = await dayColumn.boundingBox(); - if (!box) return; - - const startY = box.y + box.height * 0.3; - const centerX = box.x + box.width / 2; - - // Start dragging - await page.mouse.move(centerX, startY); - await page.mouse.down(); - await page.mouse.move(centerX, startY + 50, { steps: 3 }); - - // Press escape to cancel - await page.keyboard.press('Escape'); - await page.mouse.up(); - - // No overlay should appear - const overlay = page.locator('.quick-event-overlay'); - await expect(overlay).not.toBeVisible({ timeout: 1000 }); - }); - - test('event card shows in correct position within time grid', async ({ page }) => { - const uniqueTitle = `Position Test ${Date.now()}`; - - // Create an event by clicking on the grid - const dayColumn = page.locator('.day-column').first(); - await expect(dayColumn).toBeVisible(); - const box = await dayColumn.boundingBox(); - if (!box) return; - - await dayColumn.click({ position: { x: box.width / 2, y: box.height * 0.5 } }); - - const overlay = page.locator('.quick-event-overlay'); - await expect(overlay).toBeVisible({ timeout: 5000 }); - await page.keyboard.type(uniqueTitle); - await overlay.getByRole('button', { name: /speichern/i }).click(); - await expect(overlay).not.toBeVisible({ timeout: 5000 }); - - // Verify the event card exists and has a top style (positioned in grid) - const eventCard = page.locator('.event-card').filter({ hasText: uniqueTitle }); - await expect(eventCard).toBeVisible({ timeout: 5000 }); - - const style = await eventCard.getAttribute('style'); - expect(style).toContain('top:'); - expect(style).toContain('height:'); - - // Cleanup - await eventCard.click(); - const editOverlay = page.locator('.quick-event-overlay'); - const deleteBtn = editOverlay.getByRole('button', { name: /löschen/i }); - if (await deleteBtn.isVisible()) { - await deleteBtn.click(); - const confirmBtn = page.getByRole('button', { name: /löschen|ja|bestätigen/i }); - if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) { - await confirmBtn.click(); - } - } - }); - - test('week view shows current time indicator on today', async ({ page }) => { - const timeIndicator = page.locator('.time-indicator'); - // There should be at least one time indicator (on today's column) - await expect(timeIndicator.first()).toBeVisible({ timeout: 5000 }); - - // It should have a top percentage style - const style = await timeIndicator.first().getAttribute('style'); - expect(style).toContain('top:'); - }); - - test('week view shows correct day headers', async ({ page }) => { - const dayHeaders = page.locator('.day-header'); - const count = await dayHeaders.count(); - - // Should have 5 (weekdays only) or 7 (full week) day headers - expect(count === 5 || count === 7).toBe(true); - - // Each header should have a day name and number - for (let i = 0; i < count; i++) { - const dayName = dayHeaders.nth(i).locator('.day-name'); - const dayNumber = dayHeaders.nth(i).locator('.day-number'); - await expect(dayName).toBeVisible(); - await expect(dayNumber).toBeVisible(); - } - }); - - test('today column is highlighted', async ({ page }) => { - const todayColumn = page.locator('.day-column.today'); - await expect(todayColumn).toBeVisible({ timeout: 5000 }); - - const todayHeader = page.locator('.day-header.today'); - await expect(todayHeader).toBeVisible(); - }); -}); diff --git a/apps/calendar/apps/web-archived/eslint.config.js b/apps/calendar/apps/web-archived/eslint.config.js deleted file mode 100644 index f0e51b62e..000000000 --- a/apps/calendar/apps/web-archived/eslint.config.js +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-check -import { - baseConfig, - typescriptConfig, - svelteConfig, - prettierConfig, -} from '@manacore/eslint-config'; - -export default [ - { - ignores: ['dist/**', '.svelte-kit/**', 'node_modules/**'], - }, - ...baseConfig, - ...typescriptConfig, - ...svelteConfig, - ...prettierConfig, -]; diff --git a/apps/calendar/apps/web-archived/package.json b/apps/calendar/apps/web-archived/package.json deleted file mode 100644 index c9ecff641..000000000 --- a/apps/calendar/apps/web-archived/package.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "name": "@calendar/web", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "eslint .", - "format": "prettier --write .", - "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "test": "vitest run", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui", - "test:e2e:headed": "playwright test --headed" - }, - "devDependencies": { - "@manacore/shared-pwa": "workspace:*", - "@manacore/shared-vite-config": "workspace:*", - "@playwright/test": "^1.51.0", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.47.1", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@tailwindcss/vite": "^4.1.7", - "@testing-library/jest-dom": "^6.9.1", - "@types/d3-force": "^3.0.0", - "@types/node": "^20.0.0", - "@types/suncalc": "^1.9.2", - "@vite-pwa/sveltekit": "^1.1.0", - "jsdom": "^25.0.1", - "prettier": "^3.1.1", - "prettier-plugin-svelte": "^3.1.2", - "svelte": "^5.41.0", - "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.7", - "tslib": "^2.4.1", - "typescript": "^5.9.3", - "vite": "^6.0.0", - "vitest": "^4.1.0" - }, - "dependencies": { - "@calendar/shared": "workspace:*", - "@manacore/shared-links": "workspace:*", - "@manacore/shared-api-client": "workspace:*", - "@manacore/shared-app-onboarding": "workspace:*", - "@manacore/shared-auth": "workspace:*", - "@manacore/shared-auth-stores": "workspace:*", - "@manacore/shared-auth-ui": "workspace:*", - "@manacore/shared-branding": "workspace:*", - "@manacore/shared-error-tracking": "workspace:*", - "@manacore/feedback": "workspace:*", - "@manacore/shared-i18n": "workspace:*", - "@manacore/help": "workspace:*", - "@manacore/shared-icons": "workspace:*", - "@manacore/local-store": "workspace:*", - "@manacore/shared-profile-ui": "workspace:*", - "@manacore/shared-splitscreen": "workspace:*", - "@manacore/shared-stores": "workspace:*", - "@manacore/subscriptions": "workspace:*", - "@manacore/shared-tags": "workspace:*", - "@manacore/shared-tailwind": "workspace:*", - "@manacore/shared-theme": "workspace:*", - "@manacore/shared-theme-ui": "workspace:*", - "@manacore/shared-types": "workspace:*", - "@manacore/shared-ui": "workspace:*", - "@manacore/shared-utils": "workspace:*", - "@neodrag/svelte": "^2.3.3", - "@sqlite.org/sqlite-wasm": "^3.49.1-build1", - "d3-force": "^3.0.0", - "date-fns": "^4.1.0", - "suncalc": "^1.9.0", - "svelte-dnd-action": "^0.9.68", - "svelte-i18n": "^4.0.1" - }, - "type": "module" -} diff --git a/apps/calendar/apps/web-archived/playwright.config.ts b/apps/calendar/apps/web-archived/playwright.config.ts deleted file mode 100644 index c574e6b1e..000000000 --- a/apps/calendar/apps/web-archived/playwright.config.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -export default defineConfig({ - testDir: './e2e', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: 1, - reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : [['html']], - - use: { - baseURL: 'http://localhost:5179', - trace: 'on-first-retry', - screenshot: 'only-on-failure', - video: 'retain-on-failure', - actionTimeout: 10000, - navigationTimeout: 30000, - }, - - timeout: 60000, - expect: { timeout: 5000 }, - - projects: process.env.CI - ? [ - { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, - { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, - { name: 'webkit', use: { ...devices['Desktop Safari'] } }, - ] - : [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], - - webServer: { - command: 'pnpm run build && pnpm run preview --port 5179', - port: 5179, - reuseExistingServer: !process.env.CI, - timeout: 120000, - }, - - outputDir: 'test-results/', -}); diff --git a/apps/calendar/apps/web-archived/scripts/generate-icons.mjs b/apps/calendar/apps/web-archived/scripts/generate-icons.mjs deleted file mode 100644 index 1af7bd63f..000000000 --- a/apps/calendar/apps/web-archived/scripts/generate-icons.mjs +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env node -/** - * Generate PWA icons from SVG favicon - * Run: node scripts/generate-icons.mjs - * Requires: sharp (available in workspace) - */ - -import { readFileSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const staticDir = join(__dirname, '..', 'static'); - -const sizes = [ - { name: 'favicon.png', size: 32 }, - { name: 'pwa-192x192.png', size: 192 }, - { name: 'pwa-512x512.png', size: 512 }, - { name: 'apple-touch-icon.png', size: 180 }, -]; - -async function generateIcons() { - try { - const sharp = (await import('sharp')).default; - const svgPath = join(staticDir, 'favicon.svg'); - const svgBuffer = readFileSync(svgPath); - - for (const { name, size } of sizes) { - const outputPath = join(staticDir, name); - await sharp(svgBuffer).resize(size, size).png().toFile(outputPath); - console.log(`Generated: ${name} (${size}x${size})`); - } - - console.log('\nAll icons generated successfully!'); - } catch (error) { - if (error.code === 'ERR_MODULE_NOT_FOUND') { - console.error('Sharp is not installed. Run: pnpm add -D sharp'); - } else { - console.error('Error generating icons:', error); - } - process.exit(1); - } -} - -generateIcons(); diff --git a/apps/calendar/apps/web-archived/src/app.css b/apps/calendar/apps/web-archived/src/app.css deleted file mode 100644 index 811bd3ffe..000000000 --- a/apps/calendar/apps/web-archived/src/app.css +++ /dev/null @@ -1,261 +0,0 @@ -@import "tailwindcss"; -@import "@manacore/shared-tailwind/themes.css"; - -/* Scan shared packages for Tailwind classes */ -@source "../../../packages/shared/src"; -@source "../../../../../packages/shared-ui/src"; -@source "../../../../../packages/shared-auth-ui/src"; -@source "../../../../../packages/shared-theme-ui/src"; -@source "../../../../../packages/shared-theme-ui/src/components"; -@source "../../../../../packages/shared-theme-ui/src/pages"; - -/* Calendar-specific CSS Variables */ -@layer base { - :root { - /* Spacing */ - --spacing-xs: 0.25rem; - --spacing-sm: 0.5rem; - --spacing-md: 1rem; - --spacing-lg: 1.5rem; - --spacing-xl: 2rem; - --spacing-2xl: 3rem; - - /* Border Radius */ - --radius-sm: 0.25rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; - --radius-full: 9999px; - - /* Transitions */ - --transition-fast: 150ms ease; - --transition-base: 200ms ease; - --transition-slow: 300ms ease; - - /* Calendar-specific */ - --hour-height: 48px; - --day-header-height: 40px; - --time-column-width: 56px; - } -} - -/* Calendar Grid Styles - Using plain CSS (not @layer) for guaranteed inclusion */ -/* Hour slot in day/week view */ -.hour-slot { - height: var(--hour-height); - border-bottom: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent); - position: relative; -} - -.hour-slot:hover { - background-color: color-mix(in srgb, var(--color-muted) 30%, transparent); -} - -/* Event card in calendar */ -.event-card { - background-color: var(--color-primary); - color: var(--color-primary-foreground); - border-radius: var(--radius-sm); - padding: 2px 6px; - font-size: 0.75rem; - overflow: hidden; - cursor: pointer; - transition: transform var(--transition-fast), box-shadow var(--transition-fast); -} - -.event-card:hover { - transform: scale(1.02); - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); -} - -/* Day cell in month view */ -.day-cell { - min-height: 100px; - border: 1px solid var(--color-border); - padding: var(--spacing-xs); - transition: background-color var(--transition-fast); -} - -.day-cell:hover { - background-color: color-mix(in srgb, var(--color-muted) 30%, transparent); -} - -.day-cell.today { - background-color: color-mix(in srgb, var(--color-primary) 10%, transparent); -} - -.day-cell.other-month { - opacity: 0.5; -} - -/* Time indicator (current time line) */ -.time-indicator { - position: absolute; - left: 0; - right: 0; - height: 2px; - background-color: var(--color-error); - z-index: 10; -} - -.time-indicator::before { - content: ''; - position: absolute; - left: -4px; - top: -4px; - width: 10px; - height: 10px; - border-radius: 50%; - background-color: var(--color-error); -} - -/* Mini calendar */ -.mini-calendar { - font-size: 0.875rem; -} - -.mini-calendar .day { - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-full); - cursor: pointer; - transition: all var(--transition-fast); -} - -.mini-calendar .day:hover { - background-color: var(--color-muted); -} - -.mini-calendar .day.today { - background-color: var(--color-primary); - color: var(--color-primary-foreground); -} - -.mini-calendar .day.selected { - border: 2px solid var(--color-primary); -} - -/* Card styles */ -.card { - background-color: var(--color-surface); - border-radius: var(--radius-lg); - padding: var(--spacing-lg); - border: 1px solid var(--color-border); -} - -/* Button styles */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.5rem 1rem; - border-radius: var(--radius-md); - font-weight: 500; - font-size: 0.875rem; - transition: all var(--transition-base); - cursor: pointer; - border: none; - background: transparent; -} - -.btn-primary { - background: var(--color-primary); - color: var(--color-primary-foreground); -} - -.btn-primary:hover { - filter: brightness(0.9); -} - -.btn-primary:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-secondary { - background: var(--color-secondary); - color: var(--color-secondary-foreground); -} - -.btn-secondary:hover { - filter: brightness(0.9); -} - -.btn-ghost { - background: transparent; - color: var(--color-foreground); -} - -.btn-ghost:hover { - background: var(--color-muted); -} - -.btn-icon { - padding: 0.5rem; -} - -.btn-sm { - padding: 0.25rem 0.5rem; - font-size: 0.75rem; -} - -/* Input styles */ -.input { - display: block; - width: 100%; - padding: 0.5rem 0.75rem; - border: 2px solid var(--color-border); - border-radius: var(--radius-md); - background-color: var(--color-background); - color: var(--color-foreground); - font-size: 0.875rem; - transition: border-color var(--transition-fast); -} - -.input:focus { - outline: none; - border-color: var(--color-primary); -} - -.input::placeholder { - color: var(--color-muted-foreground); -} - -/* Select styling */ -select.input { - appearance: none; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); - background-position: right 0.5rem center; - background-repeat: no-repeat; - background-size: 1.5em 1.5em; - padding-right: 2.5rem; -} - -/* Text colors */ -.text-destructive { - color: var(--color-error); -} - -/* Scrollbar styling */ -@layer utilities { - .scrollbar-thin::-webkit-scrollbar { - width: 6px; - height: 6px; - } - - .scrollbar-thin::-webkit-scrollbar-track { - background: transparent; - } - - .scrollbar-thin::-webkit-scrollbar-thumb { - background-color: hsl(var(--muted-foreground) / 0.3); - border-radius: 3px; - } - - .scrollbar-thin::-webkit-scrollbar-thumb:hover { - background-color: hsl(var(--muted-foreground) / 0.5); - } -} diff --git a/apps/calendar/apps/web-archived/src/app.d.ts b/apps/calendar/apps/web-archived/src/app.d.ts deleted file mode 100644 index c269fca6f..000000000 --- a/apps/calendar/apps/web-archived/src/app.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const __BUILD_HASH__: string; -declare const __BUILD_TIME__: string; diff --git a/apps/calendar/apps/web-archived/src/app.html b/apps/calendar/apps/web-archived/src/app.html deleted file mode 100644 index 6cf6c7464..000000000 --- a/apps/calendar/apps/web-archived/src/app.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Calendar - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/calendar/apps/web-archived/src/hooks.client.ts b/apps/calendar/apps/web-archived/src/hooks.client.ts deleted file mode 100644 index 07c1a0078..000000000 --- a/apps/calendar/apps/web-archived/src/hooks.client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { initErrorTracking, handleSvelteError } from '@manacore/shared-error-tracking/browser'; -import type { HandleClientError } from '@sveltejs/kit'; - -initErrorTracking({ - serviceName: 'calendar-web', - dsn: (window as any).__PUBLIC_GLITCHTIP_DSN__, - environment: import.meta.env.MODE, -}); - -export const handleError: HandleClientError = ({ error }) => { - handleSvelteError(error); -}; diff --git a/apps/calendar/apps/web-archived/src/hooks.server.ts b/apps/calendar/apps/web-archived/src/hooks.server.ts deleted file mode 100644 index 1e76ae88a..000000000 --- a/apps/calendar/apps/web-archived/src/hooks.server.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Server Hooks for SvelteKit - * - Injects runtime environment variables for client-side use - * - Adds security headers - * - Auth is handled client-side via Mana Core Auth - */ - -import type { Handle } from '@sveltejs/kit'; -import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; - -// Get client-side URLs from environment (Docker runtime) -// In dev mode, Vite exposes .env vars via import.meta.env, not process.env -const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = - process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || - process.env.PUBLIC_MANA_CORE_AUTH_URL || - 'http://localhost:3001'; -const PUBLIC_BACKEND_URL_CLIENT = - process.env.PUBLIC_BACKEND_URL_CLIENT || - process.env.PUBLIC_BACKEND_URL || - 'http://localhost:3014'; -const PUBLIC_STT_URL = process.env.PUBLIC_STT_URL || 'https://stt-api.mana.how'; - -// Cross-app integration URLs (for contacts API) -const PUBLIC_CONTACTS_API_URL = process.env.PUBLIC_CONTACTS_API_URL || 'http://localhost:3015'; -const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; - -export const handle: Handle = async ({ event, resolve }) => { - const response = await resolve(event, { - transformPageChunk: ({ html }) => { - // Inject runtime environment variables into the HTML - // These will be available on window.__PUBLIC_*__ for client-side code - const envScript = ``; - return html.replace('', `${envScript}`); - }, - }); - - setSecurityHeaders(response, { - connectSrc: [ - PUBLIC_MANA_CORE_AUTH_URL_CLIENT, - PUBLIC_BACKEND_URL_CLIENT, - PUBLIC_STT_URL, - PUBLIC_CONTACTS_API_URL, - ], - }); - - return response; -}; diff --git a/apps/calendar/apps/web-archived/src/lib/api/birthdays.ts b/apps/calendar/apps/web-archived/src/lib/api/birthdays.ts deleted file mode 100644 index d5e73627c..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/birthdays.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Cross-App API Client for Contacts Backend - Birthday Data - * Allows Calendar app to fetch contact birthdays for display - */ - -import { browser } from '$app/environment'; -import { env } from '$env/dynamic/public'; -import { createApiClient } from '@manacore/shared-api-client'; -import { authStore } from '$lib/stores/auth.svelte'; - -// Get contacts API base URL from injected window variable (browser) or env (SSR) -function getContactsApiBase(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_CONTACTS_API_URL__?: string }) - .__PUBLIC_CONTACTS_API_URL__; - if (injectedUrl) return injectedUrl; - } - return env.PUBLIC_CONTACTS_API_URL || 'http://localhost:3015'; -} - -let _contactsClient: ReturnType | null = null; - -function getContactsClient() { - if (!_contactsClient) { - _contactsClient = createApiClient({ - baseUrl: getContactsApiBase(), - apiPrefix: '/api/v1', - getAuthToken: () => authStore.getValidToken(), - timeout: 30000, - debug: import.meta.env.DEV, - useRuntimeUrl: false, - }); - } - return _contactsClient; -} - -// For backwards compatibility -const contactsClient = { - get: (endpoint: string) => getContactsClient().get(endpoint), - post: (endpoint: string, body?: unknown) => getContactsClient().post(endpoint, body), - put: (endpoint: string, body?: unknown) => getContactsClient().put(endpoint, body), - patch: (endpoint: string, body?: unknown) => getContactsClient().patch(endpoint, body), - delete: (endpoint: string) => getContactsClient().delete(endpoint), -}; - -// ============================================ -// Types for Birthday Integration -// ============================================ - -/** - * Lightweight contact data for birthday display - * Only essential fields from Contacts API - */ -export interface ContactBirthdaySummary { - id: string; - displayName: string | null; - firstName: string | null; - lastName: string | null; - birthday: string; // YYYY-MM-DD format - photoUrl: string | null; -} - -/** - * Birthday event for calendar display - * Generated from ContactBirthdaySummary with display date - */ -export interface BirthdayEvent { - id: string; // Format: birthday-{contactId}-{date} - contactId: string; - title: string; // "{Name}'s Geburtstag" - displayName: string; - photoUrl: string | null; - birthday: string; // Original birthday date - age: number; // Age on this birthday (0 if birth year unknown) - startTime: string; // ISO date of the birthday occurrence - endTime: string; // Same as startTime (all-day event) - isAllDay: true; - isBirthday: true; // Type discriminator - calendarId: string; // Virtual calendar ID -} - -// ============================================ -// API Response Types -// ============================================ - -interface BirthdaysResponse { - contacts: ContactBirthdaySummary[]; -} - -// ============================================ -// API Functions -// ============================================ - -/** - * Fetch all contacts with birthdays from Contacts service - */ -export async function getBirthdays(): Promise<{ - data: ContactBirthdaySummary[] | null; - error: Error | null; -}> { - const result = await contactsClient.get('/contacts/birthdays'); - if (result.error) { - return { data: null, error: new Error(result.error.message) }; - } - return { - data: result.data?.contacts || null, - error: null, - }; -} - -// ============================================ -// Helper Functions -// ============================================ - -/** - * Get display name from contact, with fallback - */ -export function getContactDisplayName(contact: ContactBirthdaySummary): string { - if (contact.displayName) return contact.displayName; - const fullName = [contact.firstName, contact.lastName].filter(Boolean).join(' '); - return fullName || 'Unbekannt'; -} - -/** - * Birthday calendar constants - */ -export const BIRTHDAY_CALENDAR = { - id: '__birthdays__', - name: 'Geburtstage', - color: '#EC4899', // Pink -} as const; diff --git a/apps/calendar/apps/web-archived/src/lib/api/calendars.ts b/apps/calendar/apps/web-archived/src/lib/api/calendars.ts deleted file mode 100644 index 4b6197430..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/calendars.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Calendar API Client - */ - -import { fetchApi } from './client'; -import type { Calendar, CreateCalendarInput, UpdateCalendarInput } from '@calendar/shared'; - -export async function getCalendars() { - return fetchApi('/calendars'); -} - -export async function getCalendar(id: string) { - return fetchApi(`/calendars/${id}`); -} - -export async function createCalendar(data: CreateCalendarInput) { - const result = await fetchApi<{ calendar: Calendar }>('/calendars', { - method: 'POST', - body: data, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.calendar, error: null }; -} - -export async function updateCalendar(id: string, data: UpdateCalendarInput) { - const result = await fetchApi<{ calendar: Calendar }>(`/calendars/${id}`, { - method: 'PUT', - body: data, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.calendar, error: null }; -} - -export async function deleteCalendar(id: string) { - return fetchApi(`/calendars/${id}`, { - method: 'DELETE', - }); -} diff --git a/apps/calendar/apps/web-archived/src/lib/api/client.ts b/apps/calendar/apps/web-archived/src/lib/api/client.ts deleted file mode 100644 index 8d3bb12b8..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/client.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * API Client for Calendar Backend - * Uses @manacore/shared-api-client for consistent error handling - * - * Token handling: Uses authStore.getValidToken() which automatically - * refreshes expired tokens before making requests. - */ - -import { browser } from '$app/environment'; -import { env } from '$env/dynamic/public'; -import { createApiClient, type ApiResult, type ApiClient } from '@manacore/shared-api-client'; -import { authStore } from '$lib/stores/auth.svelte'; - -// Use client URL in browser (injected by hooks.server.ts), SSR URL on server -function getApiBase(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) - .__PUBLIC_BACKEND_URL__; - if (injectedUrl) return injectedUrl; - } - return env.PUBLIC_BACKEND_URL || 'http://localhost:3014'; -} - -/** - * Calendar API client instance (lazy initialized) - * - Auto token handling via authStore.getValidToken() - * - Consistent ApiResult response format - */ -let _api: ApiClient | null = null; - -function getApi(): ApiClient { - if (!_api) { - _api = createApiClient({ - baseUrl: getApiBase(), - apiPrefix: '/api/v1', - getAuthToken: () => authStore.getValidToken(), - timeout: 30000, - debug: import.meta.env.DEV, - }); - } - return _api; -} - -/** - * Request deduplication for GET requests - * Prevents identical concurrent requests from being sent multiple times - */ -const pendingRequests = new Map>>(); - -/** - * Legacy fetchApi interface for backwards compatibility - */ -export interface FetchOptions { - method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; - body?: unknown; - token?: string; - isFormData?: boolean; - timeout?: number; -} - -/** - * Fetch API wrapper using shared client - * Maintains backward compatibility with existing code - * GET requests are deduplicated — identical concurrent GETs share one in-flight request - */ -export async function fetchApi( - endpoint: string, - options: FetchOptions = {} -): Promise> { - const { method = 'GET', body, isFormData = false } = options; - const api = getApi(); - - if (isFormData && body instanceof FormData) { - return api.upload(endpoint, body); - } - - // Deduplicate GET requests - if (method === 'GET') { - const existing = pendingRequests.get(endpoint); - if (existing) { - return existing as Promise>; - } - const promise = api.get(endpoint).finally(() => { - pendingRequests.delete(endpoint); - }); - pendingRequests.set(endpoint, promise as Promise>); - return promise; - } - - switch (method) { - case 'POST': - return api.post(endpoint, body); - case 'PUT': - return api.put(endpoint, body); - case 'PATCH': - return api.patch(endpoint, body); - case 'DELETE': - return api.delete(endpoint); - default: - return api.get(endpoint); - } -} - -// Re-export types for backwards compatibility -export type { ApiResult }; diff --git a/apps/calendar/apps/web-archived/src/lib/api/event-tags.ts b/apps/calendar/apps/web-archived/src/lib/api/event-tags.ts deleted file mode 100644 index a3fb56a8b..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/event-tags.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Event Tags API Client - Uses Calendar Backend API - * - * This module provides the event tags interface for the Calendar app, - * using the calendar backend's /event-tags endpoint which supports - * tag groups (groupId). - */ - -import { fetchApi } from './client'; -import type { EventTag } from '@calendar/shared'; - -// Re-export EventTag from shared -export type { EventTag }; - -export interface CreateEventTagInput { - name: string; - color?: string; - groupId?: string | null; -} - -export interface UpdateEventTagInput { - name?: string; - color?: string; - groupId?: string | null; -} - -export async function getEventTags() { - const result = await fetchApi<{ tags: EventTag[] }>('/event-tags'); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.tags, error: null }; -} - -export async function getEventTag(id: string) { - const result = await fetchApi<{ tag: EventTag }>(`/event-tags/${id}`); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.tag, error: null }; -} - -export async function createEventTag(data: CreateEventTagInput) { - const result = await fetchApi<{ tag: EventTag }>('/event-tags', { - method: 'POST', - body: data, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.tag, error: null }; -} - -export async function updateEventTag(id: string, data: UpdateEventTagInput) { - const result = await fetchApi<{ tag: EventTag }>(`/event-tags/${id}`, { - method: 'PUT', - body: data, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.tag, error: null }; -} - -export async function deleteEventTag(id: string) { - const result = await fetchApi<{ success: boolean }>(`/event-tags/${id}`, { - method: 'DELETE', - }); - return result; -} diff --git a/apps/calendar/apps/web-archived/src/lib/api/events.test.ts b/apps/calendar/apps/web-archived/src/lib/api/events.test.ts deleted file mode 100644 index 393871859..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/events.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { CalendarEvent } from '@calendar/shared'; - -// Mock the client module -vi.mock('./client', () => ({ - fetchApi: vi.fn(), -})); - -import { fetchApi } from './client'; -import { getEvents, getEvent, createEvent, updateEvent, deleteEvent } from './events'; - -const mockFetchApi = vi.mocked(fetchApi); - -function makeEvent(overrides: Partial = {}): CalendarEvent { - return { - id: 'evt-1', - calendarId: 'cal-1', - userId: 'user-1', - title: 'Test Event', - description: null, - location: null, - startTime: '2026-03-15T10:00:00Z', - endTime: '2026-03-15T11:00:00Z', - isAllDay: false, - timezone: 'Europe/Berlin', - recurrenceRule: null, - recurrenceEndDate: null, - recurrenceExceptions: null, - parentEventId: null, - color: null, - status: 'confirmed', - externalId: null, - metadata: null, - createdAt: '2026-03-01T00:00:00Z', - updatedAt: '2026-03-01T00:00:00Z', - ...overrides, - }; -} - -describe('events API client', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('getEvents', () => { - it('should build query params with startDate and endDate', async () => { - mockFetchApi.mockResolvedValue({ - data: { events: [], pagination: { offset: 0, count: 0 } }, - error: null, - }); - - await getEvents({ - startDate: '2026-03-01T00:00:00', - endDate: '2026-03-31T23:59:59', - }); - - expect(mockFetchApi).toHaveBeenCalledOnce(); - const url = mockFetchApi.mock.calls[0][0]; - expect(url).toContain('startDate=2026-03-01T00%3A00%3A00'); - expect(url).toContain('endDate=2026-03-31T23%3A59%3A59'); - }); - - it('should include calendarIds when provided', async () => { - mockFetchApi.mockResolvedValue({ - data: { events: [], pagination: { offset: 0, count: 0 } }, - error: null, - }); - - await getEvents({ - startDate: '2026-03-01T00:00:00', - endDate: '2026-03-31T23:59:59', - calendarIds: ['cal-1', 'cal-2'], - }); - - const url = mockFetchApi.mock.calls[0][0]; - expect(url).toContain('calendarIds=cal-1%2Ccal-2'); - }); - - it('should include search param when provided', async () => { - mockFetchApi.mockResolvedValue({ - data: { events: [], pagination: { offset: 0, count: 0 } }, - error: null, - }); - - await getEvents({ - startDate: '2026-03-01T00:00:00', - endDate: '2026-03-31T23:59:59', - search: 'meeting', - }); - - const url = mockFetchApi.mock.calls[0][0]; - expect(url).toContain('search=meeting'); - }); - - it('should include limit and offset when provided', async () => { - mockFetchApi.mockResolvedValue({ - data: { events: [], pagination: { offset: 0, count: 0 } }, - error: null, - }); - - await getEvents({ - startDate: '2026-03-01T00:00:00', - endDate: '2026-03-31T23:59:59', - limit: 10, - offset: 20, - }); - - const url = mockFetchApi.mock.calls[0][0]; - expect(url).toContain('limit=10'); - expect(url).toContain('offset=20'); - }); - - it('should extract events array from response', async () => { - const events = [makeEvent(), makeEvent({ id: 'evt-2', title: 'Second' })]; - mockFetchApi.mockResolvedValue({ - data: { events, pagination: { offset: 0, count: 2 } }, - error: null, - }); - - const result = await getEvents({ - startDate: '2026-03-01T00:00:00', - endDate: '2026-03-31T23:59:59', - }); - - expect(result.data).toHaveLength(2); - expect(result.error).toBeNull(); - expect(result.pagination).toEqual({ offset: 0, count: 2 }); - }); - - it('should return error when API fails', async () => { - mockFetchApi.mockResolvedValue({ - data: null, - error: { message: 'Server error', code: 'SERVER_ERROR', status: 500 }, - }); - - const result = await getEvents({ - startDate: '2026-03-01T00:00:00', - endDate: '2026-03-31T23:59:59', - }); - - expect(result.data).toBeNull(); - expect(result.error).toEqual({ - message: 'Server error', - code: 'SERVER_ERROR', - status: 500, - }); - }); - }); - - describe('getEvent', () => { - it('should fetch a single event by ID', async () => { - const event = makeEvent(); - mockFetchApi.mockResolvedValue({ - data: { event }, - error: null, - }); - - const result = await getEvent('evt-1'); - - expect(mockFetchApi).toHaveBeenCalledWith('/events/evt-1'); - expect(result.data).toEqual(event); - expect(result.error).toBeNull(); - }); - - it('should return error when event not found', async () => { - mockFetchApi.mockResolvedValue({ - data: null, - error: { message: 'Not found', code: 'NOT_FOUND', status: 404 }, - }); - - const result = await getEvent('nonexistent'); - - expect(result.data).toBeNull(); - expect(result.error?.code).toBe('NOT_FOUND'); - }); - }); - - describe('createEvent', () => { - it('should send POST request with event data', async () => { - const event = makeEvent(); - mockFetchApi.mockResolvedValue({ - data: { event }, - error: null, - }); - - const input = { - calendarId: 'cal-1', - title: 'Test Event', - startTime: '2026-03-15T10:00:00Z', - endTime: '2026-03-15T11:00:00Z', - }; - - const result = await createEvent(input); - - expect(mockFetchApi).toHaveBeenCalledWith('/events', { - method: 'POST', - body: input, - }); - expect(result.data).toEqual(event); - expect(result.error).toBeNull(); - }); - - it('should return error on creation failure', async () => { - mockFetchApi.mockResolvedValue({ - data: null, - error: { message: 'Validation failed', code: 'VALIDATION_ERROR', status: 400 }, - }); - - const result = await createEvent({ - title: '', - startTime: '2026-03-15T10:00:00Z', - endTime: '2026-03-15T11:00:00Z', - }); - - expect(result.data).toBeNull(); - expect(result.error?.code).toBe('VALIDATION_ERROR'); - }); - }); - - describe('updateEvent', () => { - it('should send PUT request with update data', async () => { - const event = makeEvent({ title: 'Updated Title' }); - mockFetchApi.mockResolvedValue({ - data: { event }, - error: null, - }); - - const updateData = { title: 'Updated Title' }; - const result = await updateEvent('evt-1', updateData); - - expect(mockFetchApi).toHaveBeenCalledWith('/events/evt-1', { - method: 'PUT', - body: updateData, - }); - expect(result.data).toEqual(event); - expect(result.error).toBeNull(); - }); - - it('should return error on update failure', async () => { - mockFetchApi.mockResolvedValue({ - data: null, - error: { message: 'Forbidden', code: 'FORBIDDEN', status: 403 }, - }); - - const result = await updateEvent('evt-1', { title: 'Updated' }); - - expect(result.data).toBeNull(); - expect(result.error?.code).toBe('FORBIDDEN'); - }); - }); - - describe('deleteEvent', () => { - it('should send DELETE request', async () => { - mockFetchApi.mockResolvedValue({ - data: null, - error: null, - }); - - const result = await deleteEvent('evt-1'); - - expect(mockFetchApi).toHaveBeenCalledWith('/events/evt-1', { - method: 'DELETE', - }); - expect(result.error).toBeNull(); - }); - - it('should return error on delete failure', async () => { - mockFetchApi.mockResolvedValue({ - data: null, - error: { message: 'Not found', code: 'NOT_FOUND', status: 404 }, - }); - - const result = await deleteEvent('nonexistent'); - - expect(result.error?.code).toBe('NOT_FOUND'); - }); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/api/events.ts b/apps/calendar/apps/web-archived/src/lib/api/events.ts deleted file mode 100644 index e12971d17..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/events.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Events API Client - */ - -import { fetchApi } from './client'; -import type { CalendarEvent, CreateEventInput, UpdateEventInput } from '@calendar/shared'; - -export interface PaginationMeta { - limit?: number; - offset: number; - count: number; -} - -export interface QueryEventsParams { - startDate: string; - endDate: string; - calendarIds?: string[]; - search?: string; - limit?: number; - offset?: number; -} - -export async function getEvents(params: QueryEventsParams) { - const searchParams = new URLSearchParams({ - startDate: params.startDate, - endDate: params.endDate, - }); - if (params.calendarIds?.length) { - searchParams.set('calendarIds', params.calendarIds.join(',')); - } - if (params.search) { - searchParams.set('search', params.search); - } - if (params.limit !== undefined) { - searchParams.set('limit', String(params.limit)); - } - if (params.offset !== undefined) { - searchParams.set('offset', String(params.offset)); - } - const result = await fetchApi<{ events: CalendarEvent[]; pagination: PaginationMeta }>( - `/events?${searchParams.toString()}` - ); - if (result.error || !result.data) { - return { data: null, pagination: null, error: result.error }; - } - return { data: result.data.events, pagination: result.data.pagination, error: null }; -} - -export async function searchEvents(query: string, limit: number = 10) { - // Search events within a wide range (1 year past to 1 year future) - const oneYearAgo = new Date(); - oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); - const oneYearFromNow = new Date(); - oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); - - return getEvents({ - startDate: oneYearAgo.toISOString(), - endDate: oneYearFromNow.toISOString(), - search: query, - }); -} - -export async function getEvent(id: string) { - const result = await fetchApi<{ event: CalendarEvent }>(`/events/${id}`); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.event, error: null }; -} - -export async function getEventsByCalendar(calendarId: string) { - return fetchApi(`/events/calendar/${calendarId}`); -} - -export async function createEvent(data: CreateEventInput) { - const result = await fetchApi<{ event: CalendarEvent }>('/events', { - method: 'POST', - body: data, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.event, error: null }; -} - -export async function updateEvent(id: string, data: UpdateEventInput) { - const result = await fetchApi<{ event: CalendarEvent }>(`/events/${id}`, { - method: 'PUT', - body: data, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.event, error: null }; -} - -export async function deleteEvent(id: string) { - return fetchApi(`/events/${id}`, { - method: 'DELETE', - }); -} diff --git a/apps/calendar/apps/web-archived/src/lib/api/reminders.test.ts b/apps/calendar/apps/web-archived/src/lib/api/reminders.test.ts deleted file mode 100644 index b9a1224e0..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/reminders.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -vi.mock('./client', () => ({ - fetchApi: vi.fn(), -})); - -import { fetchApi } from './client'; -import { getReminders, createReminder, deleteReminder } from './reminders'; - -const mockFetchApi = vi.mocked(fetchApi); - -describe('reminders API client', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('getReminders', () => { - it('should GET /events/:eventId/reminders', async () => { - mockFetchApi.mockResolvedValue({ - data: [ - { id: 'rem-1', eventId: 'evt-1', minutesBefore: 15, status: 'pending' }, - { id: 'rem-2', eventId: 'evt-1', minutesBefore: 60, status: 'sent' }, - ], - error: null, - }); - - const result = await getReminders('evt-1'); - - expect(mockFetchApi).toHaveBeenCalledWith('/events/evt-1/reminders'); - expect(result.data).toHaveLength(2); - }); - - it('should return error on failure', async () => { - mockFetchApi.mockResolvedValue({ - data: null, - error: { message: 'Not found', code: 'NOT_FOUND', status: 404 }, - }); - - const result = await getReminders('nonexistent'); - - expect(result.error).toBeTruthy(); - }); - }); - - describe('createReminder', () => { - it('should POST to /events/:eventId/reminders with body', async () => { - mockFetchApi.mockResolvedValue({ - data: { id: 'rem-new', eventId: 'evt-1', minutesBefore: 30, status: 'pending' }, - error: null, - }); - - const result = await createReminder('evt-1', { - eventId: 'evt-1', - minutesBefore: 30, - notifyPush: true, - notifyEmail: false, - }); - - expect(mockFetchApi).toHaveBeenCalledWith('/events/evt-1/reminders', { - method: 'POST', - body: { - eventId: 'evt-1', - minutesBefore: 30, - notifyPush: true, - notifyEmail: false, - }, - }); - expect(result.data).toBeTruthy(); - }); - }); - - describe('deleteReminder', () => { - it('should DELETE /reminders/:id', async () => { - mockFetchApi.mockResolvedValue({ data: null, error: null }); - - await deleteReminder('rem-1'); - - expect(mockFetchApi).toHaveBeenCalledWith('/reminders/rem-1', { - method: 'DELETE', - }); - }); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/api/reminders.ts b/apps/calendar/apps/web-archived/src/lib/api/reminders.ts deleted file mode 100644 index c932f9a3a..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/reminders.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Reminders API Client - */ - -import { fetchApi } from './client'; -import type { Reminder, CreateReminderInput } from '@calendar/shared'; - -export async function getReminders(eventId: string) { - return fetchApi(`/events/${eventId}/reminders`); -} - -export async function createReminder(eventId: string, data: CreateReminderInput) { - return fetchApi(`/events/${eventId}/reminders`, { - method: 'POST', - body: data, - }); -} - -export async function deleteReminder(id: string) { - return fetchApi(`/reminders/${id}`, { - method: 'DELETE', - }); -} diff --git a/apps/calendar/apps/web-archived/src/lib/api/shares.ts b/apps/calendar/apps/web-archived/src/lib/api/shares.ts deleted file mode 100644 index 93f43e77d..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/shares.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Calendar Shares API Client - */ - -import { fetchApi } from './client'; -import type { CalendarShare, CreateShareInput, UpdateShareInput } from '@calendar/shared'; - -export async function getShares(calendarId: string) { - return fetchApi(`/calendars/${calendarId}/shares`); -} - -export async function createShare(calendarId: string, data: CreateShareInput) { - return fetchApi(`/calendars/${calendarId}/shares`, { - method: 'POST', - body: data, - }); -} - -export async function acceptShare(shareId: string) { - return fetchApi(`/shares/${shareId}/accept`, { - method: 'POST', - }); -} - -export async function declineShare(shareId: string) { - return fetchApi(`/shares/${shareId}/decline`, { - method: 'POST', - }); -} - -export async function updateShare(shareId: string, data: UpdateShareInput) { - return fetchApi(`/shares/${shareId}`, { - method: 'PUT', - body: data, - }); -} - -export async function deleteShare(calendarId: string, shareId: string) { - return fetchApi(`/calendars/${calendarId}/shares/${shareId}`, { - method: 'DELETE', - }); -} - -export async function getInvitations() { - return fetchApi('/shares/invitations'); -} - -export async function getSharedWithMe() { - return fetchApi('/shares/shared-with-me'); -} diff --git a/apps/calendar/apps/web-archived/src/lib/api/sync.test.ts b/apps/calendar/apps/web-archived/src/lib/api/sync.test.ts deleted file mode 100644 index fd4be5d95..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/sync.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -vi.mock('./client', () => ({ - fetchApi: vi.fn(), -})); - -import { fetchApi } from './client'; -import { - getExternalCalendars, - connectExternalCalendar, - updateExternalCalendar, - disconnectExternalCalendar, - triggerSync, - discoverCalDav, - getGoogleAuthUrl, - getICalExportUrl, -} from './sync'; - -const mockFetchApi = vi.mocked(fetchApi); - -describe('sync API client', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('getExternalCalendars', () => { - it('should fetch external calendars', async () => { - mockFetchApi.mockResolvedValue({ - data: { calendars: [{ id: 'ext-1', name: 'Test' }] }, - error: null, - }); - const result = await getExternalCalendars(); - expect(mockFetchApi).toHaveBeenCalledWith('/sync/external'); - expect(result.data).toHaveLength(1); - expect(result.data![0].name).toBe('Test'); - }); - - it('should return error on failure', async () => { - mockFetchApi.mockResolvedValue({ - data: null, - error: { message: 'Not found', code: 'NOT_FOUND', status: 404 }, - }); - const result = await getExternalCalendars(); - expect(result.data).toBeNull(); - expect(result.error).toBeTruthy(); - }); - }); - - describe('connectExternalCalendar', () => { - it('should POST to /sync/external', async () => { - mockFetchApi.mockResolvedValue({ - data: { calendar: { id: 'ext-new', name: 'New Cal' } }, - error: null, - }); - const result = await connectExternalCalendar({ - name: 'New Cal', - provider: 'ical_url', - calendarUrl: 'https://example.com/cal.ics', - }); - expect(mockFetchApi).toHaveBeenCalledWith('/sync/external', { - method: 'POST', - body: { name: 'New Cal', provider: 'ical_url', calendarUrl: 'https://example.com/cal.ics' }, - }); - expect(result.data!.name).toBe('New Cal'); - }); - }); - - describe('disconnectExternalCalendar', () => { - it('should DELETE /sync/external/:id', async () => { - mockFetchApi.mockResolvedValue({ data: { success: true }, error: null }); - await disconnectExternalCalendar('ext-1'); - expect(mockFetchApi).toHaveBeenCalledWith('/sync/external/ext-1', { method: 'DELETE' }); - }); - }); - - describe('triggerSync', () => { - it('should POST to /sync/external/:id/sync', async () => { - mockFetchApi.mockResolvedValue({ data: { success: true, eventsImported: 10 }, error: null }); - const result = await triggerSync('ext-1'); - expect(mockFetchApi).toHaveBeenCalledWith('/sync/external/ext-1/sync', { method: 'POST' }); - expect(result.data!.eventsImported).toBe(10); - }); - }); - - describe('discoverCalDav', () => { - it('should POST credentials to /sync/caldav/discover', async () => { - mockFetchApi.mockResolvedValue({ - data: { calendars: [{ url: 'https://cal.example.com/personal', name: 'Personal' }] }, - error: null, - }); - const result = await discoverCalDav('https://cal.example.com', 'user@example.com', 'pass'); - expect(mockFetchApi).toHaveBeenCalledWith('/sync/caldav/discover', { - method: 'POST', - body: { - serverUrl: 'https://cal.example.com', - username: 'user@example.com', - password: 'pass', - }, - }); - expect(result.data).toHaveLength(1); - }); - }); - - describe('getGoogleAuthUrl', () => { - it('should GET /sync/google/auth-url', async () => { - mockFetchApi.mockResolvedValue({ - data: { url: 'https://accounts.google.com/auth' }, - error: null, - }); - const result = await getGoogleAuthUrl(); - expect(mockFetchApi).toHaveBeenCalledWith('/sync/google/auth-url'); - expect(result.data).toContain('google'); - }); - }); - - describe('getICalExportUrl', () => { - it('should return the correct export URL', () => { - expect(getICalExportUrl('cal-123')).toBe('/api/v1/calendars/cal-123/export.ics'); - }); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/api/sync.ts b/apps/calendar/apps/web-archived/src/lib/api/sync.ts deleted file mode 100644 index c86505d6e..000000000 --- a/apps/calendar/apps/web-archived/src/lib/api/sync.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * External Calendar Sync API Client - */ - -import { fetchApi } from './client'; -import type { - ExternalCalendar, - ConnectExternalCalendarInput, - CalDavDiscoveryResult, -} from '@calendar/shared'; - -export interface UpdateExternalCalendarInput { - name?: string; - syncEnabled?: boolean; - syncDirection?: 'import' | 'export' | 'both'; - syncInterval?: number; - color?: string; - isVisible?: boolean; -} - -// ==================== External Calendars CRUD ==================== - -export async function getExternalCalendars() { - const result = await fetchApi<{ calendars: ExternalCalendar[] }>('/sync/external'); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.calendars, error: null }; -} - -export async function getExternalCalendar(id: string) { - const result = await fetchApi<{ calendar: ExternalCalendar }>(`/sync/external/${id}`); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.calendar, error: null }; -} - -export async function connectExternalCalendar(data: ConnectExternalCalendarInput) { - const result = await fetchApi<{ calendar: ExternalCalendar }>('/sync/external', { - method: 'POST', - body: data, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.calendar, error: null }; -} - -export async function updateExternalCalendar(id: string, data: UpdateExternalCalendarInput) { - const result = await fetchApi<{ calendar: ExternalCalendar }>(`/sync/external/${id}`, { - method: 'PUT', - body: data, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.calendar, error: null }; -} - -export async function disconnectExternalCalendar(id: string) { - return fetchApi<{ success: boolean }>(`/sync/external/${id}`, { - method: 'DELETE', - }); -} - -// ==================== Sync Operations ==================== - -export async function triggerSync(id: string) { - const result = await fetchApi<{ - success: boolean; - eventsImported?: number; - eventsExported?: number; - }>(`/sync/external/${id}/sync`, { method: 'POST' }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data, error: null }; -} - -// ==================== CalDAV Discovery ==================== - -export async function discoverCalDav(serverUrl: string, username: string, password: string) { - const result = await fetchApi('/sync/caldav/discover', { - method: 'POST', - body: { serverUrl, username, password }, - }); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.calendars, error: null }; -} - -// ==================== Google OAuth ==================== - -export async function getGoogleAuthUrl() { - const result = await fetchApi<{ url: string }>('/sync/google/auth-url'); - if (result.error || !result.data) { - return { data: null, error: result.error }; - } - return { data: result.data.url, error: null }; -} - -// ==================== iCal Export ==================== - -export function getICalExportUrl(calendarId: string): string { - // This returns the URL for direct browser download - return `/api/v1/calendars/${calendarId}/export.ics`; -} diff --git a/apps/calendar/apps/web-archived/src/lib/components/AppSlider.svelte b/apps/calendar/apps/web-archived/src/lib/components/AppSlider.svelte deleted file mode 100644 index b36ac8d18..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/AppSlider.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/LanguageSelector.svelte b/apps/calendar/apps/web-archived/src/lib/components/LanguageSelector.svelte deleted file mode 100644 index 5630e7582..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/LanguageSelector.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/ServiceStatusBanner.svelte b/apps/calendar/apps/web-archived/src/lib/components/ServiceStatusBanner.svelte deleted file mode 100644 index 6bd6310bd..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/ServiceStatusBanner.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - -{#if !available} - -{/if} - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/agenda/AgendaFilters.svelte b/apps/calendar/apps/web-archived/src/lib/components/agenda/AgendaFilters.svelte deleted file mode 100644 index f361e72a5..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/agenda/AgendaFilters.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - -
-
-
- - onRangeChange?.(v as '7' | '30' | 'all')} - placeholder="Zeitraum" - embedded={true} - /> -
-
-
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/agenda/AgendaItem.svelte b/apps/calendar/apps/web-archived/src/lib/components/agenda/AgendaItem.svelte deleted file mode 100644 index 34e56cc4d..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/agenda/AgendaItem.svelte +++ /dev/null @@ -1,110 +0,0 @@ - - - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/birthday/BirthdayPopover.svelte b/apps/calendar/apps/web-archived/src/lib/components/birthday/BirthdayPopover.svelte deleted file mode 100644 index ad2b3853c..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/birthday/BirthdayPopover.svelte +++ /dev/null @@ -1,269 +0,0 @@ - - - - - -
e.key === 'Escape' && onClose()} - role="button" - tabindex="-1" -> - - -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/AgendaView.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/AgendaView.svelte deleted file mode 100644 index 7948c8930..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/AgendaView.svelte +++ /dev/null @@ -1,440 +0,0 @@ - - -
- {#if groupedEvents.length === 0} -
- -

Keine Termine in diesem Zeitraum

-
- {:else} -
- {#each groupedEvents as group} -
-

- {formatDateHeader(group.date)} -

- -
- {#each group.events as event} - -
{ - e.preventDefault(); - e.stopPropagation(); - handleContextMenu(event, e); - }} - > -
-
-
- {#if event.isAllDay} - Ganztägig - {:else} - {format(toDate(event.startTime), 'HH:mm')} - {format( - toDate(event.endTime), - 'HH:mm' - )} - {/if} -
- - handleTitleKeydown(e, event)} - onblur={(e) => handleTitleBlur(event, e.target as HTMLSpanElement)} - onclick={(e) => e.stopPropagation()} - > - {event.title} - - {#if event.location} -
- - {event.location} -
- {/if} -
- -
- {/each} -
-
- {/each} -
- {/if} -
- - { - contextMenuVisible = false; - contextMenuEvent = null; - }} -/> - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarHeader.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarHeader.svelte deleted file mode 100644 index 23c01f7f5..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarHeader.svelte +++ /dev/null @@ -1,63 +0,0 @@ - - - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarSidebar.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarSidebar.svelte deleted file mode 100644 index 2aa11d03e..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarSidebar.svelte +++ /dev/null @@ -1,184 +0,0 @@ - - -
-
-

Meine Kalender

- -
- -
- {#each calendarsCtx.value as calendar} - - {/each} - - {#if calendarsCtx.value.length === 0} -

Keine Kalender vorhanden

- {/if} -
- - {#if externalCalendarsStore.calendars.length > 0} -
-

Externe Kalender

- -
- -
- {#each externalCalendarsStore.calendars as cal} - - {/each} -
- {/if} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarToolbar.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarToolbar.svelte deleted file mode 100644 index baac9a229..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarToolbar.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarToolbarContent.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarToolbarContent.svelte deleted file mode 100644 index 404f27d82..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/CalendarToolbarContent.svelte +++ /dev/null @@ -1,102 +0,0 @@ - - -
- - - - {#if !vertical} - - {/if} - - - settingsStore.set('showOnlyWeekdays', !settingsStore.showOnlyWeekdays)} - active={settingsStore.showOnlyWeekdays} - title="Nur Wochentage anzeigen (Mo-Fr)" - > - Mo-Fr - - - - settingsStore.set('filterHoursEnabled', !settingsStore.filterHoursEnabled)} - labelFormat="range" - /> -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/DateStrip.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/DateStrip.svelte deleted file mode 100644 index e63e478bc..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/DateStrip.svelte +++ /dev/null @@ -1,654 +0,0 @@ - - -
- -
- -
- - {#if !isTodayVisible} - - {/if} - {visibleMonth} - -
- - -
- {#each days as day} - {@const dayIsToday = isToday(day)} - {@const dayIsSelected = isSameDay(day, currentDate)} - {@const dayIsWeekend = day.getDay() === 0 || day.getDay() === 6} - {@const dayInRange = isWithinInterval(day, { start: viewRange.start, end: viewRange.end })} - {@const dayIsRangeStart = isSameDay(day, viewRange.start)} - {@const dayIsRangeEnd = isSameDay(day, viewRange.end)} - {@const isFirstOfMonth = day.getDate() === 1} - {@const moonPhase = isSignificantMoonPhase(day)} - {@const eventCount = getEventCount(day)} - {#if isFirstOfMonth} -
- {/if} - - {/each} -
-
-
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/DateStripFab.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/DateStripFab.svelte deleted file mode 100644 index c23a3708e..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/DateStripFab.svelte +++ /dev/null @@ -1,110 +0,0 @@ - - -
- - -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/EventCard.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/EventCard.svelte deleted file mode 100644 index dec881105..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/EventCard.svelte +++ /dev/null @@ -1,268 +0,0 @@ - - -
- - {#if onResizeStart} -
- {/if} - - {formattedTime} - {event.title || (isDraft ? $_('calendar.draftEvent') : '')} - {#if event.location} - {event.location} - {/if} - - - {#if onResizeStart} -
- {/if} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/MiniCalendar.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/MiniCalendar.svelte deleted file mode 100644 index b593f4c9b..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/MiniCalendar.svelte +++ /dev/null @@ -1,162 +0,0 @@ - - -
-
- - {format(currentMonth, 'MMMM yyyy', { locale: de })} - -
- -
- {#each weekDays as day} - {day} - {/each} -
- -
- {#each calendarDays as day} - - {/each} -
-
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/MonthView.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/MonthView.svelte deleted file mode 100644 index 6a7323187..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/MonthView.svelte +++ /dev/null @@ -1,626 +0,0 @@ - - -
- -
- {#each weekDays as day} -
{day}
- {/each} -
- - -
- {#each weeks as week} -
- {#each week as day} - {@const isDropTarget = isDragging && dragTargetDay && isSameDay(day, dragTargetDay)} -
handleDayClick(day, e)} - onkeydown={(e) => e.key === 'Enter' && handleDayClick(day, e as unknown as MouseEvent)} - role="gridcell" - tabindex="0" - aria-selected={isToday(day)} - aria-label={$_('a11y.createEventOn', { - values: { date: format(day, 'EEEE, d. MMMM', { locale: de }) }, - })} - > -
- - {format(day, 'd')} - -
- -
- {#each getEventsForDay(day) as event} - {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} - {@const isDraft = eventsStore.isDraftEvent(event.id)} - {@const isSearchHighlighted = searchStore.isEventHighlighted(event.id)} - {@const isSearchDimmed = searchStore.isEventDimmed(event.id)} - -
startDrag(event, e)} - onclick={(e) => !isDraft && handleEventClick(event, e)} - role="button" - tabindex="0" - aria-label={event.title || - (isDraft ? $_('calendar.draftEvent') : $_('calendar.untitled'))} - > - {#if !event.isAllDay} - {format( - typeof event.startTime === 'string' - ? new Date(event.startTime) - : event.startTime, - 'HH:mm' - )}-{format( - typeof event.endTime === 'string' ? new Date(event.endTime) : event.endTime, - 'HH:mm' - )} - {/if} - {event.title || (isDraft ? $_('calendar.draftEvent') : '')} -
- {/each} - - - {#each getBirthdaysForDay(day) as birthday} - -
birthdayPopover.handleBirthdayClick(birthday, e)} - role="button" - tabindex="0" - aria-label="{birthday.displayName} - {$_( - 'views.birthday' - )}{settingsStore.showBirthdayAge && birthday.age > 0 ? ` (${birthday.age})` : ''}" - > - 🎂 - {birthday.displayName} - {#if settingsStore.showBirthdayAge && birthday.age > 0} - ({birthday.age}) - {/if} -
- {/each} - - {#if getAllEventsForDay(day).length > 3} - - {/if} -
-
- {/each} -
- {/each} -
-
- - -{#if birthdayPopover.selectedBirthday} - -{/if} - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/OverflowIndicator.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/OverflowIndicator.svelte deleted file mode 100644 index bbb58a343..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/OverflowIndicator.svelte +++ /dev/null @@ -1,57 +0,0 @@ - - -{#if events.length > 0} -
- {#each events as { color, tooltip }} -
- {/each} -
-{/if} - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/PillCalendarSelector.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/PillCalendarSelector.svelte deleted file mode 100644 index 365df859e..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/PillCalendarSelector.svelte +++ /dev/null @@ -1,409 +0,0 @@ - - -
- - - - {#if isOpen} - - - - - - {/if} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/TagStrip.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/TagStrip.svelte deleted file mode 100644 index b0b3ac5b1..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/TagStrip.svelte +++ /dev/null @@ -1,316 +0,0 @@ - - -
-
- - - - - - - {#if !hasTags} - - {:else} - {#each sortedTags as tag (tag.id)} - - {/each} - - - - {/if} -
-
- - - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/TagStripModal.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/TagStripModal.svelte deleted file mode 100644 index 03161cc26..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/TagStripModal.svelte +++ /dev/null @@ -1,804 +0,0 @@ - - - - -{#if visible} - - - - - - - -{/if} - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/TimeColumn.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/TimeColumn.svelte deleted file mode 100644 index 04e68b959..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/TimeColumn.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - -
- {#each hours as hour} -
- {formatHour(hour)} -
- {/each} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/UnifiedBar.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/UnifiedBar.svelte deleted file mode 100644 index 1ed956733..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/UnifiedBar.svelte +++ /dev/null @@ -1,568 +0,0 @@ - - - -
- - {#if showCalendarLayers && unifiedBarStore.showCalendarToolbar} - - {/if} - - - {#if showCalendarLayers && unifiedBarStore.showTagStrip} -
- ({ - id: t.id, - name: t.name, - color: t.color || '#3b82f6', - }))} - selectedIds={settingsStore.selectedTagIds} - onToggle={(tagId) => settingsStore.toggleTagSelection(tagId)} - onClear={() => settingsStore.clearTagSelection()} - managementHref="/tags" - /> -
- {/if} - - - {#if showCalendarLayers && unifiedBarStore.showDateStrip} -
- -
- {/if} - - -
- - - - {#if showCalendarLayers} - - {/if} -
-
- - -{#if unifiedBarStore.isOverlayOpen} - -
- - -
-{/if} - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/UnifiedBarControls.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/UnifiedBarControls.svelte deleted file mode 100644 index b4385be68..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/UnifiedBarControls.svelte +++ /dev/null @@ -1,422 +0,0 @@ - - - -
- -
- - - - - -
- - - {#if currentMode === 'expanded'} -
-
- - - - - - - -
- -
- - - - - -
-
- {/if} - - -
-
-
-
-
-
-
- - - {#if currentMode === 'collapsed'} - Zusammengklappt - {:else if currentMode === 'expanded'} - Erweitert - {:else if currentMode === 'overlay'} - Menü offen - {/if} - -
-
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewCarousel.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewCarousel.svelte deleted file mode 100644 index 52fab9d06..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewCarousel.svelte +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewModePill.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewModePill.svelte deleted file mode 100644 index 8f02a3153..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewModePill.svelte +++ /dev/null @@ -1,148 +0,0 @@ - - - -
- {#each enabledViews as view} - - {/each} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewsBar.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewsBar.svelte deleted file mode 100644 index 82dfc7180..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/ViewsBar.svelte +++ /dev/null @@ -1,162 +0,0 @@ - - - -
-
-
- -
- -
- {#each enabledViews as view} - - {/each} -
-
-
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/calendar/WeekView.svelte b/apps/calendar/apps/web-archived/src/lib/components/calendar/WeekView.svelte deleted file mode 100644 index 19d77cf5a..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/calendar/WeekView.svelte +++ /dev/null @@ -1,946 +0,0 @@ - - -
- - {#if settingsStore.showWeekNumbers} -
- {$_('views.weekNumber')} - {weekNumber} -
- {/if} - - - - - -
- -
- {#each hours as hour} -
- {settingsStore.formatHour(hour)} -
- {/each} -
- - -
- {#each days as day, dayIndex} - -
- {#each hours as hour} -
- {/each} - - - {#each getBlockAllDayEventsForDay(day) as event (event.id)} - - {/each} - - - {#each getEventsForDay(day) as event (event.id)} - {@const isBeingDragged = - eventDragDrop.isDragging && eventDragDrop.draggedEvent?.id === event.id} - {@const isBeingResized = - eventDragDrop.isResizing && eventDragDrop.resizeEvent?.id === event.id} - {@const isCrossDayDrag = - isBeingDragged && - eventDragDrop.dragTargetDay !== null && - !isSameDay(day, eventDragDrop.dragTargetDay)} - - {/each} - - - {#if eventDragDrop.isDragging && eventDragDrop.draggedEvent && eventDragDrop.dragTargetDay && isSameDay(day, eventDragDrop.dragTargetDay) && !getEventsForDay(day).some((e) => e.id === eventDragDrop.draggedEvent!.id)} - - {/if} - - - {#if dragToCreate.isCreating && dragToCreate.createTargetDay && isSameDay(day, dragToCreate.createTargetDay)} -
- {dragToCreate.getCreatePreviewTime()} - (Neuer Termin) -
- {/if} - - - {#if true} - {@const overflow = getOverflowEventsForDay(day)} - {#if overflow.before.length > 0} -
- {#each overflow.before as event} -
- {/each} -
- {/if} - {#if overflow.after.length > 0} -
- {#each overflow.after as event} -
- {/each} -
- {/if} - {/if} - - - {#if isToday(day)} -
- {settingsStore.formatTime(timeIndicator.now)} -
- {/if} -
- {/each} -
-
-
- - -{#if birthdayPopover.selectedBirthday} - -{/if} - - { - contextMenuVisible = false; - contextMenuEvent = null; - }} -/> - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/event/AttendeeSelector.svelte b/apps/calendar/apps/web-archived/src/lib/components/event/AttendeeSelector.svelte deleted file mode 100644 index ab0e94869..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/event/AttendeeSelector.svelte +++ /dev/null @@ -1,266 +0,0 @@ - - -
- - {#if attendees.length > 0} -
- {#each attendees as attendee (attendee.email)} -
- - -
-
- {attendee.name || attendee.email} -
- {#if attendee.name && attendee.email} -
- {attendee.email} -
- {/if} - {#if attendee.company} -
- {attendee.company} -
- {/if} -
- - -
- - - {#if showStatusDropdown === attendee.email} -
- {#each statusOptions as option (option.value)} - - {/each} -
- {/if} -
- - - -
- {/each} -
- {/if} - - - -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/event/EventDetailModal.svelte b/apps/calendar/apps/web-archived/src/lib/components/event/EventDetailModal.svelte deleted file mode 100644 index 42f744c31..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/event/EventDetailModal.svelte +++ /dev/null @@ -1,695 +0,0 @@ - - - - - (showRecurrenceDialog = false)} -/> - - - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/event/EventForm.svelte b/apps/calendar/apps/web-archived/src/lib/components/event/EventForm.svelte deleted file mode 100644 index 25f494e50..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/event/EventForm.svelte +++ /dev/null @@ -1,512 +0,0 @@ - - -
-
- - -
- -
- Kalender - {#if calendarsCtx.value.length > 0} - (calendarId = typeof v === 'string' ? v : '')} - placeholder="Kalender wählen" - /> - {:else} -

Standardkalender wird automatisch erstellt

- {/if} -
- -
- -
- - {#if isAllDay} -
- Anzeigeart - (allDayDisplayMode = (v as 'default' | 'header' | 'block') || 'default')} - placeholder="Anzeigeart wählen" - /> -
- {/if} - -
-
- - -
- {#if !isAllDay} -
- - -
- {/if} -
- -
-
- - -
- {#if !isAllDay} -
- - -
- {/if} -
- - - { - recurrenceRule = rule; - recurrenceEndDate = endDt; - }} - /> - - - (reminderDrafts = drafts)} - /> - -
- - - - - - - - {#if showLocationDetails} -
-
- - -
- -
-
- - -
-
- - -
-
- -
- - -
-
- {/if} -
- -
- - -
- - - {#if availableTags.length > 0} -
- Tags - -
- {/if} - - -
- Verantwortliche Person - (responsiblePerson = person)} - /> -
- - -
- Teilnehmer - (attendees = newAttendees)} - /> -
- -
- - -
- diff --git a/apps/calendar/apps/web-archived/src/lib/components/event/QuickEventOverlay.svelte b/apps/calendar/apps/web-archived/src/lib/components/event/QuickEventOverlay.svelte deleted file mode 100644 index a6dd0c635..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/event/QuickEventOverlay.svelte +++ /dev/null @@ -1,1816 +0,0 @@ - - - - - (showRecurrenceDialog = false)} -/> - - - - - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/event/RecurrenceEditDialog.svelte b/apps/calendar/apps/web-archived/src/lib/components/event/RecurrenceEditDialog.svelte deleted file mode 100644 index 531aec7f7..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/event/RecurrenceEditDialog.svelte +++ /dev/null @@ -1,109 +0,0 @@ - - - -
- - - - - -
- - {#snippet footer()} - - {/snippet} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/event/RecurrenceSelector.svelte b/apps/calendar/apps/web-archived/src/lib/components/event/RecurrenceSelector.svelte deleted file mode 100644 index 17d8595ef..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/event/RecurrenceSelector.svelte +++ /dev/null @@ -1,306 +0,0 @@ - - -
-
- - Wiederholung -
- - - - {#if showCustom} -
-
- - - -
- - {#if pattern.frequency === 'WEEKLY'} -
- -
- {#each weekdays as day} - - {/each} -
-
- {/if} - -
- - -
-
- {/if} - - {#if recurrenceRule} -

{currentDescription}

- {/if} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/event/ReminderSelector.svelte b/apps/calendar/apps/web-archived/src/lib/components/event/ReminderSelector.svelte deleted file mode 100644 index 6c6243bb3..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/event/ReminderSelector.svelte +++ /dev/null @@ -1,383 +0,0 @@ - - -
-
- - Erinnerungen -
- - {#if isSavedEvent} - - {#if displayReminders.length === 0} -

Keine Erinnerungen

- {/if} - - {#each displayReminders as reminder (reminder.id)} -
-
- {getLabel(reminder.minutesBefore)} -
- {#if reminder.notifyPush} - - - - {/if} - {#if reminder.notifyEmail} - - - - {/if} -
- {#if reminder.status === 'sent'} - Gesendet - {:else if reminder.status === 'failed'} - Fehlgeschlagen - {/if} -
- -
- {/each} - - -
- -
- {:else} - - {#each drafts as draft, index} -
-
- - - -
- -
- {/each} - - - {/if} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/event/ResponsiblePersonSelector.svelte b/apps/calendar/apps/web-archived/src/lib/components/event/ResponsiblePersonSelector.svelte deleted file mode 100644 index 14f3b12b6..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/event/ResponsiblePersonSelector.svelte +++ /dev/null @@ -1,209 +0,0 @@ - - -
- {#if responsiblePerson} - -
- - -
-
- {responsiblePerson.name || responsiblePerson.email} -
- {#if responsiblePerson.name && responsiblePerson.email} -
- {responsiblePerson.email} -
- {/if} - {#if responsiblePerson.company} -
- {responsiblePerson.company} -
- {/if} -
- - - {#if responsiblePerson.contactId} - - {/if} - - - -
- {:else if showSelector} - - - - {:else} - - - {/if} -
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/settings/CalendarManagement.svelte b/apps/calendar/apps/web-archived/src/lib/components/settings/CalendarManagement.svelte deleted file mode 100644 index ed352d35a..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/settings/CalendarManagement.svelte +++ /dev/null @@ -1,501 +0,0 @@ - - -
-
- -
- - {#if showNewCalendarForm} -
-
{ - e.preventDefault(); - handleCreateCalendar(); - }} - > -
- - -
-
- - -
-
-
- {/if} - -
- {#each calendars as calendar} - {#if editingCalendar?.id === calendar.id} -
-
{ - e.preventDefault(); - handleUpdateCalendar(); - }} - > -
-
- - -
- -
- -
- - {editColor} -
-
-
- - - -
- - -
-
-
- {:else} -
-
- - {calendar.name} - {#if calendar.isDefault} - {$_('settings.default')} - {/if} -
-
- - {#if !calendar.isDefault} - - {/if} -
-
- {/if} - {/each} - - {#if calendars.length === 0} -
-

{$_('settings.noCalendars')}

-
- {/if} -
-
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/settings/SettingsModal.svelte b/apps/calendar/apps/web-archived/src/lib/components/settings/SettingsModal.svelte deleted file mode 100644 index 8990caeb9..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/settings/SettingsModal.svelte +++ /dev/null @@ -1,705 +0,0 @@ - - - - -{#if visible} - - - - - - - -{/if} - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/skeletons/AgendaSkeleton.svelte b/apps/calendar/apps/web-archived/src/lib/components/skeletons/AgendaSkeleton.svelte deleted file mode 100644 index ca4b77da6..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/skeletons/AgendaSkeleton.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - -
- - {#each Array(4) as _, groupIndex} -
- -
- -
- - - {#each Array(groupIndex === 0 ? 3 : 2) as _, eventIndex} -
- - - - -
- - - - - - {#if eventIndex === 0} - - {/if} -
-
- {/each} -
- {/each} -
diff --git a/apps/calendar/apps/web-archived/src/lib/components/skeletons/AppLoadingSkeleton.svelte b/apps/calendar/apps/web-archived/src/lib/components/skeletons/AppLoadingSkeleton.svelte deleted file mode 100644 index a46eb0b57..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/skeletons/AppLoadingSkeleton.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - -
-
- -
- - -
- - -
- -
- -
- - -
-
- - -
- {#each Array(7) as _} - - {/each} -
- - -
- {#each Array(35) as _, i} -
- -
- {/each} -
-
- - -
- -
-
-
diff --git a/apps/calendar/apps/web-archived/src/lib/components/skeletons/CalendarViewSkeleton.svelte b/apps/calendar/apps/web-archived/src/lib/components/skeletons/CalendarViewSkeleton.svelte deleted file mode 100644 index d48b95c7f..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/skeletons/CalendarViewSkeleton.svelte +++ /dev/null @@ -1,149 +0,0 @@ - - -
- -
- -
- - - {#each days as _, dayIndex} -
- - -
- {/each} -
- - -
- -
- {#each hours as _, hourIndex} -
- -
- {/each} -
- - - {#each days as _, dayIndex} -
- {#each hours as _, hourIndex} -
- {/each} - - - {#if dayIndex === 1} -
- -
- {/if} - {#if dayIndex === 2} -
- -
- {/if} - {#if dayIndex === 3} -
- -
- {/if} - {#if dayIndex === 4} -
- -
-
- -
- {/if} - {#if dayIndex === 5} -
- -
- {/if} -
- {/each} -
-
- - diff --git a/apps/calendar/apps/web-archived/src/lib/components/skeletons/EventDetailSkeleton.svelte b/apps/calendar/apps/web-archived/src/lib/components/skeletons/EventDetailSkeleton.svelte deleted file mode 100644 index c0a68389a..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/skeletons/EventDetailSkeleton.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - -
- -
- -
- - -
-
- - -
- -
- - -
-
- - -
- -
- - -
-
- - -
- -
- - -
-
- - -
- -
- -
- - - -
-
-
- - -
- -
- -
- {#each Array(3) as _, i} -
- - -
- {/each} -
-
-
-
diff --git a/apps/calendar/apps/web-archived/src/lib/components/skeletons/RedirectSkeleton.svelte b/apps/calendar/apps/web-archived/src/lib/components/skeletons/RedirectSkeleton.svelte deleted file mode 100644 index db8130ed8..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/skeletons/RedirectSkeleton.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
- - -
diff --git a/apps/calendar/apps/web-archived/src/lib/components/skeletons/index.ts b/apps/calendar/apps/web-archived/src/lib/components/skeletons/index.ts deleted file mode 100644 index 032422277..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/skeletons/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Calendar App Skeleton Components - * - * App-specific skeleton loaders that match the exact layout of calendar components. - * Built on top of @manacore/shared-ui skeleton primitives. - */ - -// App Loading Skeleton -export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte'; - -// Agenda Skeleton -export { default as AgendaSkeleton } from './AgendaSkeleton.svelte'; - -// Event Detail Skeleton -export { default as EventDetailSkeleton } from './EventDetailSkeleton.svelte'; - -// Redirect Skeleton -export { default as RedirectSkeleton } from './RedirectSkeleton.svelte'; - -// Calendar View Skeleton -export { default as CalendarViewSkeleton } from './CalendarViewSkeleton.svelte'; diff --git a/apps/calendar/apps/web-archived/src/lib/components/tags/index.ts b/apps/calendar/apps/web-archived/src/lib/components/tags/index.ts deleted file mode 100644 index 8e217ea36..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/tags/index.ts +++ /dev/null @@ -1 +0,0 @@ -// Tag components (tag groups removed - flat tag list only) diff --git a/apps/calendar/apps/web-archived/src/lib/components/voice/VoiceRecordButton.svelte b/apps/calendar/apps/web-archived/src/lib/components/voice/VoiceRecordButton.svelte deleted file mode 100644 index 9253b1870..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/voice/VoiceRecordButton.svelte +++ /dev/null @@ -1,223 +0,0 @@ - - -{#if isSupported} - - - - {#if hasError && errorMessage} - - {/if} -{/if} - - diff --git a/apps/calendar/apps/web-archived/src/lib/components/voice/VoiceRecordingModal.svelte b/apps/calendar/apps/web-archived/src/lib/components/voice/VoiceRecordingModal.svelte deleted file mode 100644 index 6bdb147e4..000000000 --- a/apps/calendar/apps/web-archived/src/lib/components/voice/VoiceRecordingModal.svelte +++ /dev/null @@ -1,345 +0,0 @@ - - - - -{#if isVisible} - -
- - - -{/if} - - diff --git a/apps/calendar/apps/web-archived/src/lib/composables/index.ts b/apps/calendar/apps/web-archived/src/lib/composables/index.ts deleted file mode 100644 index c03549eec..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Calendar Composables - * Reusable logic extracted from components - */ - -// Visible hours and time indicator -export { useVisibleHours, useCurrentTimeIndicator } from './useVisibleHours.svelte'; - -// Event drag/drop and resize (comprehensive composable) -export { - useEventDragDrop, - type EventDragDropConfig, - type EventDragState, - type EventResizeState, -} from './useEventDragDrop.svelte'; - -// Drag-to-create -export { useDragToCreate, type DragToCreateConfig } from './useDragToCreate.svelte'; - -// Keyboard handling -export { useCalendarKeyboard, type CancellableOperation } from './useCalendarKeyboard.svelte'; - -// Swipe navigation (carousel gesture handling) -export { useSwipeNavigation, type SwipeNavigationConfig } from './useSwipeNavigation.svelte'; - -// Birthday popover management -export { useBirthdayPopover } from './useBirthdayPopover.svelte'; diff --git a/apps/calendar/apps/web-archived/src/lib/composables/useBirthdayPopover.svelte.ts b/apps/calendar/apps/web-archived/src/lib/composables/useBirthdayPopover.svelte.ts deleted file mode 100644 index 400e1b6ef..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/useBirthdayPopover.svelte.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Birthday Popover Composable - * Manages birthday popover state and handlers for calendar views - */ - -import type { BirthdayEvent } from '$lib/api/birthdays'; - -export function useBirthdayPopover() { - let selectedBirthday = $state(null); - let popoverPosition = $state<{ x: number; y: number }>({ x: 0, y: 0 }); - - /** - * Handle click on a birthday indicator to show the popover - */ - function handleBirthdayClick(birthday: BirthdayEvent, e: MouseEvent) { - e.stopPropagation(); - selectedBirthday = birthday; - popoverPosition = { x: e.clientX, y: e.clientY }; - } - - /** - * Close the birthday popover - */ - function closePopover() { - selectedBirthday = null; - } - - /** - * Check if popover is currently open - */ - function isOpen(): boolean { - return selectedBirthday !== null; - } - - return { - // State (reactive getters) - get selectedBirthday() { - return selectedBirthday; - }, - get popoverPosition() { - return popoverPosition; - }, - - // Methods - handleBirthdayClick, - closePopover, - isOpen, - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/composables/useCalendarKeyboard.svelte.ts b/apps/calendar/apps/web-archived/src/lib/composables/useCalendarKeyboard.svelte.ts deleted file mode 100644 index 3c229551d..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/useCalendarKeyboard.svelte.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Calendar Keyboard Handling Composable - * Handles keyboard shortcuts for calendar views (e.g., Escape to cancel drag/resize) - */ - -export interface CancellableOperation { - /** Check if operation is active */ - isActive: () => boolean; - /** Cancel the operation */ - cancel: () => void; -} - -/** - * Creates a keyboard handler that cancels operations on Escape key - * Automatically sets up and cleans up the event listener via $effect - * - * @param operations - Array of operations that can be cancelled (e.g., drag/drop, resize) - */ -export function useCalendarKeyboard(operations: CancellableOperation[]) { - function handleKeyDown(e: KeyboardEvent) { - if (e.key === 'Escape') { - // Check if any operation is active - const activeOperation = operations.find((op) => op.isActive()); - if (activeOperation) { - e.preventDefault(); - activeOperation.cancel(); - } - } - } - - // Setup listener - call this in $effect - function setup() { - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - } - - return { - setup, - handleKeyDown, - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/composables/useDragToCreate.svelte.ts b/apps/calendar/apps/web-archived/src/lib/composables/useDragToCreate.svelte.ts deleted file mode 100644 index df55f23a4..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/useDragToCreate.svelte.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Drag-to-Create Composable - * Handles click-and-drag on the calendar grid to create new events - */ - -import { DEFAULT_EVENT_DURATION_MINUTES } from '$lib/utils/calendarConstants'; -import { formatTime, getSnapMinutes, getDayFromX, getMinutesFromY } from '$lib/utils/drag-helpers'; - -export interface DragToCreateConfig { - containerEl: HTMLElement | null; - days: Date[]; - firstVisibleHour: number; - lastVisibleHour: number; - totalVisibleHours: number; - hourHeight: number; - minutesToPercent: (minutes: number) => number; - snapMinutes?: number; - isOtherOperationActive: () => boolean; - onCreateEnd?: (startTime: Date, endTime: Date, position: { x: number; y: number }) => void; -} - -export function useDragToCreate(getConfig: () => DragToCreateConfig) { - let isCreating = $state(false); - let createTargetDay = $state(null); - let createStartMinutes = $state(0); - let createEndMinutes = $state(0); - let createPreviewTop = $state(0); - let createPreviewHeight = $state(0); - let hasMoved = $state(false); - - function dayFromX(clientX: number): Date | null { - const config = getConfig(); - return getDayFromX(clientX, config.containerEl, config.days); - } - - function minutesFromY(clientY: number): number { - const config = getConfig(); - return getMinutesFromY( - clientY, - config.containerEl, - config.totalVisibleHours, - config.hourHeight, - config.firstVisibleHour, - config.snapMinutes - ); - } - - function updatePreview() { - const config = getConfig(); - createPreviewTop = config.minutesToPercent(createStartMinutes); - const duration = createEndMinutes - createStartMinutes; - createPreviewHeight = (duration / (config.totalVisibleHours * 60)) * 100; - } - - function startCreate(e: PointerEvent) { - const config = getConfig(); - if (config.isOtherOperationActive()) return; - - // Don't start creating if clicking on interactive elements - const target = e.target as HTMLElement; - if ( - target.closest( - '.event-card, .task-block, .all-day-event, .all-day-block-event, .overflow-indicator, .resize-handle' - ) - ) { - return; - } - - e.preventDefault(); - - const day = dayFromX(e.clientX); - if (!day) return; - - const minutes = minutesFromY(e.clientY); - const snap = getSnapMinutes(config.snapMinutes); - const snappedMinutes = Math.round(minutes / snap) * snap; - - isCreating = true; - hasMoved = false; - createTargetDay = day; - createStartMinutes = snappedMinutes; - createEndMinutes = snappedMinutes + DEFAULT_EVENT_DURATION_MINUTES; - - updatePreview(); - - document.addEventListener('pointermove', handleCreateMove); - document.addEventListener('pointerup', handleCreateEnd); - } - - function handleCreateMove(e: PointerEvent) { - if (!isCreating) return; - - hasMoved = true; - const config = getConfig(); - const snap = getSnapMinutes(config.snapMinutes); - - const day = dayFromX(e.clientX); - if (day) createTargetDay = day; - - const minutes = minutesFromY(e.clientY); - const snappedMinutes = Math.round(minutes / snap) * snap; - - if (snappedMinutes >= createStartMinutes) { - createEndMinutes = Math.max(snappedMinutes, createStartMinutes + snap); - } else { - createEndMinutes = createStartMinutes + snap; - createStartMinutes = snappedMinutes; - } - - createStartMinutes = Math.max(config.firstVisibleHour * 60, createStartMinutes); - createEndMinutes = Math.min(config.lastVisibleHour * 60, createEndMinutes); - - updatePreview(); - } - - function handleCreateEnd(e: PointerEvent) { - document.removeEventListener('pointermove', handleCreateMove); - document.removeEventListener('pointerup', handleCreateEnd); - - if (!isCreating || !createTargetDay) { - isCreating = false; - return; - } - - const startTime = new Date(createTargetDay); - startTime.setHours(Math.floor(createStartMinutes / 60), createStartMinutes % 60, 0, 0); - - const endTime = new Date(createTargetDay); - endTime.setHours(Math.floor(createEndMinutes / 60), createEndMinutes % 60, 0, 0); - - isCreating = false; - createTargetDay = null; - hasMoved = false; - - const config = getConfig(); - config.onCreateEnd?.(startTime, endTime, { x: e.clientX, y: e.clientY }); - } - - function getCreatePreviewTime(): string { - return `${formatTime(Math.floor(createStartMinutes / 60), createStartMinutes % 60)} - ${formatTime(Math.floor(createEndMinutes / 60), createEndMinutes % 60)}`; - } - - function cancel() { - if (isCreating) { - document.removeEventListener('pointermove', handleCreateMove); - document.removeEventListener('pointerup', handleCreateEnd); - isCreating = false; - createTargetDay = null; - hasMoved = false; - } - } - - return { - get isCreating() { - return isCreating; - }, - get createTargetDay() { - return createTargetDay; - }, - get createPreviewTop() { - return createPreviewTop; - }, - get createPreviewHeight() { - return createPreviewHeight; - }, - - startCreate, - cancel, - getCreatePreviewTime, - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/composables/useDragToCreate.test.ts b/apps/calendar/apps/web-archived/src/lib/composables/useDragToCreate.test.ts deleted file mode 100644 index c3176825c..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/useDragToCreate.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -// Polyfill PointerEvent for jsdom -if (typeof globalThis.PointerEvent === 'undefined') { - globalThis.PointerEvent = class PointerEvent extends MouseEvent { - constructor(type: string, params: PointerEventInit = {}) { - super(type, params); - } - } as unknown as typeof PointerEvent; -} - -vi.mock('$lib/utils/calendarConstants', () => ({ - SNAP_INTERVAL_MINUTES: 15, - DEFAULT_EVENT_DURATION_MINUTES: 30, -})); - -import { useDragToCreate } from './useDragToCreate.svelte'; - -function createMockContainer() { - return { - getBoundingClientRect: () => ({ - left: 0, - top: 0, - right: 700, - bottom: 960, - width: 700, - height: 960, - }), - parentElement: { scrollTop: 0 }, - } as unknown as HTMLElement; -} - -function makeDays(): Date[] { - return Array.from({ length: 7 }, (_, i) => { - const d = new Date('2026-03-02'); - d.setDate(d.getDate() + i); - return d; - }); -} - -function minutesToPercent(minutes: number): number { - const firstHour = 0; - const totalHours = 24; - const adjusted = minutes - firstHour * 60; - return (adjusted / (totalHours * 60)) * 100; -} - -describe('useDragToCreate', () => { - let onCreateEnd: ReturnType; - - function createInstance(overrides = {}) { - const container = createMockContainer(); - onCreateEnd = vi.fn(); - return useDragToCreate(() => ({ - containerEl: container, - days: makeDays(), - firstVisibleHour: 0, - lastVisibleHour: 24, - totalVisibleHours: 24, - hourHeight: 40, - minutesToPercent, - isOtherOperationActive: () => false, - onCreateEnd, - ...overrides, - })); - } - - it('should start in idle state', () => { - const dtc = createInstance(); - expect(dtc.isCreating).toBe(false); - expect(dtc.createTargetDay).toBeNull(); - }); - - it('should not start create if other operation is active', () => { - const dtc = createInstance({ isOtherOperationActive: () => true }); - const target = document.createElement('div'); - const event = new PointerEvent('pointerdown', { clientX: 100, clientY: 200 }); - Object.defineProperty(event, 'target', { value: target }); - dtc.startCreate(event); - expect(dtc.isCreating).toBe(false); - }); - - it('should cancel on cancel()', () => { - const dtc = createInstance(); - const target = document.createElement('div'); - const event = new PointerEvent('pointerdown', { clientX: 100, clientY: 200 }); - Object.defineProperty(event, 'target', { value: target }); - dtc.startCreate(event); - dtc.cancel(); - expect(dtc.isCreating).toBe(false); - expect(dtc.createTargetDay).toBeNull(); - }); - - it('should generate correct preview time format', () => { - const dtc = createInstance(); - // getCreatePreviewTime uses internal state, returns HH:MM - HH:MM format - const time = dtc.getCreatePreviewTime(); - expect(time).toMatch(/^\d{2}:\d{2} - \d{2}:\d{2}$/); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/composables/useEventDragDrop.svelte.ts b/apps/calendar/apps/web-archived/src/lib/composables/useEventDragDrop.svelte.ts deleted file mode 100644 index e31c2f93f..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/useEventDragDrop.svelte.ts +++ /dev/null @@ -1,427 +0,0 @@ -/** - * Event Drag & Drop + Resize Composable - * Extracts duplicated drag/resize logic from WeekView, DayView, MultiDayView - */ - -import type { CalendarEvent } from '@calendar/shared'; -import { differenceInMinutes, addMinutes, setHours, setMinutes } from 'date-fns'; -import { toDate } from '$lib/utils/eventDateHelpers'; -import { eventsStore } from '$lib/stores/events.svelte'; -import { formatTime, getDayFromX, getMinutesFromY } from '$lib/utils/drag-helpers'; - -export interface EventDragDropConfig { - /** Reference to the container element for position calculations */ - containerEl: HTMLElement | null; - /** Array of visible days (for multi-day views) or single day (for day view) */ - days: Date[]; - /** First visible hour (for filtered hours mode) */ - firstVisibleHour: number; - /** Last visible hour (for filtered hours mode) */ - lastVisibleHour: number; - /** Total visible hours */ - totalVisibleHours: number; - /** Height of one hour in pixels */ - hourHeight: number; - /** Minutes per snap interval (default: 15) */ - snapMinutes?: number; - /** Function to convert minutes to percentage position */ - minutesToPercent: (minutes: number) => number; -} - -export interface EventDragState { - isDragging: boolean; - draggedEvent: CalendarEvent | null; - dragTargetDay: Date | null; - dragPreviewTop: number; - dragPreviewHeight: number; - hasMoved: boolean; -} - -export interface EventResizeState { - isResizing: boolean; - resizeEvent: CalendarEvent | null; - resizeEdge: 'top' | 'bottom'; - resizePreviewTop: number; - resizePreviewHeight: number; -} - -export function useEventDragDrop(getConfig: () => EventDragDropConfig) { - // ========== Drag State ========== - let isDragging = $state(false); - let draggedEvent = $state(null); - let dragOffsetMinutes = $state(0); - let dragTargetDay = $state(null); - let dragPreviewTop = $state(0); - let dragPreviewHeight = $state(0); - - // ========== Resize State ========== - let isResizing = $state(false); - let resizeEvent = $state(null); - let resizeEdge = $state<'top' | 'bottom'>('bottom'); - let resizeOriginalStart = $state(null); - let resizeOriginalEnd = $state(null); - let resizePreviewTop = $state(0); - let resizePreviewHeight = $state(0); - let resizeOffsetMinutes = $state(0); - - // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) - let hasMoved = $state(false); - - // ========== Helper Functions ========== - - function dayFromX(clientX: number): Date | null { - const config = getConfig(); - return getDayFromX(clientX, config.containerEl, config.days); - } - - function minutesFromY(clientY: number): number { - const config = getConfig(); - return getMinutesFromY( - clientY, - config.containerEl, - config.totalVisibleHours, - config.hourHeight, - config.firstVisibleHour, - config.snapMinutes - ); - } - - // ========== Drag Functions ========== - - function startDrag(event: CalendarEvent, e: PointerEvent) { - e.preventDefault(); - e.stopPropagation(); - - const config = getConfig(); - - isDragging = true; - draggedEvent = event; - hasMoved = false; - - const start = toDate(event.startTime); - const end = toDate(event.endTime); - const duration = differenceInMinutes(end, start); - - // Calculate initial preview position - const startMinutes = start.getHours() * 60 + start.getMinutes(); - dragPreviewTop = config.minutesToPercent(startMinutes); - dragPreviewHeight = (duration / (config.totalVisibleHours * 60)) * 100; - dragTargetDay = start; - - // Calculate offset from event start to click position - const clickMinutes = minutesFromY(e.clientY); - dragOffsetMinutes = clickMinutes - startMinutes; - - document.addEventListener('pointermove', handleDragMove); - document.addEventListener('pointerup', handleDragEnd); - } - - function handleDragMove(e: PointerEvent) { - if (!isDragging || !draggedEvent) return; - - const config = getConfig(); - hasMoved = true; - - // Calculate new position - const newDay = dayFromX(e.clientX); - const newMinutes = minutesFromY(e.clientY) - dragOffsetMinutes; - - // Clamp to valid range - const clampedMinutes = Math.max( - config.firstVisibleHour * 60, - Math.min(config.lastVisibleHour * 60 - 15, newMinutes) - ); - - // Update preview - dragPreviewTop = config.minutesToPercent(clampedMinutes); - if (newDay) { - dragTargetDay = newDay; - } - } - - async function handleDragEnd(e: PointerEvent) { - document.removeEventListener('pointermove', handleDragMove); - document.removeEventListener('pointerup', handleDragEnd); - - if (!isDragging || !draggedEvent || !dragTargetDay || !hasMoved) { - cleanupDrag(); - return; - } - - const start = toDate(draggedEvent.startTime); - const end = toDate(draggedEvent.endTime); - const duration = differenceInMinutes(end, start); - - // Calculate new start time - const newMinutes = minutesFromY(e.clientY) - dragOffsetMinutes; - const clampedMinutes = Math.max(0, Math.min(24 * 60 - 15, newMinutes)); - const newHours = Math.floor(clampedMinutes / 60); - const newMins = clampedMinutes % 60; - - let newStart = new Date(dragTargetDay); - newStart = setHours(newStart, newHours); - newStart = setMinutes(newStart, newMins); - - const newEnd = addMinutes(newStart, duration); - - // Update event via store - if (eventsStore.isDraftEvent(draggedEvent.id)) { - eventsStore.updateDraftEvent({ - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); - } else { - await eventsStore.updateEvent(draggedEvent.id, { - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); - } - - cleanupDrag(); - } - - function cleanupDrag() { - isDragging = false; - draggedEvent = null; - dragTargetDay = null; - hasMoved = false; - } - - // ========== Resize Functions ========== - - function startResize(event: CalendarEvent, edge: 'top' | 'bottom', e: PointerEvent) { - e.preventDefault(); - e.stopPropagation(); - - const config = getConfig(); - - isResizing = true; - resizeEvent = event; - resizeEdge = edge; - hasMoved = false; - - const start = toDate(event.startTime); - const end = toDate(event.endTime); - - resizeOriginalStart = start; - resizeOriginalEnd = end; - - // Set initial preview - const startMinutes = start.getHours() * 60 + start.getMinutes(); - const endMinutes = end.getHours() * 60 + end.getMinutes(); - const duration = differenceInMinutes(end, start); - resizePreviewTop = config.minutesToPercent(startMinutes); - resizePreviewHeight = (duration / (config.totalVisibleHours * 60)) * 100; - - // Calculate offset between snapped click position and actual event boundary - const clickMinutes = minutesFromY(e.clientY); - if (edge === 'top') { - resizeOffsetMinutes = clickMinutes - startMinutes; - } else { - resizeOffsetMinutes = clickMinutes - endMinutes; - } - - document.addEventListener('pointermove', handleResizeMove); - document.addEventListener('pointerup', handleResizeEnd); - } - - function handleResizeMove(e: PointerEvent) { - if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) return; - - const config = getConfig(); - hasMoved = true; - - const currentMinutes = minutesFromY(e.clientY); - // Apply offset to prevent jumping when drag starts - const adjustedMinutes = currentMinutes - resizeOffsetMinutes; - const originalStartMinutes = - resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); - const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); - - if (resizeEdge === 'bottom') { - // Resize from bottom - change end time - const newEndMinutes = Math.max( - originalStartMinutes + 15, - Math.min(config.lastVisibleHour * 60, adjustedMinutes) - ); - const newDuration = newEndMinutes - originalStartMinutes; - resizePreviewHeight = (newDuration / (config.totalVisibleHours * 60)) * 100; - } else { - // Resize from top - change start time - const newStartMinutes = Math.max( - config.firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, adjustedMinutes) - ); - const newDuration = originalEndMinutes - newStartMinutes; - resizePreviewTop = config.minutesToPercent(newStartMinutes); - resizePreviewHeight = (newDuration / (config.totalVisibleHours * 60)) * 100; - } - } - - async function handleResizeEnd(e: PointerEvent) { - document.removeEventListener('pointermove', handleResizeMove); - document.removeEventListener('pointerup', handleResizeEnd); - - if (!isResizing || !resizeEvent || !resizeOriginalStart || !resizeOriginalEnd || !hasMoved) { - cleanupResize(); - return; - } - - const config = getConfig(); - const currentMinutes = minutesFromY(e.clientY); - // Apply offset to prevent jumping - const adjustedMinutes = currentMinutes - resizeOffsetMinutes; - const originalStartMinutes = - resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); - const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); - - let newStart = resizeOriginalStart; - let newEnd = resizeOriginalEnd; - - if (resizeEdge === 'bottom') { - const newEndMinutes = Math.max( - originalStartMinutes + 15, - Math.min(config.lastVisibleHour * 60, adjustedMinutes) - ); - const newHours = Math.floor(newEndMinutes / 60); - const newMins = newEndMinutes % 60; - newEnd = setHours(new Date(resizeOriginalEnd), newHours); - newEnd = setMinutes(newEnd, newMins); - } else { - const newStartMinutes = Math.max( - config.firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, adjustedMinutes) - ); - const newHours = Math.floor(newStartMinutes / 60); - const newMins = newStartMinutes % 60; - newStart = setHours(new Date(resizeOriginalStart), newHours); - newStart = setMinutes(newStart, newMins); - } - - // Update event via store - if (eventsStore.isDraftEvent(resizeEvent.id)) { - eventsStore.updateDraftEvent({ - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); - } else { - await eventsStore.updateEvent(resizeEvent.id, { - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); - } - - cleanupResize(); - } - - function cleanupResize() { - isResizing = false; - resizeEvent = null; - resizeOriginalStart = null; - resizeOriginalEnd = null; - resizeOffsetMinutes = 0; - hasMoved = false; - } - - // ========== Combined Cleanup ========== - - function cleanup() { - document.removeEventListener('pointermove', handleDragMove); - document.removeEventListener('pointerup', handleDragEnd); - document.removeEventListener('pointermove', handleResizeMove); - document.removeEventListener('pointerup', handleResizeEnd); - cleanupDrag(); - cleanupResize(); - } - - /** - * Cancel any active drag/resize operation (e.g., on Escape key) - */ - function cancel() { - if (isDragging || isResizing) { - cleanup(); - } - } - - /** - * Get formatted time range during resize preview - */ - function getResizePreviewTime(): string { - if (!resizeEvent || !resizeOriginalStart || !resizeOriginalEnd) return ''; - - const config = getConfig(); - const origStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); - const origEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); - - const previewStartMinutes = - (resizePreviewTop / 100) * config.totalVisibleHours * 60 + config.firstVisibleHour * 60; - const previewEndMinutes = - previewStartMinutes + (resizePreviewHeight / 100) * config.totalVisibleHours * 60; - - let startMin: number; - let endMin: number; - - if (resizeEdge === 'top') { - startMin = Math.round(previewStartMinutes); - endMin = origEndMinutes; - } else { - startMin = origStartMinutes; - endMin = Math.round(previewEndMinutes); - } - - return `${formatTime(Math.floor(startMin / 60), startMin % 60)} - ${formatTime(Math.floor(endMin / 60), endMin % 60)}`; - } - - return { - // Drag state (reactive getters) - get isDragging() { - return isDragging; - }, - get draggedEvent() { - return draggedEvent; - }, - get dragTargetDay() { - return dragTargetDay; - }, - get dragPreviewTop() { - return dragPreviewTop; - }, - get dragPreviewHeight() { - return dragPreviewHeight; - }, - - // Resize state (reactive getters) - get isResizing() { - return isResizing; - }, - get resizeEvent() { - return resizeEvent; - }, - get resizeEdge() { - return resizeEdge; - }, - get resizePreviewTop() { - return resizePreviewTop; - }, - get resizePreviewHeight() { - return resizePreviewHeight; - }, - - // Shared state - get hasMoved() { - return hasMoved; - }, - - // Reset hasMoved after click handling - resetHasMoved() { - hasMoved = false; - }, - - // Methods - startDrag, - startResize, - cancel, - cleanup, - getResizePreviewTime, - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/composables/useEventDragDrop.test.ts b/apps/calendar/apps/web-archived/src/lib/composables/useEventDragDrop.test.ts deleted file mode 100644 index 5cdf8bed2..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/useEventDragDrop.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -// Polyfill PointerEvent for jsdom -if (typeof globalThis.PointerEvent === 'undefined') { - globalThis.PointerEvent = class PointerEvent extends MouseEvent { - constructor(type: string, params: PointerEventInit = {}) { - super(type, params); - } - } as unknown as typeof PointerEvent; -} - -vi.mock('$lib/utils/eventDateHelpers', () => ({ - toDate: (d: string | Date) => new Date(d), -})); - -vi.mock('$lib/stores/events.svelte', () => ({ - eventsStore: { - isDraftEvent: vi.fn(() => false), - updateDraftEvent: vi.fn(), - updateEvent: vi.fn().mockResolvedValue({ data: {}, error: null }), - }, -})); - -vi.mock('$lib/utils/calendarConstants', () => ({ - SNAP_INTERVAL_MINUTES: 15, -})); - -import { useEventDragDrop } from './useEventDragDrop.svelte'; - -function createMockContainer() { - return { - getBoundingClientRect: () => ({ - left: 0, - top: 0, - right: 700, - bottom: 960, - width: 700, - height: 960, - }), - parentElement: { scrollTop: 0 }, - } as unknown as HTMLElement; -} - -function makeDays(): Date[] { - return Array.from({ length: 7 }, (_, i) => { - const d = new Date('2026-03-02'); - d.setDate(d.getDate() + i); - return d; - }); -} - -function minutesToPercent(minutes: number): number { - return (minutes / (24 * 60)) * 100; -} - -function makeConfig() { - return { - containerEl: createMockContainer(), - days: makeDays(), - firstVisibleHour: 0, - lastVisibleHour: 24, - totalVisibleHours: 24, - hourHeight: 40, - minutesToPercent, - }; -} - -describe('useEventDragDrop', () => { - it('should start in idle state', () => { - const dd = useEventDragDrop(() => makeConfig()); - expect(dd.isDragging).toBe(false); - expect(dd.isResizing).toBe(false); - expect(dd.draggedEvent).toBeNull(); - expect(dd.resizeEvent).toBeNull(); - expect(dd.hasMoved).toBe(false); - }); - - it('should set isDragging when startDrag is called', () => { - const dd = useEventDragDrop(() => makeConfig()); - const event = { - id: 'evt-1', - startTime: '2026-03-02T10:00:00', - endTime: '2026-03-02T11:00:00', - }; - const pointerEvent = new PointerEvent('pointerdown', { - clientX: 100, - clientY: 200, - }); - - dd.startDrag(event as any, pointerEvent); - - expect(dd.isDragging).toBe(true); - expect(dd.draggedEvent).toBeTruthy(); - expect(dd.draggedEvent!.id).toBe('evt-1'); - - // Cleanup - dd.cancel(); - }); - - it('should set isResizing when startResize is called', () => { - const dd = useEventDragDrop(() => makeConfig()); - const event = { - id: 'evt-1', - startTime: '2026-03-02T10:00:00', - endTime: '2026-03-02T11:00:00', - }; - const pointerEvent = new PointerEvent('pointerdown', { - clientX: 100, - clientY: 200, - }); - - dd.startResize(event as any, 'bottom', pointerEvent); - - expect(dd.isResizing).toBe(true); - expect(dd.resizeEvent).toBeTruthy(); - - dd.cancel(); - }); - - it('should reset all state on cancel', () => { - const dd = useEventDragDrop(() => makeConfig()); - const event = { - id: 'evt-1', - startTime: '2026-03-02T10:00:00', - endTime: '2026-03-02T11:00:00', - }; - const pointerEvent = new PointerEvent('pointerdown', { - clientX: 100, - clientY: 200, - }); - - dd.startDrag(event as any, pointerEvent); - expect(dd.isDragging).toBe(true); - - dd.cancel(); - expect(dd.isDragging).toBe(false); - expect(dd.draggedEvent).toBeNull(); - }); - - it('should reset hasMoved on resetHasMoved', () => { - const dd = useEventDragDrop(() => makeConfig()); - // hasMoved starts false - expect(dd.hasMoved).toBe(false); - dd.resetHasMoved(); - expect(dd.hasMoved).toBe(false); - }); - - it('should return empty string for getResizePreviewTime when not resizing', () => { - const dd = useEventDragDrop(() => makeConfig()); - expect(dd.getResizePreviewTime()).toBe(''); - }); - - it('should calculate preview positions on drag start', () => { - const dd = useEventDragDrop(() => makeConfig()); - const event = { - id: 'evt-1', - startTime: '2026-03-02T12:00:00', // noon - endTime: '2026-03-02T13:00:00', // 1pm - }; - const pointerEvent = new PointerEvent('pointerdown', { - clientX: 100, - clientY: 480, // middle of container - }); - - dd.startDrag(event as any, pointerEvent); - - // Preview top should be around 50% (12:00 = 720 min / 1440 min) - expect(dd.dragPreviewTop).toBeCloseTo(50, 0); - // Height should be ~4.17% (60 min / 1440 min) - expect(dd.dragPreviewHeight).toBeCloseTo(4.17, 0); - - dd.cancel(); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/composables/useSwipeNavigation.svelte.ts b/apps/calendar/apps/web-archived/src/lib/composables/useSwipeNavigation.svelte.ts deleted file mode 100644 index 82f181cd7..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/useSwipeNavigation.svelte.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * Swipe Navigation Composable - * Extracts touch/wheel/velocity/snap/animation logic from ViewCarousel - */ - -export interface SwipeNavigationConfig { - /** Get current viewport width */ - getViewportWidth: () => number; - /** Navigate to previous page */ - onNavigatePrev: () => void; - /** Navigate to next page */ - onNavigateNext: () => void; - /** Whether swipe is disabled */ - disabled?: boolean; - /** Snap threshold as fraction of viewport width (default: 0.15) */ - snapThreshold?: number; - /** Velocity threshold in px/ms (default: 0.5) */ - velocityThreshold?: number; - /** Animation speed in px/ms (default: 3.0) */ - animationSpeed?: number; - /** Wheel debounce in ms (default: 50) */ - wheelDebounceMs?: number; -} - -export function useSwipeNavigation(getConfig: () => SwipeNavigationConfig) { - // Swipe tracking state - let offsetX = $state(0); - let startX = $state(0); - let isSwiping = $state(false); - let isAnimating = $state(false); - let animatingDirection: 'prev' | 'next' | null = null; - - // Velocity tracking - let lastX = 0; - let lastTime = 0; - let velocity = 0; - - // Animation frame tracking - let animationFrameId: number | null = null; - let pendingCallback: (() => void) | null = null; - - // Wheel debounce - let wheelDebounceTimer: ReturnType | null = null; - - function getDefaults() { - const config = getConfig(); - return { - snapThreshold: config.snapThreshold ?? 0.15, - velocityThreshold: config.velocityThreshold ?? 0.5, - animationSpeed: config.animationSpeed ?? 3.0, - wheelDebounceMs: config.wheelDebounceMs ?? 50, - }; - } - - function handleWheel(e: WheelEvent) { - const config = getConfig(); - if (config.disabled) return; - if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return; - - const target = e.target as HTMLElement; - if (target.closest('[data-event-id]') || target.closest('[data-dragging]')) return; - - e.preventDefault(); - const viewportWidth = config.getViewportWidth(); - - if (isAnimating) { - const scrollDirection = e.deltaX < 0 ? 'next' : 'prev'; - if (scrollDirection === animatingDirection && Math.abs(e.deltaX) > 10) { - chainNavigation(scrollDirection); - } - return; - } - - offsetX += e.deltaX * -1; - offsetX = Math.max(-viewportWidth, Math.min(viewportWidth, offsetX)); - - const { wheelDebounceMs } = getDefaults(); - if (wheelDebounceTimer) clearTimeout(wheelDebounceTimer); - wheelDebounceTimer = setTimeout(snapToPage, wheelDebounceMs); - } - - function handleTouchStart(e: TouchEvent) { - const config = getConfig(); - if (config.disabled || isAnimating) return; - - const target = e.target as HTMLElement; - if (target.closest('[data-event-id]') || target.closest('[data-dragging]')) return; - - startX = e.touches[0].clientX; - lastX = startX; - lastTime = performance.now(); - velocity = 0; - isSwiping = true; - - if (wheelDebounceTimer) { - clearTimeout(wheelDebounceTimer); - wheelDebounceTimer = null; - } - } - - function handleTouchMove(e: TouchEvent) { - const config = getConfig(); - if (!isSwiping || config.disabled) return; - - const currentX = e.touches[0].clientX; - const currentTime = performance.now(); - const dt = currentTime - lastTime; - if (dt > 0) velocity = (currentX - lastX) / dt; - - lastX = currentX; - lastTime = currentTime; - - const viewportWidth = config.getViewportWidth(); - offsetX = currentX - startX; - offsetX = Math.max(-viewportWidth, Math.min(viewportWidth, offsetX)); - } - - function handleTouchEnd() { - if (!isSwiping) return; - isSwiping = false; - snapToPage(); - } - - function handleTouchCancel() { - if (!isSwiping) return; - isSwiping = false; - isAnimating = true; - animateToOffset(0, () => { - isAnimating = false; - }); - } - - function chainNavigation(direction: 'prev' | 'next') { - const config = getConfig(); - if (animationFrameId !== null) { - cancelAnimationFrame(animationFrameId); - animationFrameId = null; - } - - if (animatingDirection === 'prev') config.onNavigatePrev(); - else if (animatingDirection === 'next') config.onNavigateNext(); - - const viewportWidth = config.getViewportWidth(); - offsetX = direction === 'prev' ? viewportWidth * 0.4 : -viewportWidth * 0.4; - animatingDirection = direction; - - const targetOffset = direction === 'prev' ? viewportWidth : -viewportWidth; - pendingCallback = () => { - if (direction === 'prev') config.onNavigatePrev(); - else config.onNavigateNext(); - offsetX = 0; - isAnimating = false; - animatingDirection = null; - pendingCallback = null; - }; - - animateToOffset(targetOffset, pendingCallback); - } - - function snapToPage() { - const config = getConfig(); - const viewportWidth = config.getViewportWidth(); - if (isAnimating || viewportWidth === 0) return; - - const { snapThreshold, velocityThreshold } = getDefaults(); - const threshold = viewportWidth * snapThreshold; - const hasHighVelocity = Math.abs(velocity) > velocityThreshold; - - let targetPage: 'prev' | 'next' | 'current' = 'current'; - - if (offsetX > threshold || (hasHighVelocity && velocity > 0 && offsetX > 0)) { - targetPage = 'prev'; - } else if (offsetX < -threshold || (hasHighVelocity && velocity < 0 && offsetX < 0)) { - targetPage = 'next'; - } - - isAnimating = true; - animatingDirection = targetPage === 'current' ? null : targetPage; - - if (targetPage === 'prev') { - pendingCallback = () => { - config.onNavigatePrev(); - offsetX = 0; - isAnimating = false; - animatingDirection = null; - pendingCallback = null; - }; - animateToOffset(viewportWidth, pendingCallback); - } else if (targetPage === 'next') { - pendingCallback = () => { - config.onNavigateNext(); - offsetX = 0; - isAnimating = false; - animatingDirection = null; - pendingCallback = null; - }; - animateToOffset(-viewportWidth, pendingCallback); - } else { - pendingCallback = () => { - isAnimating = false; - animatingDirection = null; - pendingCallback = null; - }; - animateToOffset(0, pendingCallback); - } - } - - function animateToOffset(targetX: number, onComplete: () => void) { - if (animationFrameId !== null) { - cancelAnimationFrame(animationFrameId); - } - - const { animationSpeed } = getDefaults(); - const sX = offsetX; - const distance = targetX - sX; - const direction = Math.sign(distance); - - if (Math.abs(distance) < 1) { - offsetX = targetX; - onComplete(); - return; - } - - let lastFrameTime = performance.now(); - - function tick() { - const now = performance.now(); - const dt = now - lastFrameTime; - lastFrameTime = now; - - offsetX += animationSpeed * dt * direction; - - const reachedTarget = - (direction > 0 && offsetX >= targetX) || (direction < 0 && offsetX <= targetX); - - if (reachedTarget) { - offsetX = targetX; - animationFrameId = null; - onComplete(); - } else { - animationFrameId = requestAnimationFrame(tick); - } - } - - animationFrameId = requestAnimationFrame(tick); - } - - return { - get offsetX() { - return offsetX; - }, - get isSwiping() { - return isSwiping; - }, - get isAnimating() { - return isAnimating; - }, - - handleWheel, - handleTouchStart, - handleTouchMove, - handleTouchEnd, - handleTouchCancel, - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/composables/useVisibleHours.svelte.ts b/apps/calendar/apps/web-archived/src/lib/composables/useVisibleHours.svelte.ts deleted file mode 100644 index d823418cb..000000000 --- a/apps/calendar/apps/web-archived/src/lib/composables/useVisibleHours.svelte.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * useVisibleHours Composable - * - * Provides hour filtering and time-to-position calculations for calendar views. - * Extracts common logic from WeekView, MultiDayView, and DayView. - */ - -import { settingsStore } from '$lib/stores/settings.svelte'; - -const ALL_HOURS = Array.from({ length: 24 }, (_, i) => i); - -/** - * Creates reactive hour visibility state and helper functions - */ -export function useVisibleHours() { - // Filtered hours based on settings - let hours = $derived( - settingsStore.filterHoursEnabled - ? ALL_HOURS.filter((h) => h >= settingsStore.dayStartHour && h < settingsStore.dayEndHour) - : ALL_HOURS - ); - - // Calculate visible hours range for positioning - let firstVisibleHour = $derived( - settingsStore.filterHoursEnabled ? settingsStore.dayStartHour : 0 - ); - - let lastVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayEndHour : 24); - - let totalVisibleHours = $derived(lastVisibleHour - firstVisibleHour); - - /** - * Convert minutes (from midnight) to percentage position - * accounting for hidden hours when filtering is enabled - */ - function minutesToPercent(minutes: number): number { - const adjustedMinutes = minutes - firstVisibleHour * 60; - return (adjustedMinutes / (totalVisibleHours * 60)) * 100; - } - - /** - * Convert percentage position back to minutes (from midnight) - */ - function percentToMinutes(percent: number): number { - return (percent / 100) * (totalVisibleHours * 60) + firstVisibleHour * 60; - } - - /** - * Check if a time range overlaps with the visible hours range - */ - function isTimeRangeVisible(startMinutes: number, endMinutes: number): boolean { - const visibleStartMinutes = firstVisibleHour * 60; - const visibleEndMinutes = lastVisibleHour * 60; - return startMinutes < visibleEndMinutes && endMinutes > visibleStartMinutes; - } - - return { - get hours() { - return hours; - }, - get firstVisibleHour() { - return firstVisibleHour; - }, - get lastVisibleHour() { - return lastVisibleHour; - }, - get totalVisibleHours() { - return totalVisibleHours; - }, - minutesToPercent, - percentToMinutes, - isTimeRangeVisible, - }; -} - -/** - * Creates a reactive current time indicator - * Updates every minute and provides position calculation - */ -export function useCurrentTimeIndicator() { - let now = $state(new Date()); - - // Update current time every minute - $effect(() => { - const interval = setInterval(() => { - now = new Date(); - }, 60000); - return () => clearInterval(interval); - }); - - return { - get now() { - return now; - }, - /** - * Get current time as minutes from midnight - */ - get currentMinutes() { - return now.getHours() * 60 + now.getMinutes(); - }, - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/config/helpConfig.ts b/apps/calendar/apps/web-archived/src/lib/config/helpConfig.ts deleted file mode 100644 index acffe0f51..000000000 --- a/apps/calendar/apps/web-archived/src/lib/config/helpConfig.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { NavigationArrow, CalendarBlank } from '@manacore/shared-icons'; -import { - COMMON_SHORTCUTS, - COMMON_SYNTAX, - DEFAULT_LIVE_EXAMPLE, - type HelpModalConfig, - type ShortcutCategory, - type SyntaxGroup, -} from '@manacore/shared-ui'; - -/** - * Calendar-specific keyboard shortcuts - */ -const CALENDAR_SHORTCUTS: ShortcutCategory[] = [ - { - id: 'navigation', - title: 'Navigation', - icon: NavigationArrow, - shortcuts: [ - { - keys: ['Cmd', '1'], - altKeys: ['Ctrl', '1'], - description: 'Kalender öffnen', - category: 'navigation', - }, - { - keys: ['Cmd', '2'], - altKeys: ['Ctrl', '2'], - description: 'Einstellungen öffnen', - category: 'navigation', - }, - ], - }, - { - id: 'calendar', - title: 'Kalender', - icon: CalendarBlank, - shortcuts: [ - { - keys: ['Enter'], - description: 'Event/Task öffnen', - category: 'calendar', - }, - { - keys: ['Space'], - description: 'Event/Task öffnen', - category: 'calendar', - }, - { - keys: ['Esc'], - description: 'Drag/Resize abbrechen', - category: 'calendar', - }, - ], - }, -]; - -/** - * Calendar-specific syntax patterns (extends common syntax) - */ -const CALENDAR_SYNTAX: SyntaxGroup[] = [ - // Calendar uses all common syntax patterns -]; - -/** - * Complete help configuration for the Calendar app - * Combines common shortcuts/syntax with Calendar-specific ones - */ -export const CALENDAR_HELP_CONFIG: HelpModalConfig = { - shortcuts: [...COMMON_SHORTCUTS, ...CALENDAR_SHORTCUTS], - syntax: [...COMMON_SYNTAX, ...CALENDAR_SYNTAX], - defaultTab: 'shortcuts', - liveExample: DEFAULT_LIVE_EXAMPLE, -}; - -/** - * Export individual parts for customization - */ -export { CALENDAR_SHORTCUTS, CALENDAR_SYNTAX }; diff --git a/apps/calendar/apps/web-archived/src/lib/content/help/index.test.ts b/apps/calendar/apps/web-archived/src/lib/content/help/index.test.ts deleted file mode 100644 index cfb673b12..000000000 --- a/apps/calendar/apps/web-archived/src/lib/content/help/index.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { getCalendarHelpContent } from './index'; - -describe('Calendar Help Content', () => { - it('returns valid German content', () => { - const content = getCalendarHelpContent('de'); - - expect(content.faq.length).toBeGreaterThan(0); - content.faq.forEach((faq) => { - expect(faq.id).toBeTruthy(); - expect(faq.question).toBeTruthy(); - expect(faq.answer).toBeTruthy(); - }); - - expect(content.features).toBeDefined(); - expect(content.contact).toBeDefined(); - expect(content.contact.supportEmail).toBe('support@mana.how'); - }); - - it('returns valid English content', () => { - const content = getCalendarHelpContent('en'); - - expect(content.faq.length).toBeGreaterThan(0); - content.faq.forEach((faq) => { - expect(faq.id).toBeTruthy(); - expect(faq.question).toBeTruthy(); - expect(faq.answer).toBeTruthy(); - }); - - expect(content.features).toBeDefined(); - expect(content.contact).toBeDefined(); - }); - - it('returns same number of FAQ items for both languages', () => { - const de = getCalendarHelpContent('de'); - const en = getCalendarHelpContent('en'); - - expect(de.faq.length).toBe(en.faq.length); - expect(de.features.length).toBe(en.features.length); - }); - - it('has unique FAQ IDs', () => { - const content = getCalendarHelpContent('de'); - const ids = content.faq.map((f) => f.id); - expect(new Set(ids).size).toBe(ids.length); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/content/help/index.ts b/apps/calendar/apps/web-archived/src/lib/content/help/index.ts deleted file mode 100644 index 2622f1c69..000000000 --- a/apps/calendar/apps/web-archived/src/lib/content/help/index.ts +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Help content for Calendar app - */ - -import type { HelpContent } from '@manacore/help'; -import { getPrivacyFAQs } from '@manacore/help'; - -export function getCalendarHelpContent(locale: string): HelpContent { - const isDE = locale === 'de'; - const isFR = locale === 'fr'; - const isIT = locale === 'it'; - const isES = locale === 'es'; - - function t(de: string, en: string, fr?: string, it?: string, es?: string): string { - if (isDE) return de; - if (isFR) return fr ?? en; - if (isIT) return it ?? en; - if (isES) return es ?? en; - return en; - } - - return { - faq: [ - { - id: 'faq-create-event', - question: t( - 'Wie erstelle ich einen Termin?', - 'How do I create an event?', - 'Comment créer un événement ?', - 'Come creo un evento?', - '¿Cómo creo un evento?' - ), - answer: t( - '

Du kannst Termine auf verschiedene Arten erstellen:

  • Schnelleingabe: Nutze die Eingabeleiste oben — tippe z.B. "Meeting morgen 14-16 Uhr @Arbeit"
  • Klick & Ziehen: Klicke und ziehe in der Wochenansicht, um einen Zeitraum auszuwählen
  • Neuer Termin: Klicke auf das + Symbol für das vollständige Formular
', - '

You can create events in several ways:

  • Quick input: Use the input bar at the top — type e.g. "Meeting tomorrow 2-4pm @Work"
  • Click & drag: Click and drag in the week view to select a time range
  • New event: Click the + icon for the full form
' - ), - category: 'features', - order: 1, - language: isDE ? 'de' : isFR ? 'fr' : isIT ? 'it' : isES ? 'es' : 'en', - tags: isDE ? ['termin', 'erstellen', 'neu'] : ['event', 'create', 'new'], - }, - { - id: 'faq-recurring', - question: t( - 'Wie erstelle ich wiederkehrende Termine?', - 'How do I create recurring events?' - ), - answer: t( - '

Öffne einen Termin und setze die Wiederholung. Unterstützte Optionen:

  • Täglich, wöchentlich, monatlich, jährlich
  • Bestimmte Wochentage (z.B. Mo, Mi, Fr)
  • Alle X Wochen/Monate
  • Enddatum oder Anzahl der Wiederholungen

In der Schnelleingabe kannst du auch "wöchentlich", "täglich" oder "jeden Montag" eingeben.

', - '

Open an event and set the recurrence. Supported options:

  • Daily, weekly, monthly, yearly
  • Specific weekdays (e.g. Mon, Wed, Fri)
  • Every X weeks/months
  • End date or number of occurrences

In the quick input, you can also type "weekly", "daily", or "every Monday".

' - ), - category: 'features', - order: 2, - language: isDE ? 'de' : 'en', - tags: isDE ? ['wiederkehrend', 'wiederholung', 'serie'] : ['recurring', 'repeat', 'series'], - }, - { - id: 'faq-share-calendar', - question: t( - 'Wie teile ich einen Kalender mit anderen?', - 'How do I share a calendar with others?' - ), - answer: t( - '

Gehe zu Einstellungen > Freigabe und wähle den Kalender aus. Du kannst:

  • Per E-Mail einladen (Lese- oder Schreibzugriff)
  • Per Link teilen (mit optionalem Ablaufdatum)
  • Berechtigungen nachträglich ändern oder entziehen
', - '

Go to Settings > Sharing and select the calendar. You can:

  • Invite by email (read or write access)
  • Share by link (with optional expiration)
  • Change or revoke permissions later
' - ), - category: 'features', - order: 3, - language: isDE ? 'de' : 'en', - tags: isDE ? ['teilen', 'freigabe', 'einladen'] : ['share', 'invite', 'permissions'], - }, - { - id: 'faq-sync', - question: t('Kann ich externe Kalender synchronisieren?', 'Can I sync external calendars?'), - answer: t( - '

Ja! Gehe zu Einstellungen > Sync. Unterstützte Quellen:

  • CalDAV: Nextcloud, Radicale, etc.
  • iCal URL: Jeder öffentliche Kalender-Feed
  • Google Kalender (über CalDAV)
  • Apple Kalender (über CalDAV)

Die Synchronisation erfolgt automatisch im eingestellten Intervall.

', - '

Yes! Go to Settings > Sync. Supported sources:

  • CalDAV: Nextcloud, Radicale, etc.
  • iCal URL: Any public calendar feed
  • Google Calendar (via CalDAV)
  • Apple Calendar (via CalDAV)

Synchronization happens automatically at the configured interval.

' - ), - category: 'technical', - order: 4, - language: isDE ? 'de' : 'en', - tags: isDE - ? ['sync', 'caldav', 'google', 'apple', 'extern'] - : ['sync', 'caldav', 'google', 'apple', 'external'], - }, - ...getPrivacyFAQs(locale, { dataTypeDE: 'Kalenderdaten', dataTypeEN: 'calendar data' }), - ], - features: [ - { - id: 'feature-calendars', - title: t('Mehrere Kalender', 'Multiple Calendars'), - description: t( - 'Erstelle farbcodierte Kalender für Arbeit, Privat und mehr', - 'Create color-coded calendars for work, personal, and more' - ), - icon: '📅', - category: 'core', - highlights: t( - 'Farbcodierung,Ein-/Ausblenden,Standard-Kalender', - 'Color coding,Show/hide,Default calendar' - ).split(','), - content: '', - order: 1, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-views', - title: t('Flexible Ansichten', 'Flexible Views'), - description: t( - 'Wechsle zwischen Tag-, Wochen-, Monats- und Agenda-Ansicht', - 'Switch between day, week, month, and agenda views' - ), - icon: '👁️', - category: 'core', - highlights: t( - 'Wochenansicht,Monatsansicht,Agenda-Liste,Tagesansicht', - 'Week view,Month view,Agenda list,Day view' - ).split(','), - content: '', - order: 2, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-recurring', - title: t('Wiederkehrende Termine', 'Recurring Events'), - description: t( - 'Erstelle Serien mit flexiblen Wiederholungsregeln', - 'Create series with flexible recurrence rules' - ), - icon: '🔄', - category: 'advanced', - highlights: t( - 'RFC 5545 RRULE,Ausnahmen,Enddatum oder Anzahl', - 'RFC 5545 RRULE,Exceptions,End date or count' - ).split(','), - content: '', - order: 3, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-sharing', - title: t('Kalender teilen', 'Calendar Sharing'), - description: t( - 'Teile Kalender mit Lese- oder Schreibzugriff', - 'Share calendars with read or write access' - ), - icon: '🤝', - category: 'advanced', - highlights: t( - 'E-Mail-Einladung,Link-Freigabe,Berechtigungen', - 'Email invitation,Link sharing,Permissions' - ).split(','), - content: '', - order: 4, - language: isDE ? 'de' : 'en', - }, - ], - shortcuts: [ - { - id: 'shortcuts-navigation', - category: 'navigation', - title: 'Navigation', - language: isDE ? 'de' : 'en', - order: 1, - shortcuts: [ - { - shortcut: 'Cmd/Ctrl + 1', - action: t('Kalender öffnen', 'Open Calendar'), - }, - { - shortcut: 'Cmd/Ctrl + 2', - action: t('Einstellungen öffnen', 'Open Settings'), - }, - ], - }, - { - id: 'shortcuts-calendar', - category: 'app-specific', - title: t('Kalender', 'Calendar'), - language: isDE ? 'de' : 'en', - order: 2, - shortcuts: [ - { - shortcut: 'Enter / Space', - action: t('Event/Task öffnen', 'Open event/task'), - }, - { - shortcut: 'Esc', - action: t('Drag/Resize abbrechen', 'Cancel drag/resize'), - }, - ], - }, - ], - gettingStarted: [], - changelog: [], - contact: { - id: 'contact-support', - title: t('Support kontaktieren', 'Contact Support'), - content: t( - '

Unser Support-Team hilft dir bei allen Fragen rund um den Kalender.

', - '

Our support team is here to help you with any calendar-related questions.

' - ), - language: isDE ? 'de' : 'en', - order: 1, - supportEmail: 'support@mana.how', - documentationUrl: 'https://mana.how/docs', - responseTime: t('Normalerweise innerhalb von 24 Stunden', 'Usually within 24 hours'), - }, - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/data/guest-seed.ts b/apps/calendar/apps/web-archived/src/lib/data/guest-seed.ts deleted file mode 100644 index 45932fbe6..000000000 --- a/apps/calendar/apps/web-archived/src/lib/data/guest-seed.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Guest seed data for the Calendar app. - * - * These records are loaded into IndexedDB when a new guest visits the app. - * They provide a "Persoenlich" calendar with two sample events so the user - * can immediately see how the app works. - */ - -import type { LocalCalendar, LocalEvent } from './local-store'; - -const PERSONAL_CALENDAR_ID = 'personal-calendar'; - -export const guestCalendars: LocalCalendar[] = [ - { - id: PERSONAL_CALENDAR_ID, - name: 'Persönlich', - color: '#3B82F6', - isDefault: true, - isVisible: true, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }, -]; - -const now = new Date(); -const today10 = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0, 0); -const today11 = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 11, 0, 0); - -const tomorrow = new Date(now); -tomorrow.setDate(tomorrow.getDate() + 1); -const tomorrow14 = new Date( - tomorrow.getFullYear(), - tomorrow.getMonth(), - tomorrow.getDate(), - 14, - 0, - 0 -); -const tomorrow15 = new Date( - tomorrow.getFullYear(), - tomorrow.getMonth(), - tomorrow.getDate(), - 15, - 30, - 0 -); - -export const guestEvents: LocalEvent[] = [ - { - id: 'sample-event-1', - calendarId: PERSONAL_CALENDAR_ID, - title: 'Willkommen bei Kalender!', - description: 'Dies ist ein Beispieltermin. Tippe darauf, um ihn zu bearbeiten oder zu löschen.', - startDate: today10.toISOString(), - endDate: today11.toISOString(), - allDay: false, - location: null, - recurrenceRule: null, - color: null, - reminders: null, - }, - { - id: 'sample-event-2', - calendarId: PERSONAL_CALENDAR_ID, - title: 'Mittagessen mit Freunden', - description: null, - startDate: tomorrow14.toISOString(), - endDate: tomorrow15.toISOString(), - allDay: false, - location: 'Café am See', - recurrenceRule: null, - color: null, - reminders: null, - }, -]; diff --git a/apps/calendar/apps/web-archived/src/lib/data/local-store.ts b/apps/calendar/apps/web-archived/src/lib/data/local-store.ts deleted file mode 100644 index 1900b68ce..000000000 --- a/apps/calendar/apps/web-archived/src/lib/data/local-store.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Calendar App — Local-First Data Layer - * - * Defines the IndexedDB database, collections, and guest seed data. - * This is the single source of truth for all Calendar data. - */ - -import { createLocalStore, type BaseRecord } from '@manacore/local-store'; -import { guestCalendars, guestEvents } from './guest-seed'; - -// ─── Types ────────────────────────────────────────────────── - -export interface LocalCalendar extends BaseRecord { - name: string; - color: string; - isDefault: boolean; - isVisible: boolean; - timezone: string; -} - -export interface LocalEvent extends BaseRecord { - calendarId: string; - title: string; - description?: string | null; - startDate: string; - endDate: string; - allDay: boolean; - location?: string | null; - recurrenceRule?: string | null; - color?: string | null; - reminders?: unknown | null; -} - -// ─── Store ────────────────────────────────────────────────── - -const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050'; - -export const calendarStore = createLocalStore({ - appId: 'calendar', - collections: [ - { - name: 'calendars', - indexes: ['isDefault', 'isVisible'], - guestSeed: guestCalendars, - }, - { - name: 'events', - indexes: ['calendarId', 'startDate', 'endDate', 'allDay', '[calendarId+startDate]'], - guestSeed: guestEvents, - }, - ], - sync: { - serverUrl: SYNC_SERVER_URL, - }, -}); - -// Typed collection accessors -export const calendarCollection = calendarStore.collection('calendars'); -export const eventCollection = calendarStore.collection('events'); diff --git a/apps/calendar/apps/web-archived/src/lib/data/queries.ts b/apps/calendar/apps/web-archived/src/lib/data/queries.ts deleted file mode 100644 index 27f17c233..000000000 --- a/apps/calendar/apps/web-archived/src/lib/data/queries.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Reactive Queries & Pure Filter Helpers for Calendar - * - * Uses Dexie liveQuery to automatically re-render when IndexedDB changes - * (local writes, sync updates, other tabs). Components call these hooks - * at init time; no manual fetch/refresh needed. - */ - -import { useLiveQueryWithDefault } from '@manacore/local-store/svelte'; -import { - calendarCollection, - eventCollection, - type LocalCalendar, - type LocalEvent, -} from './local-store'; -import type { Calendar, CalendarEvent } from '@calendar/shared'; -import { parseRRule, generateOccurrences } from '@calendar/shared'; -import { isSameDay, isWithinInterval, differenceInMilliseconds, format } from 'date-fns'; -import { BIRTHDAY_CALENDAR } from '$lib/api/birthdays'; - -// ─── Type Converters ─────────────────────────────────────── - -/** Convert a LocalCalendar (IndexedDB) to the shared Calendar type. */ -export function toCalendar(local: LocalCalendar): Calendar { - return { - id: local.id, - userId: 'guest', - name: local.name, - color: local.color, - isDefault: local.isDefault, - isVisible: local.isVisible, - timezone: local.timezone, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: local.updatedAt ?? new Date().toISOString(), - }; -} - -/** Convert a LocalEvent (IndexedDB) to the shared CalendarEvent type. */ -export function toCalendarEvent(local: LocalEvent): CalendarEvent { - return { - id: local.id, - calendarId: local.calendarId, - userId: 'guest', - title: local.title, - description: local.description ?? null, - location: local.location ?? null, - startTime: local.startDate, - endTime: local.endDate, - isAllDay: local.allDay, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - recurrenceRule: local.recurrenceRule ?? null, - recurrenceEndDate: null, - recurrenceExceptions: null, - parentEventId: null, - color: local.color ?? null, - status: 'confirmed', - externalId: null, - metadata: null, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: local.updatedAt ?? new Date().toISOString(), - }; -} - -// ─── Live Query Hooks (call during component init) ───────── - -/** All calendars. Auto-updates on any change. */ -export function useAllCalendars() { - return useLiveQueryWithDefault(async () => { - const locals = await calendarCollection.getAll(); - return locals.map(toCalendar); - }, [] as Calendar[]); -} - -/** All events. Auto-updates on any change. */ -export function useAllEvents() { - return useLiveQueryWithDefault(async () => { - const locals = await eventCollection.getAll(); - return locals.map(toCalendarEvent); - }, [] as CalendarEvent[]); -} - -// ─── Pure Calendar Helpers ───────────────────────────────── - -/** Get visible calendars (where isVisible is true). */ -export function getVisibleCalendars(calendars: Calendar[]): Calendar[] { - return calendars.filter((c) => c.isVisible); -} - -/** Get the default calendar, falling back to the first calendar. */ -export function getDefaultCalendar(calendars: Calendar[]): Calendar | null { - return calendars.find((c) => c.isDefault) || calendars[0] || null; -} - -/** Get a calendar by ID. */ -export function getCalendarById(calendars: Calendar[], id: string): Calendar | undefined { - return calendars.find((c) => c.id === id); -} - -/** Get a calendar's color by ID, with fallback. */ -export function getCalendarColor(calendars: Calendar[], id: string): string { - const calendar = calendars.find((c) => c.id === id); - return calendar?.color || '#3b82f6'; -} - -/** Get a calendar's color by ID, with birthday calendar support and fallback. */ -export function getCalendarColorWithBirthdays(calendars: Calendar[], id: string): string { - if (id === BIRTHDAY_CALENDAR.id) { - return BIRTHDAY_CALENDAR.color; - } - return getCalendarColor(calendars, id); -} - -// ─── Pure Event Helpers ──────────────────────────────────── - -/** Get an event by ID. */ -export function getEventById(events: CalendarEvent[], id: string): CalendarEvent | undefined { - return events.find((e) => e.id === id); -} - -/** Convert a date string or Date to a Date. */ -function toDate(dateStr: string | Date): Date { - return typeof dateStr === 'string' ? new Date(dateStr) : dateStr; -} - -/** - * Expand recurring events into individual occurrences for a given range. - * Each occurrence gets a synthetic ID: `{parentId}__recurrence__{dateISO}` - */ -export function expandRecurringEvents( - rawEvents: CalendarEvent[], - rangeStart: Date, - rangeEnd: Date -): CalendarEvent[] { - const result: CalendarEvent[] = []; - - for (const event of rawEvents) { - if (!event.recurrenceRule) { - result.push(event); - continue; - } - - const pattern = parseRRule(event.recurrenceRule); - if (!pattern) { - result.push(event); - continue; - } - - const eventStart = toDate(event.startTime); - const eventEnd = toDate(event.endTime); - const durationMs = differenceInMilliseconds(eventEnd, eventStart); - const exceptions = (event.recurrenceExceptions as string[]) || []; - - const occurrences = generateOccurrences(eventStart, pattern, rangeStart, rangeEnd, exceptions); - - for (const occurrenceDate of occurrences) { - const occEnd = new Date(occurrenceDate.getTime() + durationMs); - const dateKey = format(occurrenceDate, 'yyyy-MM-dd'); - - result.push({ - ...event, - id: `${event.id}__recurrence__${dateKey}`, - parentEventId: event.id, - startTime: occurrenceDate.toISOString(), - endTime: occEnd.toISOString(), - }); - } - } - - return result; -} - -/** - * Get events for a specific date range, including recurrence expansion. - */ -export function getEventsForRange( - allEvents: CalendarEvent[], - rangeStart: Date, - rangeEnd: Date -): CalendarEvent[] { - // Filter to events that overlap the range - const inRange = allEvents.filter((event) => { - const eventStart = toDate(event.startTime); - const eventEnd = toDate(event.endTime); - return eventStart <= rangeEnd && eventEnd >= rangeStart; - }); - - // Also include recurring events that might generate occurrences in range - const recurring = allEvents.filter((event) => event.recurrenceRule && !inRange.includes(event)); - - return expandRecurringEvents([...inRange, ...recurring], rangeStart, rangeEnd); -} - -/** - * Get events for a specific day (pure helper, no draft support). - */ -export function getEventsForDay(events: CalendarEvent[], date: Date): CalendarEvent[] { - return events.filter((event) => { - const eventStart = toDate(event.startTime); - const eventEnd = toDate(event.endTime); - - if (event.isAllDay) { - return ( - isWithinInterval(date, { start: eventStart, end: eventEnd }) || isSameDay(date, eventStart) - ); - } - - return isSameDay(date, eventStart); - }); -} - -/** - * Get events within a time range. - */ -export function getEventsInRange(events: CalendarEvent[], start: Date, end: Date): CalendarEvent[] { - return events.filter((event) => { - const eventStart = toDate(event.startTime); - const eventEnd = toDate(event.endTime); - return eventStart <= end && eventEnd >= start; - }); -} diff --git a/apps/calendar/apps/web-archived/src/lib/i18n/index.ts b/apps/calendar/apps/web-archived/src/lib/i18n/index.ts deleted file mode 100644 index 87c65a934..000000000 --- a/apps/calendar/apps/web-archived/src/lib/i18n/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { browser } from '$app/environment'; -import { init, register, locale, waitLocale } from 'svelte-i18n'; - -// List of supported locales -export const supportedLocales = ['de', 'en', 'it', 'fr', 'es'] as const; -export type SupportedLocale = (typeof supportedLocales)[number]; - -// Default locale -const defaultLocale = 'de'; - -// Register all available locales -register('de', () => import('./locales/de.json')); -register('en', () => import('./locales/en.json')); -register('it', () => import('./locales/it.json')); -register('fr', () => import('./locales/fr.json')); -register('es', () => import('./locales/es.json')); - -// Get initial locale from browser or localStorage -function getInitialLocale(): SupportedLocale { - if (browser) { - // Check localStorage first - const stored = localStorage.getItem('calendar_locale'); - if (stored && supportedLocales.includes(stored as SupportedLocale)) { - return stored as SupportedLocale; - } - - // Fall back to browser language - const browserLang = navigator.language.split('-')[0]; - if (supportedLocales.includes(browserLang as SupportedLocale)) { - return browserLang as SupportedLocale; - } - } - - return defaultLocale; -} - -// Initialize i18n at module scope (required for SSR) -// getInitialLocale() internally checks for browser and falls back to defaultLocale -init({ - fallbackLocale: defaultLocale, - initialLocale: getInitialLocale(), -}); - -// Set locale and persist to localStorage -export function setLocale(newLocale: SupportedLocale) { - locale.set(newLocale); - if (browser) { - localStorage.setItem('calendar_locale', newLocale); - } -} - -// Wait for locale to be loaded (useful for SSR) -export { waitLocale }; diff --git a/apps/calendar/apps/web-archived/src/lib/i18n/locales/de.json b/apps/calendar/apps/web-archived/src/lib/i18n/locales/de.json deleted file mode 100644 index 5a19df88a..000000000 --- a/apps/calendar/apps/web-archived/src/lib/i18n/locales/de.json +++ /dev/null @@ -1,296 +0,0 @@ -{ - "app": { - "name": "Kalender", - "loading": "Laden..." - }, - "nav": { - "calendar": "Kalender", - "calendars": "Kalender", - "agenda": "Agenda", - "settings": "Einstellungen", - "feedback": "Feedback" - }, - "views": { - "day": "Tag", - "5day": "5 Tage", - "week": "Woche", - "10day": "10 Tage", - "14day": "14 Tage", - "month": "Monat", - "year": "Jahr", - "agenda": "Agenda", - "weekdaysOnly": "Nur Wochentage", - "weekNumber": "KW", - "moreEvents": "+{count} mehr", - "allDay": "Ganztägig", - "birthday": "Geburtstag", - "weekView": "Wochenansicht", - "monthView": "Monatsansicht" - }, - "calendar": { - "today": "Heute", - "newEvent": "Neuer Termin", - "noEvents": "Keine Termine", - "allDay": "Ganztägig", - "myCalendars": "Meine Kalender", - "sharedCalendars": "Geteilte Kalender", - "draftEvent": "(Neuer Termin)", - "untitled": "Ohne Titel", - "hideSidebar": "Sidebar ausblenden", - "showSidebar": "Sidebar einblenden", - "contextMenu": { - "edit": "Bearbeiten", - "duplicate": "Duplizieren", - "copy": "Kopie", - "delete": "Löschen" - } - }, - "event": { - "title": "Titel", - "description": "Beschreibung", - "location": "Ort", - "start": "Beginn", - "end": "Ende", - "allDay": "Ganztägig", - "repeat": "Wiederholen", - "reminder": "Erinnerung", - "calendar": "Kalender", - "save": "Speichern", - "delete": "Löschen", - "cancel": "Abbrechen", - "changeStartTime": "Startzeit ändern", - "changeEndTime": "Endzeit ändern" - }, - "repeat": { - "none": "Nicht wiederholen", - "daily": "Täglich", - "weekly": "Wöchentlich", - "monthly": "Monatlich", - "yearly": "Jährlich" - }, - "reminder": { - "atTime": "Zum Zeitpunkt", - "5min": "5 Minuten vorher", - "15min": "15 Minuten vorher", - "30min": "30 Minuten vorher", - "1hour": "1 Stunde vorher", - "1day": "1 Tag vorher" - }, - "share": { - "share": "Teilen", - "shareCalendar": "Kalender teilen", - "permissions": "Berechtigungen", - "read": "Nur lesen", - "write": "Lesen & Schreiben", - "admin": "Administrator", - "pending": "Ausstehend", - "accepted": "Akzeptiert" - }, - "auth": { - "login": "Anmelden", - "logout": "Abmelden", - "register": "Registrieren", - "email": "E-Mail", - "password": "Passwort", - "forgotPassword": "Passwort vergessen?" - }, - "toast": { - "eventLoadError": "Termine konnten nicht geladen werden", - "eventUpdateError": "Termin konnte nicht aktualisiert werden", - "eventDeleteError": "Termin konnte nicht gelöscht werden", - "eventDeleted": "Termin gelöscht", - "error": "Fehler", - "calendarShared": "Kalender mit {email} geteilt", - "shareLinkCreated": "Freigabe-Link erstellt", - "inviteAccepted": "Einladung angenommen", - "shareRemoved": "Freigabe entfernt", - "shareError": "Freigabe fehlgeschlagen", - "updateError": "Aktualisierung fehlgeschlagen", - "removeError": "Entfernen fehlgeschlagen", - "declineError": "Ablehnung fehlgeschlagen", - "calendarConnected": "{name} verbunden", - "calendarDisconnected": "{name} getrennt", - "syncCompleted": "Synchronisation abgeschlossen", - "connectionError": "Verbindung fehlgeschlagen", - "syncError": "Sync fehlgeschlagen" - }, - "common": { - "save": "Speichern", - "cancel": "Abbrechen", - "delete": "Löschen", - "edit": "Bearbeiten", - "add": "Hinzufügen", - "close": "Schließen", - "search": "Suchen", - "error": "Fehler", - "success": "Erfolgreich", - "calendar": "Kalender", - "create": "Erstellen" - }, - "settings": { - "myCalendars": "Meine Kalender", - "externalCalendars": "Externe Kalender", - "shares": "Kalender-Freigaben", - "appSettings": "App-Einstellungen", - "appSettingsDesc": "Diese Einstellungen werden mit allen Mana Apps synchronisiert", - "calendarView": "Kalender-Ansicht", - "events": "Termine", - "birthdays": "Geburtstage", - "account": "Konto", - "newCalendar": "Neuer Kalender", - "calendarName": "Kalender Name", - "name": "Name", - "color": "Farbe", - "default": "Standard", - "setAsDefault": "Als Standardkalender verwenden", - "currentDefault": "aktueller Standard", - "noCalendars": "Keine Kalender vorhanden", - "calendarCreated": "Kalender erstellt", - "calendarUpdated": "Kalender aktualisiert", - "calendarDeleted": "Kalender gelöscht", - "confirmDeleteCalendar": "Möchten Sie \"{name}\" wirklich löschen?", - "externalCalendarsDesc": "Verbinde Google Calendar, Apple Calendar, CalDAV oder iCal-URLs.", - "manageSync": "Kalender-Sync verwalten", - "sharesDesc": "Teile Kalender mit anderen Nutzern oder verwalte Einladungen.", - "manageShares": "Freigaben verwalten", - "defaultView": "Standard-Ansicht", - "defaultViewDesc": "Ansicht beim Öffnen des Kalenders", - "selectView": "Ansicht wählen", - "viewWeek": "Woche", - "viewMonth": "Monat", - "viewAgenda": "Agenda", - "timeFormat": "Zeitformat", - "timeFormatDesc": "Anzeige der Uhrzeiten", - "weekdaysOnly": "Nur Werktage anzeigen", - "weekdaysOnlyDesc": "Wochenenden in der Kalenderansicht ausblenden", - "showWeekNumbers": "Wochennummern anzeigen", - "showWeekNumbersDesc": "Kalenderwoche (KW) in der Ansicht anzeigen", - "filterHours": "Stunden filtern", - "filterHoursDesc": "Nur bestimmte Stunden in der Tages-/Wochenansicht anzeigen", - "visibleHours": "Sichtbare Stunden", - "visibleHoursDesc": "Zeitbereich der in der Kalenderansicht angezeigt wird", - "hoursFrom": "Von", - "hoursTo": "Bis", - "allDayEvents": "Ganztägige Termine", - "allDayEventsDesc": "Wie sollen ganztägige Termine angezeigt werden?", - "allDayInHeader": "In Kopfzeile", - "allDayAsBlock": "Als Tagesblock", - "defaultDuration": "Standard-Dauer", - "defaultDurationDesc": "Voreingestellte Dauer für neue Termine", - "selectDuration": "Dauer wählen", - "durationMinutes": "{count} Minuten", - "durationHours": "{count} Stunde(n)", - "defaultReminder": "Standard-Erinnerung", - "defaultReminderDesc": "Voreingestellte Erinnerung für neue Termine", - "selectReminder": "Erinnerung wählen", - "reminderNone": "Keine", - "reminderMinutes": "{count} Minuten", - "reminderHour": "1 Stunde", - "reminderDay": "1 Tag", - "showBirthdays": "Geburtstage anzeigen", - "showBirthdaysDesc": "Geburtstage aus Kontakten im Kalender anzeigen", - "showAge": "Alter anzeigen", - "showAgeDesc": "Das Alter der Person bei Geburtstagen anzeigen" - }, - "errors": { - "loadEvents": "Termine konnten nicht geladen werden", - "createEvent": "Termin konnte nicht erstellt werden", - "updateEvent": "Termin konnte nicht aktualisiert werden", - "deleteEvent": "Termin konnte nicht gelöscht werden" - }, - "success": { - "eventCreated": "Termin erstellt", - "eventDeleted": "Termin gelöscht" - }, - "a11y": { - "createEventOn": "Termin erstellen am {date}", - "slotTime": "{day} {time}" - }, - "error": { - "notFound": "Seite nicht gefunden", - "backToHome": "Zurück zur Startseite" - }, - "sync": { - "pageTitle": "Kalender-Sync - Einstellungen", - "title": "Kalender-Sync", - "back": "Zurück", - "connectCalendar": "Kalender verbinden", - "description": "Verbinde externe Kalender, um Termine zu importieren und zu synchronisieren.", - "emptyState": "Keine externen Kalender verbunden", - "syncNow": "Jetzt synchronisieren", - "disconnect": "Verbindung trennen", - "confirmDisconnect": "\"{name}\" wirklich trennen? Synchronisierte Termine werden gelöscht.", - "neverSynced": "Noch nie", - "directionLabel": "Richtung", - "lastSync": "Letzte Sync", - "statusLabel": "Status", - "autoSync": "Auto-Sync", - "connectCaldav": "CalDAV-Server verbinden", - "connectProvider": "{provider} verbinden", - "searching": "Suche...", - "searchCalendars": "Kalender suchen", - "discoveredCalendars": "Gefundene Kalender:", - "connecting": "Verbinde...", - "connect": "Verbinden", - "direction": { - "import": "Nur Import", - "export": "Nur Export", - "both": "Bidirektional" - }, - "status": { - "error": "Fehler", - "active": "Aktiv (alle {interval} Min.)", - "paused": "Pausiert" - }, - "providers": { - "icalUrl": "iCal URL", - "icalUrlDesc": "ICS-Link importieren (z.B. Feiertage)", - "caldav": "CalDAV", - "caldavDesc": "CalDAV-Server verbinden", - "google": "Google Calendar", - "googleDesc": "Mit Google Kalender synchronisieren", - "apple": "Apple Calendar", - "appleDesc": "iCloud Kalender verbinden" - }, - "form": { - "serverUrl": "Server-URL", - "username": "Benutzername", - "password": "Passwort", - "name": "Name", - "namePlaceholder": "Mein externer Kalender", - "url": "URL", - "syncDirection": "Sync-Richtung" - } - }, - "sharing": { - "pageTitle": "Kalender-Freigaben - Einstellungen", - "title": "Freigaben", - "back": "Zurück", - "shareCalendar": "Kalender teilen", - "invitations": "Einladungen ({count})", - "calendarInvitation": "Kalender-Einladung", - "access": "Zugriff", - "accept": "Annehmen", - "sharedWithMe": "Mit mir geteilt", - "sharedCalendar": "Geteilter Kalender", - "shareMyCalendars": "Meine Kalender teilen", - "notSharedYet": "Noch nicht geteilt", - "linkShare": "Link-Freigabe", - "pending": "Ausstehend", - "removeShare": "Freigabe entfernen", - "confirmRemoveShare": "Freigabe wirklich entfernen?", - "addPerson": "Person hinzufügen", - "share": "Teilen", - "sharing": "Teile...", - "permission": { - "read": "Lesen", - "write": "Lesen & Bearbeiten", - "admin": "Administrator" - }, - "form": { - "calendar": "Kalender", - "email": "E-Mail-Adresse", - "permission": "Berechtigung" - } - } -} diff --git a/apps/calendar/apps/web-archived/src/lib/i18n/locales/en.json b/apps/calendar/apps/web-archived/src/lib/i18n/locales/en.json deleted file mode 100644 index 06c63f2a1..000000000 --- a/apps/calendar/apps/web-archived/src/lib/i18n/locales/en.json +++ /dev/null @@ -1,296 +0,0 @@ -{ - "app": { - "name": "Calendar", - "loading": "Loading..." - }, - "nav": { - "calendar": "Calendar", - "calendars": "Calendars", - "agenda": "Agenda", - "settings": "Settings", - "feedback": "Feedback" - }, - "views": { - "day": "Day", - "5day": "5 Days", - "week": "Week", - "10day": "10 Days", - "14day": "14 Days", - "month": "Month", - "year": "Year", - "agenda": "Agenda", - "weekdaysOnly": "Weekdays only", - "weekNumber": "W", - "moreEvents": "+{count} more", - "allDay": "All day", - "birthday": "Birthday", - "weekView": "Week view", - "monthView": "Month view" - }, - "calendar": { - "today": "Today", - "newEvent": "New Event", - "noEvents": "No events", - "allDay": "All day", - "myCalendars": "My Calendars", - "sharedCalendars": "Shared Calendars", - "draftEvent": "(New Event)", - "untitled": "Untitled", - "hideSidebar": "Hide sidebar", - "showSidebar": "Show sidebar", - "contextMenu": { - "edit": "Edit", - "duplicate": "Duplicate", - "copy": "Copy", - "delete": "Delete" - } - }, - "event": { - "title": "Title", - "description": "Description", - "location": "Location", - "start": "Start", - "end": "End", - "allDay": "All day", - "repeat": "Repeat", - "reminder": "Reminder", - "calendar": "Calendar", - "save": "Save", - "delete": "Delete", - "cancel": "Cancel", - "changeStartTime": "Change start time", - "changeEndTime": "Change end time" - }, - "repeat": { - "none": "Don't repeat", - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly", - "yearly": "Yearly" - }, - "reminder": { - "atTime": "At time of event", - "5min": "5 minutes before", - "15min": "15 minutes before", - "30min": "30 minutes before", - "1hour": "1 hour before", - "1day": "1 day before" - }, - "share": { - "share": "Share", - "shareCalendar": "Share calendar", - "permissions": "Permissions", - "read": "View only", - "write": "Can edit", - "admin": "Admin", - "pending": "Pending", - "accepted": "Accepted" - }, - "auth": { - "login": "Login", - "logout": "Logout", - "register": "Register", - "email": "Email", - "password": "Password", - "forgotPassword": "Forgot password?" - }, - "toast": { - "eventLoadError": "Failed to load events", - "eventUpdateError": "Failed to update event", - "eventDeleteError": "Failed to delete event", - "eventDeleted": "Event deleted", - "error": "Error", - "calendarShared": "Calendar shared with {email}", - "shareLinkCreated": "Share link created", - "inviteAccepted": "Invitation accepted", - "shareRemoved": "Share removed", - "shareError": "Sharing failed", - "updateError": "Update failed", - "removeError": "Remove failed", - "declineError": "Decline failed", - "calendarConnected": "{name} connected", - "calendarDisconnected": "{name} disconnected", - "syncCompleted": "Sync completed", - "connectionError": "Connection failed", - "syncError": "Sync failed" - }, - "common": { - "save": "Save", - "cancel": "Cancel", - "delete": "Delete", - "edit": "Edit", - "add": "Add", - "close": "Close", - "search": "Search", - "error": "Error", - "success": "Success", - "calendar": "Calendar", - "create": "Create" - }, - "settings": { - "myCalendars": "My Calendars", - "externalCalendars": "External Calendars", - "shares": "Calendar Sharing", - "appSettings": "App Settings", - "appSettingsDesc": "These settings are synced across all Mana apps", - "calendarView": "Calendar View", - "events": "Events", - "birthdays": "Birthdays", - "account": "Account", - "newCalendar": "New Calendar", - "calendarName": "Calendar Name", - "name": "Name", - "color": "Color", - "default": "Default", - "setAsDefault": "Set as default calendar", - "currentDefault": "current default", - "noCalendars": "No calendars available", - "calendarCreated": "Calendar created", - "calendarUpdated": "Calendar updated", - "calendarDeleted": "Calendar deleted", - "confirmDeleteCalendar": "Are you sure you want to delete \"{name}\"?", - "externalCalendarsDesc": "Connect Google Calendar, Apple Calendar, CalDAV or iCal URLs.", - "manageSync": "Manage calendar sync", - "sharesDesc": "Share calendars with other users or manage invitations.", - "manageShares": "Manage sharing", - "defaultView": "Default View", - "defaultViewDesc": "View when opening the calendar", - "selectView": "Select view", - "viewWeek": "Week", - "viewMonth": "Month", - "viewAgenda": "Agenda", - "timeFormat": "Time Format", - "timeFormatDesc": "How times are displayed", - "weekdaysOnly": "Show weekdays only", - "weekdaysOnlyDesc": "Hide weekends in the calendar view", - "showWeekNumbers": "Show week numbers", - "showWeekNumbersDesc": "Display week numbers in the calendar view", - "filterHours": "Filter hours", - "filterHoursDesc": "Only show certain hours in day/week view", - "visibleHours": "Visible hours", - "visibleHoursDesc": "Time range displayed in the calendar view", - "hoursFrom": "From", - "hoursTo": "To", - "allDayEvents": "All-day events", - "allDayEventsDesc": "How should all-day events be displayed?", - "allDayInHeader": "In header", - "allDayAsBlock": "As day block", - "defaultDuration": "Default Duration", - "defaultDurationDesc": "Default duration for new events", - "selectDuration": "Select duration", - "durationMinutes": "{count} minutes", - "durationHours": "{count} hour(s)", - "defaultReminder": "Default Reminder", - "defaultReminderDesc": "Default reminder for new events", - "selectReminder": "Select reminder", - "reminderNone": "None", - "reminderMinutes": "{count} minutes", - "reminderHour": "1 hour", - "reminderDay": "1 day", - "showBirthdays": "Show birthdays", - "showBirthdaysDesc": "Show birthdays from contacts in the calendar", - "showAge": "Show age", - "showAgeDesc": "Display the person's age on birthdays" - }, - "errors": { - "loadEvents": "Failed to load events", - "createEvent": "Failed to create event", - "updateEvent": "Failed to update event", - "deleteEvent": "Failed to delete event" - }, - "success": { - "eventCreated": "Event created", - "eventDeleted": "Event deleted" - }, - "a11y": { - "createEventOn": "Create event on {date}", - "slotTime": "{day} {time}" - }, - "error": { - "notFound": "Page not found", - "backToHome": "Back to home" - }, - "sync": { - "pageTitle": "Calendar Sync - Settings", - "title": "Calendar Sync", - "back": "Back", - "connectCalendar": "Connect calendar", - "description": "Connect external calendars to import and synchronize events.", - "emptyState": "No external calendars connected", - "syncNow": "Sync now", - "disconnect": "Disconnect", - "confirmDisconnect": "Really disconnect \"{name}\"? Synchronized events will be deleted.", - "neverSynced": "Never", - "directionLabel": "Direction", - "lastSync": "Last sync", - "statusLabel": "Status", - "autoSync": "Auto-Sync", - "connectCaldav": "Connect CalDAV server", - "connectProvider": "Connect {provider}", - "searching": "Searching...", - "searchCalendars": "Search calendars", - "discoveredCalendars": "Discovered calendars:", - "connecting": "Connecting...", - "connect": "Connect", - "direction": { - "import": "Import only", - "export": "Export only", - "both": "Bidirectional" - }, - "status": { - "error": "Error", - "active": "Active (every {interval} min.)", - "paused": "Paused" - }, - "providers": { - "icalUrl": "iCal URL", - "icalUrlDesc": "Import ICS link (e.g. holidays)", - "caldav": "CalDAV", - "caldavDesc": "Connect CalDAV server", - "google": "Google Calendar", - "googleDesc": "Sync with Google Calendar", - "apple": "Apple Calendar", - "appleDesc": "Connect iCloud Calendar" - }, - "form": { - "serverUrl": "Server URL", - "username": "Username", - "password": "Password", - "name": "Name", - "namePlaceholder": "My external calendar", - "url": "URL", - "syncDirection": "Sync direction" - } - }, - "sharing": { - "pageTitle": "Calendar Sharing - Settings", - "title": "Sharing", - "back": "Back", - "shareCalendar": "Share calendar", - "invitations": "Invitations ({count})", - "calendarInvitation": "Calendar invitation", - "access": "access", - "accept": "Accept", - "sharedWithMe": "Shared with me", - "sharedCalendar": "Shared calendar", - "shareMyCalendars": "Share my calendars", - "notSharedYet": "Not shared yet", - "linkShare": "Link share", - "pending": "Pending", - "removeShare": "Remove share", - "confirmRemoveShare": "Really remove this share?", - "addPerson": "Add person", - "share": "Share", - "sharing": "Sharing...", - "permission": { - "read": "Read", - "write": "Read & Edit", - "admin": "Admin" - }, - "form": { - "calendar": "Calendar", - "email": "Email address", - "permission": "Permission" - } - } -} diff --git a/apps/calendar/apps/web-archived/src/lib/i18n/locales/es.json b/apps/calendar/apps/web-archived/src/lib/i18n/locales/es.json deleted file mode 100644 index 45c7809fc..000000000 --- a/apps/calendar/apps/web-archived/src/lib/i18n/locales/es.json +++ /dev/null @@ -1,275 +0,0 @@ -{ - "app": { - "name": "Calendario", - "loading": "Cargando..." - }, - "nav": { - "calendar": "Calendario", - "calendars": "Calendarios", - "agenda": "Agenda", - "settings": "Configuración", - "feedback": "Comentarios" - }, - "views": { - "day": "Día", - "5day": "5 días", - "week": "Semana", - "10day": "10 días", - "14day": "14 días", - "month": "Mes", - "year": "Año", - "agenda": "Agenda", - "weekdaysOnly": "Solo días laborables", - "allDay": "Todo el día", - "birthday": "Cumpleaños", - "weekView": "Vista semanal", - "monthView": "Vista mensual" - }, - "calendar": { - "today": "Hoy", - "newEvent": "Nuevo evento", - "noEvents": "Sin eventos", - "allDay": "Todo el día", - "myCalendars": "Mis calendarios", - "sharedCalendars": "Calendarios compartidos", - "untitled": "Sin título", - "contextMenu": { - "edit": "Editar", - "duplicate": "Duplicar", - "copy": "Copia", - "delete": "Eliminar" - } - }, - "event": { - "title": "Título", - "description": "Descripción", - "location": "Ubicación", - "start": "Inicio", - "end": "Fin", - "allDay": "Todo el día", - "repeat": "Repetir", - "reminder": "Recordatorio", - "calendar": "Calendario", - "save": "Guardar", - "delete": "Eliminar", - "cancel": "Cancelar" - }, - "repeat": { - "none": "No repetir", - "daily": "Diario", - "weekly": "Semanal", - "monthly": "Mensual", - "yearly": "Anual" - }, - "reminder": { - "atTime": "Al momento del evento", - "5min": "5 minutos antes", - "15min": "15 minutos antes", - "30min": "30 minutos antes", - "1hour": "1 hora antes", - "1day": "1 día antes" - }, - "share": { - "share": "Compartir", - "shareCalendar": "Compartir calendario", - "permissions": "Permisos", - "read": "Solo lectura", - "write": "Puede editar", - "admin": "Administrador", - "pending": "Pendiente", - "accepted": "Aceptado" - }, - "auth": { - "login": "Iniciar sesión", - "logout": "Cerrar sesión", - "register": "Registrarse", - "email": "Correo electrónico", - "password": "Contraseña", - "forgotPassword": "¿Olvidaste tu contraseña?" - }, - "toast": { - "eventLoadError": "No se pudieron cargar los eventos", - "eventUpdateError": "No se pudo actualizar el evento", - "eventDeleteError": "No se pudo eliminar el evento", - "eventDeleted": "Evento eliminado", - "error": "Error", - "calendarShared": "Calendario compartido con {email}", - "shareLinkCreated": "Enlace de compartir creado", - "inviteAccepted": "Invitación aceptada", - "shareRemoved": "Compartir eliminado", - "shareError": "Error al compartir", - "updateError": "Error al actualizar", - "removeError": "Error al eliminar", - "declineError": "Error al rechazar", - "calendarConnected": "{name} conectado", - "calendarDisconnected": "{name} desconectado", - "syncCompleted": "Sincronización completada", - "connectionError": "Error de conexión", - "syncError": "Error de sincronización" - }, - "common": { - "save": "Guardar", - "cancel": "Cancelar", - "delete": "Eliminar", - "edit": "Editar", - "add": "Agregar", - "close": "Cerrar", - "search": "Buscar", - "error": "Error", - "success": "Éxito", - "calendar": "Calendario", - "create": "Crear" - }, - "settings": { - "myCalendars": "Mis calendarios", - "externalCalendars": "Calendarios externos", - "shares": "Compartir calendarios", - "appSettings": "Configuración de la app", - "appSettingsDesc": "Estos ajustes se sincronizan con todas las apps de Mana", - "calendarView": "Vista del calendario", - "events": "Eventos", - "birthdays": "Cumpleaños", - "account": "Cuenta", - "newCalendar": "Nuevo calendario", - "calendarName": "Nombre del calendario", - "name": "Nombre", - "color": "Color", - "default": "Predeterminado", - "setAsDefault": "Establecer como calendario predeterminado", - "currentDefault": "predeterminado actual", - "noCalendars": "No hay calendarios disponibles", - "calendarCreated": "Calendario creado", - "calendarUpdated": "Calendario actualizado", - "calendarDeleted": "Calendario eliminado", - "confirmDeleteCalendar": "¿Seguro que quieres eliminar \"{name}\"?", - "externalCalendarsDesc": "Conecta Google Calendar, Apple Calendar, CalDAV o URLs de iCal.", - "manageSync": "Gestionar sincronización", - "sharesDesc": "Comparte calendarios con otros usuarios o gestiona invitaciones.", - "manageShares": "Gestionar compartidos", - "defaultView": "Vista predeterminada", - "defaultViewDesc": "Vista al abrir el calendario", - "selectView": "Seleccionar vista", - "viewWeek": "Semana", - "viewMonth": "Mes", - "viewAgenda": "Agenda", - "timeFormat": "Formato de hora", - "timeFormatDesc": "Cómo se muestran las horas", - "weekdaysOnly": "Mostrar solo días laborables", - "weekdaysOnlyDesc": "Ocultar fines de semana en la vista del calendario", - "showWeekNumbers": "Mostrar números de semana", - "showWeekNumbersDesc": "Mostrar el número de semana en la vista del calendario", - "filterHours": "Filtrar horas", - "filterHoursDesc": "Mostrar solo ciertas horas en la vista de día/semana", - "visibleHours": "Horas visibles", - "visibleHoursDesc": "Rango horario mostrado en la vista del calendario", - "hoursFrom": "Desde", - "hoursTo": "Hasta", - "allDayEvents": "Eventos de todo el día", - "allDayEventsDesc": "¿Cómo se deben mostrar los eventos de todo el día?", - "allDayInHeader": "En encabezado", - "allDayAsBlock": "Como bloque diario", - "defaultDuration": "Duración predeterminada", - "defaultDurationDesc": "Duración predeterminada para nuevos eventos", - "selectDuration": "Seleccionar duración", - "durationMinutes": "{count} minutos", - "durationHours": "{count} hora(s)", - "defaultReminder": "Recordatorio predeterminado", - "defaultReminderDesc": "Recordatorio predeterminado para nuevos eventos", - "selectReminder": "Seleccionar recordatorio", - "reminderNone": "Ninguno", - "reminderMinutes": "{count} minutos", - "reminderHour": "1 hora", - "reminderDay": "1 día", - "showBirthdays": "Mostrar cumpleaños", - "showBirthdaysDesc": "Mostrar cumpleaños de contactos en el calendario", - "showAge": "Mostrar edad", - "showAgeDesc": "Mostrar la edad de la persona en los cumpleaños" - }, - "error": { - "notFound": "Página no encontrada", - "backToHome": "Volver al inicio" - }, - "sync": { - "pageTitle": "Sincronización de calendarios - Configuración", - "title": "Sincronización", - "back": "Volver", - "connectCalendar": "Conectar calendario", - "description": "Conecta calendarios externos para importar y sincronizar eventos.", - "emptyState": "No hay calendarios externos conectados", - "syncNow": "Sincronizar ahora", - "disconnect": "Desconectar", - "confirmDisconnect": "¿Realmente desconectar \"{name}\"? Los eventos sincronizados serán eliminados.", - "neverSynced": "Nunca", - "directionLabel": "Dirección", - "lastSync": "Última sincronización", - "statusLabel": "Estado", - "autoSync": "Sincronización automática", - "connectCaldav": "Conectar servidor CalDAV", - "connectProvider": "Conectar {provider}", - "searching": "Buscando...", - "searchCalendars": "Buscar calendarios", - "discoveredCalendars": "Calendarios encontrados:", - "connecting": "Conectando...", - "connect": "Conectar", - "direction": { - "import": "Solo importar", - "export": "Solo exportar", - "both": "Bidireccional" - }, - "status": { - "error": "Error", - "active": "Activo (cada {interval} min.)", - "paused": "Pausado" - }, - "providers": { - "icalUrl": "URL iCal", - "icalUrlDesc": "Importar enlace ICS (ej. festivos)", - "caldav": "CalDAV", - "caldavDesc": "Conectar servidor CalDAV", - "google": "Google Calendar", - "googleDesc": "Sincronizar con Google Calendar", - "apple": "Apple Calendar", - "appleDesc": "Conectar calendario de iCloud" - }, - "form": { - "serverUrl": "URL del servidor", - "username": "Nombre de usuario", - "password": "Contraseña", - "name": "Nombre", - "namePlaceholder": "Mi calendario externo", - "url": "URL", - "syncDirection": "Dirección de sincronización" - } - }, - "sharing": { - "pageTitle": "Compartir calendarios - Configuración", - "title": "Compartidos", - "back": "Volver", - "shareCalendar": "Compartir calendario", - "invitations": "Invitaciones ({count})", - "calendarInvitation": "Invitación de calendario", - "access": "acceso", - "accept": "Aceptar", - "sharedWithMe": "Compartidos conmigo", - "sharedCalendar": "Calendario compartido", - "shareMyCalendars": "Compartir mis calendarios", - "notSharedYet": "Aún no compartido", - "linkShare": "Compartir por enlace", - "pending": "Pendiente", - "removeShare": "Eliminar compartido", - "confirmRemoveShare": "¿Realmente eliminar este compartido?", - "addPerson": "Agregar persona", - "share": "Compartir", - "sharing": "Compartiendo...", - "permission": { - "read": "Lectura", - "write": "Lectura y edición", - "admin": "Administrador" - }, - "form": { - "calendar": "Calendario", - "email": "Dirección de correo", - "permission": "Permiso" - } - } -} diff --git a/apps/calendar/apps/web-archived/src/lib/i18n/locales/fr.json b/apps/calendar/apps/web-archived/src/lib/i18n/locales/fr.json deleted file mode 100644 index 68db567da..000000000 --- a/apps/calendar/apps/web-archived/src/lib/i18n/locales/fr.json +++ /dev/null @@ -1,275 +0,0 @@ -{ - "app": { - "name": "Calendrier", - "loading": "Chargement..." - }, - "nav": { - "calendar": "Calendrier", - "calendars": "Calendriers", - "agenda": "Agenda", - "settings": "Paramètres", - "feedback": "Commentaires" - }, - "views": { - "day": "Jour", - "5day": "5 jours", - "week": "Semaine", - "10day": "10 jours", - "14day": "14 jours", - "month": "Mois", - "year": "Année", - "agenda": "Agenda", - "weekdaysOnly": "Jours ouvrables", - "allDay": "Toute la journée", - "birthday": "Anniversaire", - "weekView": "Vue semaine", - "monthView": "Vue mois" - }, - "calendar": { - "today": "Aujourd'hui", - "newEvent": "Nouvel événement", - "noEvents": "Aucun événement", - "allDay": "Toute la journée", - "myCalendars": "Mes calendriers", - "sharedCalendars": "Calendriers partagés", - "untitled": "Sans titre", - "contextMenu": { - "edit": "Modifier", - "duplicate": "Dupliquer", - "copy": "Copie", - "delete": "Supprimer" - } - }, - "event": { - "title": "Titre", - "description": "Description", - "location": "Lieu", - "start": "Début", - "end": "Fin", - "allDay": "Toute la journée", - "repeat": "Répéter", - "reminder": "Rappel", - "calendar": "Calendrier", - "save": "Enregistrer", - "delete": "Supprimer", - "cancel": "Annuler" - }, - "repeat": { - "none": "Ne pas répéter", - "daily": "Quotidien", - "weekly": "Hebdomadaire", - "monthly": "Mensuel", - "yearly": "Annuel" - }, - "reminder": { - "atTime": "Au moment de l'événement", - "5min": "5 minutes avant", - "15min": "15 minutes avant", - "30min": "30 minutes avant", - "1hour": "1 heure avant", - "1day": "1 jour avant" - }, - "share": { - "share": "Partager", - "shareCalendar": "Partager le calendrier", - "permissions": "Autorisations", - "read": "Lecture seule", - "write": "Modification", - "admin": "Administrateur", - "pending": "En attente", - "accepted": "Accepté" - }, - "auth": { - "login": "Connexion", - "logout": "Déconnexion", - "register": "Inscription", - "email": "E-mail", - "password": "Mot de passe", - "forgotPassword": "Mot de passe oublié?" - }, - "toast": { - "eventLoadError": "Impossible de charger les événements", - "eventUpdateError": "Impossible de mettre à jour l'événement", - "eventDeleteError": "Impossible de supprimer l'événement", - "eventDeleted": "Événement supprimé", - "error": "Erreur", - "calendarShared": "Calendrier partagé avec {email}", - "shareLinkCreated": "Lien de partage créé", - "inviteAccepted": "Invitation acceptée", - "shareRemoved": "Partage supprimé", - "shareError": "Échec du partage", - "updateError": "Échec de la mise à jour", - "removeError": "Échec de la suppression", - "declineError": "Échec du refus", - "calendarConnected": "{name} connecté", - "calendarDisconnected": "{name} déconnecté", - "syncCompleted": "Synchronisation terminée", - "connectionError": "Échec de la connexion", - "syncError": "Échec de la synchronisation" - }, - "common": { - "save": "Enregistrer", - "cancel": "Annuler", - "delete": "Supprimer", - "edit": "Modifier", - "add": "Ajouter", - "close": "Fermer", - "search": "Rechercher", - "error": "Erreur", - "success": "Succès", - "calendar": "Calendrier", - "create": "Créer" - }, - "settings": { - "myCalendars": "Mes calendriers", - "externalCalendars": "Calendriers externes", - "shares": "Partage de calendriers", - "appSettings": "Paramètres de l'app", - "appSettingsDesc": "Ces paramètres sont synchronisés avec toutes les apps Mana", - "calendarView": "Vue du calendrier", - "events": "Événements", - "birthdays": "Anniversaires", - "account": "Compte", - "newCalendar": "Nouveau calendrier", - "calendarName": "Nom du calendrier", - "name": "Nom", - "color": "Couleur", - "default": "Par défaut", - "setAsDefault": "Définir comme calendrier par défaut", - "currentDefault": "par défaut actuel", - "noCalendars": "Aucun calendrier disponible", - "calendarCreated": "Calendrier créé", - "calendarUpdated": "Calendrier mis à jour", - "calendarDeleted": "Calendrier supprimé", - "confirmDeleteCalendar": "Voulez-vous vraiment supprimer \"{name}\" ?", - "externalCalendarsDesc": "Connectez Google Calendar, Apple Calendar, CalDAV ou des URLs iCal.", - "manageSync": "Gérer la synchronisation", - "sharesDesc": "Partagez des calendriers avec d'autres utilisateurs ou gérez les invitations.", - "manageShares": "Gérer les partages", - "defaultView": "Vue par défaut", - "defaultViewDesc": "Vue à l'ouverture du calendrier", - "selectView": "Choisir la vue", - "viewWeek": "Semaine", - "viewMonth": "Mois", - "viewAgenda": "Agenda", - "timeFormat": "Format de l'heure", - "timeFormatDesc": "Affichage des heures", - "weekdaysOnly": "Afficher uniquement les jours ouvrables", - "weekdaysOnlyDesc": "Masquer les week-ends dans la vue du calendrier", - "showWeekNumbers": "Afficher les numéros de semaine", - "showWeekNumbersDesc": "Afficher le numéro de semaine dans la vue du calendrier", - "filterHours": "Filtrer les heures", - "filterHoursDesc": "Afficher uniquement certaines heures dans la vue jour/semaine", - "visibleHours": "Heures visibles", - "visibleHoursDesc": "Plage horaire affichée dans la vue du calendrier", - "hoursFrom": "De", - "hoursTo": "À", - "allDayEvents": "Événements sur toute la journée", - "allDayEventsDesc": "Comment afficher les événements sur toute la journée ?", - "allDayInHeader": "Dans l'en-tête", - "allDayAsBlock": "En bloc journalier", - "defaultDuration": "Durée par défaut", - "defaultDurationDesc": "Durée par défaut pour les nouveaux événements", - "selectDuration": "Choisir la durée", - "durationMinutes": "{count} minutes", - "durationHours": "{count} heure(s)", - "defaultReminder": "Rappel par défaut", - "defaultReminderDesc": "Rappel par défaut pour les nouveaux événements", - "selectReminder": "Choisir le rappel", - "reminderNone": "Aucun", - "reminderMinutes": "{count} minutes", - "reminderHour": "1 heure", - "reminderDay": "1 jour", - "showBirthdays": "Afficher les anniversaires", - "showBirthdaysDesc": "Afficher les anniversaires des contacts dans le calendrier", - "showAge": "Afficher l'âge", - "showAgeDesc": "Afficher l'âge de la personne lors des anniversaires" - }, - "error": { - "notFound": "Page non trouvée", - "backToHome": "Retour à l'accueil" - }, - "sync": { - "pageTitle": "Sync des calendriers - Paramètres", - "title": "Sync des calendriers", - "back": "Retour", - "connectCalendar": "Connecter un calendrier", - "description": "Connectez des calendriers externes pour importer et synchroniser des événements.", - "emptyState": "Aucun calendrier externe connecté", - "syncNow": "Synchroniser maintenant", - "disconnect": "Déconnecter", - "confirmDisconnect": "Vraiment déconnecter \"{name}\" ? Les événements synchronisés seront supprimés.", - "neverSynced": "Jamais", - "directionLabel": "Direction", - "lastSync": "Dernière sync", - "statusLabel": "Statut", - "autoSync": "Sync auto", - "connectCaldav": "Connecter un serveur CalDAV", - "connectProvider": "Connecter {provider}", - "searching": "Recherche...", - "searchCalendars": "Rechercher des calendriers", - "discoveredCalendars": "Calendriers trouvés :", - "connecting": "Connexion...", - "connect": "Connecter", - "direction": { - "import": "Import uniquement", - "export": "Export uniquement", - "both": "Bidirectionnel" - }, - "status": { - "error": "Erreur", - "active": "Actif (toutes les {interval} min.)", - "paused": "En pause" - }, - "providers": { - "icalUrl": "URL iCal", - "icalUrlDesc": "Importer un lien ICS (ex. jours fériés)", - "caldav": "CalDAV", - "caldavDesc": "Connecter un serveur CalDAV", - "google": "Google Calendar", - "googleDesc": "Synchroniser avec Google Agenda", - "apple": "Apple Calendar", - "appleDesc": "Connecter le calendrier iCloud" - }, - "form": { - "serverUrl": "URL du serveur", - "username": "Nom d'utilisateur", - "password": "Mot de passe", - "name": "Nom", - "namePlaceholder": "Mon calendrier externe", - "url": "URL", - "syncDirection": "Direction de sync" - } - }, - "sharing": { - "pageTitle": "Partage de calendriers - Paramètres", - "title": "Partages", - "back": "Retour", - "shareCalendar": "Partager le calendrier", - "invitations": "Invitations ({count})", - "calendarInvitation": "Invitation de calendrier", - "access": "accès", - "accept": "Accepter", - "sharedWithMe": "Partagés avec moi", - "sharedCalendar": "Calendrier partagé", - "shareMyCalendars": "Partager mes calendriers", - "notSharedYet": "Pas encore partagé", - "linkShare": "Partage par lien", - "pending": "En attente", - "removeShare": "Supprimer le partage", - "confirmRemoveShare": "Vraiment supprimer ce partage ?", - "addPerson": "Ajouter une personne", - "share": "Partager", - "sharing": "Partage...", - "permission": { - "read": "Lecture", - "write": "Lecture & modification", - "admin": "Administrateur" - }, - "form": { - "calendar": "Calendrier", - "email": "Adresse e-mail", - "permission": "Autorisation" - } - } -} diff --git a/apps/calendar/apps/web-archived/src/lib/i18n/locales/it.json b/apps/calendar/apps/web-archived/src/lib/i18n/locales/it.json deleted file mode 100644 index 7f4c648b1..000000000 --- a/apps/calendar/apps/web-archived/src/lib/i18n/locales/it.json +++ /dev/null @@ -1,275 +0,0 @@ -{ - "app": { - "name": "Calendario", - "loading": "Caricamento..." - }, - "nav": { - "calendar": "Calendario", - "calendars": "Calendari", - "agenda": "Agenda", - "settings": "Impostazioni", - "feedback": "Feedback" - }, - "views": { - "day": "Giorno", - "5day": "5 giorni", - "week": "Settimana", - "10day": "10 giorni", - "14day": "14 giorni", - "month": "Mese", - "year": "Anno", - "agenda": "Agenda", - "weekdaysOnly": "Solo giorni feriali", - "allDay": "Tutto il giorno", - "birthday": "Compleanno", - "weekView": "Vista settimanale", - "monthView": "Vista mensile" - }, - "calendar": { - "today": "Oggi", - "newEvent": "Nuovo evento", - "noEvents": "Nessun evento", - "allDay": "Tutto il giorno", - "myCalendars": "I miei calendari", - "sharedCalendars": "Calendari condivisi", - "untitled": "Senza titolo", - "contextMenu": { - "edit": "Modifica", - "duplicate": "Duplica", - "copy": "Copia", - "delete": "Elimina" - } - }, - "event": { - "title": "Titolo", - "description": "Descrizione", - "location": "Luogo", - "start": "Inizio", - "end": "Fine", - "allDay": "Tutto il giorno", - "repeat": "Ripeti", - "reminder": "Promemoria", - "calendar": "Calendario", - "save": "Salva", - "delete": "Elimina", - "cancel": "Annulla" - }, - "repeat": { - "none": "Non ripetere", - "daily": "Giornaliero", - "weekly": "Settimanale", - "monthly": "Mensile", - "yearly": "Annuale" - }, - "reminder": { - "atTime": "All'ora dell'evento", - "5min": "5 minuti prima", - "15min": "15 minuti prima", - "30min": "30 minuti prima", - "1hour": "1 ora prima", - "1day": "1 giorno prima" - }, - "share": { - "share": "Condividi", - "shareCalendar": "Condividi calendario", - "permissions": "Autorizzazioni", - "read": "Solo lettura", - "write": "Può modificare", - "admin": "Amministratore", - "pending": "In attesa", - "accepted": "Accettato" - }, - "auth": { - "login": "Accedi", - "logout": "Esci", - "register": "Registrati", - "email": "Email", - "password": "Password", - "forgotPassword": "Password dimenticata?" - }, - "toast": { - "eventLoadError": "Impossibile caricare gli eventi", - "eventUpdateError": "Impossibile aggiornare l'evento", - "eventDeleteError": "Impossibile eliminare l'evento", - "eventDeleted": "Evento eliminato", - "error": "Errore", - "calendarShared": "Calendario condiviso con {email}", - "shareLinkCreated": "Link di condivisione creato", - "inviteAccepted": "Invito accettato", - "shareRemoved": "Condivisione rimossa", - "shareError": "Condivisione fallita", - "updateError": "Aggiornamento fallito", - "removeError": "Rimozione fallita", - "declineError": "Rifiuto fallito", - "calendarConnected": "{name} connesso", - "calendarDisconnected": "{name} disconnesso", - "syncCompleted": "Sincronizzazione completata", - "connectionError": "Connessione fallita", - "syncError": "Sincronizzazione fallita" - }, - "common": { - "save": "Salva", - "cancel": "Annulla", - "delete": "Elimina", - "edit": "Modifica", - "add": "Aggiungi", - "close": "Chiudi", - "search": "Cerca", - "error": "Errore", - "success": "Successo", - "calendar": "Calendario", - "create": "Crea" - }, - "settings": { - "myCalendars": "I miei calendari", - "externalCalendars": "Calendari esterni", - "shares": "Condivisione calendari", - "appSettings": "Impostazioni app", - "appSettingsDesc": "Queste impostazioni vengono sincronizzate con tutte le app Mana", - "calendarView": "Vista del calendario", - "events": "Eventi", - "birthdays": "Compleanni", - "account": "Account", - "newCalendar": "Nuovo calendario", - "calendarName": "Nome del calendario", - "name": "Nome", - "color": "Colore", - "default": "Predefinito", - "setAsDefault": "Imposta come calendario predefinito", - "currentDefault": "predefinito attuale", - "noCalendars": "Nessun calendario disponibile", - "calendarCreated": "Calendario creato", - "calendarUpdated": "Calendario aggiornato", - "calendarDeleted": "Calendario eliminato", - "confirmDeleteCalendar": "Vuoi davvero eliminare \"{name}\"?", - "externalCalendarsDesc": "Collega Google Calendar, Apple Calendar, CalDAV o URL iCal.", - "manageSync": "Gestisci sincronizzazione", - "sharesDesc": "Condividi calendari con altri utenti o gestisci gli inviti.", - "manageShares": "Gestisci condivisioni", - "defaultView": "Vista predefinita", - "defaultViewDesc": "Vista all'apertura del calendario", - "selectView": "Seleziona vista", - "viewWeek": "Settimana", - "viewMonth": "Mese", - "viewAgenda": "Agenda", - "timeFormat": "Formato ora", - "timeFormatDesc": "Come vengono visualizzati gli orari", - "weekdaysOnly": "Mostra solo giorni feriali", - "weekdaysOnlyDesc": "Nascondi i fine settimana nella vista del calendario", - "showWeekNumbers": "Mostra numeri di settimana", - "showWeekNumbersDesc": "Mostra il numero della settimana nella vista del calendario", - "filterHours": "Filtra ore", - "filterHoursDesc": "Mostra solo determinate ore nella vista giorno/settimana", - "visibleHours": "Ore visibili", - "visibleHoursDesc": "Intervallo orario visualizzato nella vista del calendario", - "hoursFrom": "Da", - "hoursTo": "A", - "allDayEvents": "Eventi tutto il giorno", - "allDayEventsDesc": "Come devono essere visualizzati gli eventi tutto il giorno?", - "allDayInHeader": "Nell'intestazione", - "allDayAsBlock": "Come blocco giornaliero", - "defaultDuration": "Durata predefinita", - "defaultDurationDesc": "Durata predefinita per i nuovi eventi", - "selectDuration": "Seleziona durata", - "durationMinutes": "{count} minuti", - "durationHours": "{count} ora/e", - "defaultReminder": "Promemoria predefinito", - "defaultReminderDesc": "Promemoria predefinito per i nuovi eventi", - "selectReminder": "Seleziona promemoria", - "reminderNone": "Nessuno", - "reminderMinutes": "{count} minuti", - "reminderHour": "1 ora", - "reminderDay": "1 giorno", - "showBirthdays": "Mostra compleanni", - "showBirthdaysDesc": "Mostra i compleanni dei contatti nel calendario", - "showAge": "Mostra età", - "showAgeDesc": "Mostra l'età della persona nei compleanni" - }, - "error": { - "notFound": "Pagina non trovata", - "backToHome": "Torna alla home" - }, - "sync": { - "pageTitle": "Sincronizzazione calendari - Impostazioni", - "title": "Sincronizzazione", - "back": "Indietro", - "connectCalendar": "Collega calendario", - "description": "Collega calendari esterni per importare e sincronizzare eventi.", - "emptyState": "Nessun calendario esterno collegato", - "syncNow": "Sincronizza ora", - "disconnect": "Disconnetti", - "confirmDisconnect": "Disconnettere davvero \"{name}\"? Gli eventi sincronizzati verranno eliminati.", - "neverSynced": "Mai", - "directionLabel": "Direzione", - "lastSync": "Ultima sincronizzazione", - "statusLabel": "Stato", - "autoSync": "Sincronizzazione automatica", - "connectCaldav": "Collega server CalDAV", - "connectProvider": "Collega {provider}", - "searching": "Ricerca...", - "searchCalendars": "Cerca calendari", - "discoveredCalendars": "Calendari trovati:", - "connecting": "Collegamento...", - "connect": "Collega", - "direction": { - "import": "Solo importazione", - "export": "Solo esportazione", - "both": "Bidirezionale" - }, - "status": { - "error": "Errore", - "active": "Attivo (ogni {interval} min.)", - "paused": "In pausa" - }, - "providers": { - "icalUrl": "URL iCal", - "icalUrlDesc": "Importa link ICS (es. festività)", - "caldav": "CalDAV", - "caldavDesc": "Collega server CalDAV", - "google": "Google Calendar", - "googleDesc": "Sincronizza con Google Calendar", - "apple": "Apple Calendar", - "appleDesc": "Collega calendario iCloud" - }, - "form": { - "serverUrl": "URL del server", - "username": "Nome utente", - "password": "Password", - "name": "Nome", - "namePlaceholder": "Il mio calendario esterno", - "url": "URL", - "syncDirection": "Direzione di sincronizzazione" - } - }, - "sharing": { - "pageTitle": "Condivisione calendari - Impostazioni", - "title": "Condivisioni", - "back": "Indietro", - "shareCalendar": "Condividi calendario", - "invitations": "Inviti ({count})", - "calendarInvitation": "Invito calendario", - "access": "accesso", - "accept": "Accetta", - "sharedWithMe": "Condivisi con me", - "sharedCalendar": "Calendario condiviso", - "shareMyCalendars": "Condividi i miei calendari", - "notSharedYet": "Non ancora condiviso", - "linkShare": "Condivisione tramite link", - "pending": "In attesa", - "removeShare": "Rimuovi condivisione", - "confirmRemoveShare": "Rimuovere davvero questa condivisione?", - "addPerson": "Aggiungi persona", - "share": "Condividi", - "sharing": "Condivisione...", - "permission": { - "read": "Lettura", - "write": "Lettura e modifica", - "admin": "Amministratore" - }, - "form": { - "calendar": "Calendario", - "email": "Indirizzo email", - "permission": "Autorizzazione" - } - } -} diff --git a/apps/calendar/apps/web-archived/src/lib/services/feedback.ts b/apps/calendar/apps/web-archived/src/lib/services/feedback.ts deleted file mode 100644 index e4032e017..000000000 --- a/apps/calendar/apps/web-archived/src/lib/services/feedback.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Feedback Service Instance for Calendar Web App - */ - -import { browser } from '$app/environment'; -import { createFeedbackService } from '@manacore/feedback'; -import { authStore } from '$lib/stores/auth.svelte'; - -// Get auth URL dynamically at runtime -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - return 'http://localhost:3001'; -} - -export const feedbackService = createFeedbackService({ - apiUrl: getAuthUrl(), - appId: 'calendar', - getAuthToken: async () => authStore.getAccessToken(), -}); diff --git a/apps/calendar/apps/web-archived/src/lib/services/stt.ts b/apps/calendar/apps/web-archived/src/lib/services/stt.ts deleted file mode 100644 index 037865503..000000000 --- a/apps/calendar/apps/web-archived/src/lib/services/stt.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Speech-to-Text (STT) Service Client - * - * Communicates with the mana-stt service for audio transcription. - */ - -import { browser } from '$app/environment'; - -/** - * STT service URL - uses runtime injection for production, env var for dev - */ -function getSttUrl(): string { - if (!browser) return 'http://localhost:3020'; - // Check runtime-injected variable first (production Docker) - const runtimeUrl = (window as any).__PUBLIC_STT_URL__; - if (runtimeUrl) return runtimeUrl; - // Fall back to build-time env var or default - return import.meta.env.PUBLIC_STT_URL || 'https://stt-api.mana.how'; -} - -const STT_URL = getSttUrl(); - -export interface TranscriptionResult { - /** The transcribed text */ - text: string; - /** Detected or specified language */ - language: string; - /** Model used for transcription */ - model: string; -} - -export interface TranscriptionError { - message: string; - code?: string; -} - -export type TranscriptionResponse = - | { success: true; data: TranscriptionResult } - | { success: false; error: TranscriptionError }; - -/** - * Transcribe audio using the mana-stt service - * - * @param audioBlob - The audio blob to transcribe - * @param language - Optional language code ('de', 'en', etc.) or 'auto' for auto-detection - * @returns The transcription result or error - */ -export async function transcribeAudio( - audioBlob: Blob, - language?: string -): Promise { - try { - const formData = new FormData(); - - // Determine file extension based on MIME type - const mimeType = audioBlob.type || 'audio/webm'; - let extension = 'webm'; - if (mimeType.includes('ogg')) extension = 'ogg'; - else if (mimeType.includes('mp4')) extension = 'mp4'; - else if (mimeType.includes('mpeg') || mimeType.includes('mp3')) extension = 'mp3'; - - formData.append('file', audioBlob, `recording.${extension}`); - - // Add language parameter if specified (and not 'auto') - if (language && language !== 'auto') { - formData.append('language', language); - } - - const response = await fetch(`${STT_URL}/transcribe`, { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - let errorMessage = 'Transcription failed'; - try { - const errorData = await response.json(); - errorMessage = errorData.error || errorData.message || errorMessage; - } catch { - errorMessage = `HTTP ${response.status}: ${response.statusText}`; - } - - return { - success: false, - error: { - message: errorMessage, - code: `HTTP_${response.status}`, - }, - }; - } - - const data = await response.json(); - - // Handle empty transcription - if (!data.text || data.text.trim() === '') { - return { - success: false, - error: { - message: 'Keine Sprache erkannt. Bitte erneut versuchen.', - code: 'EMPTY_TRANSCRIPTION', - }, - }; - } - - return { - success: true, - data: { - text: data.text.trim(), - language: data.language || language || 'auto', - model: data.model || 'unknown', - }, - }; - } catch (error) { - // Handle network errors - if (error instanceof TypeError && error.message.includes('fetch')) { - return { - success: false, - error: { - message: 'Spracherkennung nicht verfügbar', - code: 'NETWORK_ERROR', - }, - }; - } - - return { - success: false, - error: { - message: error instanceof Error ? error.message : 'Unknown error', - code: 'UNKNOWN_ERROR', - }, - }; - } -} - -/** - * Check if the STT service is available - */ -export async function checkSttServiceHealth(): Promise { - try { - const response = await fetch(`${STT_URL}/health`, { - method: 'GET', - signal: AbortSignal.timeout(5000), // 5 second timeout - }); - return response.ok; - } catch { - return false; - } -} diff --git a/apps/calendar/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts deleted file mode 100644 index f5df717a9..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { createAppOnboardingStore, type AppOnboardingStep } from '@manacore/shared-app-onboarding'; -import { userSettings } from './user-settings.svelte'; -import { settingsStore } from './settings.svelte'; -import type { CalendarViewType } from '@calendar/shared'; - -/** - * Calendar-specific onboarding steps - */ -const calendarOnboardingSteps: AppOnboardingStep[] = [ - { - id: 'features', - type: 'info', - question: 'Willkommen bei Kalender!', - description: 'Das kann Kalender für dich tun:', - emoji: '📅', - gradient: { from: 'blue-500', to: 'blue-700' }, - bullets: [ - 'Termine erstellen & verwalten', - 'Wochen-, Monats- & Agenda-Ansicht', - 'Schnelleingabe per Text', - 'Drag & Drop zum Verschieben', - ], - }, - { - id: 'weekStart', - type: 'select', - question: 'Wann beginnt deine Woche?', - description: 'Diese Einstellung bestimmt die Anordnung deiner Kalenderansicht.', - emoji: '📅', - gradient: { from: 'blue-500', to: 'blue-700' }, - options: [ - { - id: 'monday', - label: 'Montag', - description: 'Europäischer Standard', - emoji: '1️⃣', - }, - { - id: 'sunday', - label: 'Sonntag', - description: 'Amerikanischer Standard', - emoji: '7️⃣', - }, - ], - defaultValue: 'monday', - }, - { - id: 'defaultView', - type: 'select', - question: 'Welche Ansicht bevorzugst du?', - description: 'Du kannst die Ansicht jederzeit in der App wechseln.', - emoji: '👁️', - gradient: { from: 'indigo-500', to: 'indigo-700' }, - options: [ - { - id: 'week', - label: 'Wochenansicht', - description: '7-Tage-Übersicht (Empfohlen)', - emoji: '🗓️', - }, - { - id: 'month', - label: 'Monatsansicht', - description: 'Kompakte Monatsübersicht', - emoji: '📅', - }, - { - id: 'agenda', - label: 'Agenda', - description: 'Chronologische Terminliste', - emoji: '📋', - }, - ], - defaultValue: 'week', - }, - { - id: 'welcome', - type: 'info', - question: 'Dein Kalender ist bereit!', - description: 'Hier sind einige Tipps für den Start:', - emoji: '🎉', - gradient: { from: 'primary', to: 'primary/70' }, - bullets: [ - 'Nutze die Schnelleingabe unten, um Termine per Text zu erstellen', - 'Drücke "F" für den Fokus-Modus ohne Ablenkungen', - 'Pfeiltasten navigieren zwischen Tagen/Wochen', - 'Ziehe Termine per Drag & Drop auf neue Zeiten', - ], - }, -]; - -/** - * Calendar app onboarding store - * - * Usage in components: - * ```svelte - * - * - * {#if calendarOnboarding.shouldShow} - * - * {/if} - * ``` - */ -export const calendarOnboarding = createAppOnboardingStore({ - appId: 'calendar', - steps: calendarOnboardingSteps, - userSettings, - onComplete: async (preferences) => { - // Apply week start preference - if (preferences.weekStart === 'monday') { - settingsStore.set('weekStartsOn', 1); - } else if (preferences.weekStart === 'sunday') { - settingsStore.set('weekStartsOn', 0); - } - - // Apply default view preference - const viewMap: Record = { - week: 'week', - month: 'month', - agenda: 'agenda', - }; - const selectedView = preferences.defaultView as string; - if (selectedView && viewMap[selectedView]) { - settingsStore.set('defaultView', viewMap[selectedView]); - } - }, - onSkip: async () => { - // Defaults are already sensible, nothing to do - }, -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/auth.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/auth.svelte.ts deleted file mode 100644 index 56987dc6a..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/auth.svelte.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Auth Store — uses centralized Mana auth factory. - */ - -import { createManaAuthStore } from '@manacore/shared-auth-stores'; - -export const authStore = createManaAuthStore({ - devBackendPort: 3014, -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/birthdays.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/birthdays.svelte.ts deleted file mode 100644 index 443a74e18..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/birthdays.svelte.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Birthdays Store - Manages contact birthdays for calendar display - * Cross-app integration with Contacts Backend - */ - -import { browser } from '$app/environment'; -import * as api from '$lib/api/birthdays'; -import type { ContactBirthdaySummary, BirthdayEvent } from '$lib/api/birthdays'; -import { getContactDisplayName, BIRTHDAY_CALENDAR } from '$lib/api/birthdays'; -import { differenceInYears, isSameDay, isWithinInterval, parseISO, format } from 'date-fns'; - -// Re-export types for convenience -export type { ContactBirthdaySummary, BirthdayEvent }; - -// ============================================ -// State -// ============================================ - -let birthdays = $state([]); -let loading = $state(false); -let error = $state(null); -let serviceAvailable = $state(true); -let lastFetchTime = $state(0); - -// Cache settings -const CACHE_TTL = 5 * 60 * 1000; // 5 minutes - -// ============================================ -// Store Export -// ============================================ - -export const birthdaysStore = { - // ========== Getters ========== - get birthdays() { - return birthdays ?? []; - }, - get loading() { - return loading; - }, - get error() { - return error; - }, - get serviceAvailable() { - return serviceAvailable; - }, - get calendarId() { - return BIRTHDAY_CALENDAR.id; - }, - get calendarColor() { - return BIRTHDAY_CALENDAR.color; - }, - get calendarName() { - return BIRTHDAY_CALENDAR.name; - }, - - // ========== Birthday Getters ========== - - /** - * Get birthday events for a specific day - * Matches by month and day (ignores year) - */ - getBirthdaysForDay(date: Date): BirthdayEvent[] { - const currentBirthdays = birthdays ?? []; - if (!Array.isArray(currentBirthdays) || currentBirthdays.length === 0) return []; - - return currentBirthdays - .filter((contact) => { - if (!contact.birthday) return false; - const birthdayDate = parseISO(contact.birthday); - // Compare month and day only - return ( - birthdayDate.getMonth() === date.getMonth() && birthdayDate.getDate() === date.getDate() - ); - }) - .map((contact) => this.toBirthdayEvent(contact, date)); - }, - - /** - * Get birthday events within a date range - */ - getBirthdaysInRange(start: Date, end: Date): BirthdayEvent[] { - const currentBirthdays = birthdays ?? []; - if (!Array.isArray(currentBirthdays) || currentBirthdays.length === 0) return []; - - const events: BirthdayEvent[] = []; - const current = new Date(start); - - // Iterate through each day in range - while (current <= end) { - const dayBirthdays = this.getBirthdaysForDay(current); - events.push(...dayBirthdays); - current.setDate(current.getDate() + 1); - } - - return events; - }, - - /** - * Check if a specific day has any birthdays - */ - hasBirthdaysOnDay(date: Date): boolean { - const currentBirthdays = birthdays ?? []; - if (!Array.isArray(currentBirthdays)) return false; - - return currentBirthdays.some((contact) => { - if (!contact.birthday) return false; - const birthdayDate = parseISO(contact.birthday); - return ( - birthdayDate.getMonth() === date.getMonth() && birthdayDate.getDate() === date.getDate() - ); - }); - }, - - /** - * Get upcoming birthdays (next N days) - */ - getUpcomingBirthdays(days: number = 30): BirthdayEvent[] { - const start = new Date(); - const end = new Date(); - end.setDate(end.getDate() + days); - return this.getBirthdaysInRange(start, end); - }, - - /** - * Convert contact to birthday event - */ - toBirthdayEvent(contact: ContactBirthdaySummary, displayDate: Date): BirthdayEvent { - const displayName = getContactDisplayName(contact); - const birthdayDate = parseISO(contact.birthday); - const birthYear = birthdayDate.getFullYear(); - - // Calculate age (0 if year seems invalid, e.g., 1900 default) - let age = differenceInYears(displayDate, birthdayDate); - if (birthYear < 1900 || birthYear > new Date().getFullYear()) { - age = 0; // Unknown birth year - } - - const dateStr = format(displayDate, 'yyyy-MM-dd'); - - return { - id: `birthday-${contact.id}-${dateStr}`, - contactId: contact.id, - title: `${displayName}`, - displayName, - photoUrl: contact.photoUrl, - birthday: contact.birthday, - age, - startTime: displayDate.toISOString(), - endTime: displayDate.toISOString(), - isAllDay: true, - isBirthday: true, - calendarId: BIRTHDAY_CALENDAR.id, - }; - }, - - // ========== API Methods ========== - - /** - * Fetch birthdays from Contacts service - * Uses cache to avoid frequent refetches - */ - async fetchBirthdays(force = false) { - if (!browser) return; - - // Use cache if still valid - if (!force && Date.now() - lastFetchTime < CACHE_TTL && birthdays.length > 0) { - return; - } - - loading = true; - error = null; - - const result = await api.getBirthdays(); - - if (result.error) { - error = result.error.message; - serviceAvailable = false; - } else { - birthdays = result.data || []; - serviceAvailable = true; - lastFetchTime = Date.now(); - } - - loading = false; - }, - - /** - * Check if Contacts service is available - */ - async checkServiceHealth(): Promise { - const result = await api.getBirthdays(); - serviceAvailable = !result.error; - return serviceAvailable; - }, - - /** - * Clear birthdays cache - */ - clear() { - birthdays = []; - lastFetchTime = 0; - }, - - /** - * Get contact by ID from cached birthdays - */ - getContactById(id: string): ContactBirthdaySummary | undefined { - const currentBirthdays = birthdays ?? []; - if (!Array.isArray(currentBirthdays)) return undefined; - return currentBirthdays.find((c) => c.id === id); - }, - - /** - * Count of contacts with birthdays - */ - get count(): number { - return birthdays?.length ?? 0; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/calendars.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/calendars.svelte.ts deleted file mode 100644 index 14d94f7d8..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/calendars.svelte.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Calendars Store — Mutations Only - * - * Reads come from useLiveQuery (see $lib/data/queries.ts). - * This store only handles writes to IndexedDB. - */ - -import type { Calendar, CreateCalendarInput, UpdateCalendarInput } from '@calendar/shared'; -import { calendarCollection, type LocalCalendar } from '$lib/data/local-store'; -import { BIRTHDAY_CALENDAR } from '$lib/api/birthdays'; -import { settingsStore } from './settings.svelte'; -import { CalendarEvents } from '@manacore/shared-utils/analytics'; -import { toCalendar } from '$lib/data/queries'; - -// Mutation error state -let error = $state(null); - -export const calendarsStore = { - get error() { - return error; - }, - get birthdayCalendarId() { - return BIRTHDAY_CALENDAR.id; - }, - get guestCalendarId() { - return 'personal-calendar'; - }, - - /** - * Create a new calendar — writes to IndexedDB instantly. - */ - async createCalendar(data: CreateCalendarInput) { - error = null; - try { - const newLocal: LocalCalendar = { - id: crypto.randomUUID(), - name: data.name, - color: data.color ?? '#3B82F6', - isDefault: data.isDefault ?? false, - isVisible: data.isVisible ?? true, - timezone: data.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone, - }; - - const inserted = await calendarCollection.insert(newLocal); - const newCalendar = toCalendar(inserted); - CalendarEvents.calendarCreated(); - return { data: newCalendar, error: null }; - } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to create calendar'; - error = msg; - return { data: null, error: { message: msg } }; - } - }, - - /** - * Update a calendar — writes to IndexedDB instantly. - */ - async updateCalendar(id: string, data: UpdateCalendarInput) { - error = null; - try { - const updated = await calendarCollection.update(id, data as Partial); - if (updated) { - const updatedCalendar = toCalendar(updated); - return { data: updatedCalendar, error: null }; - } - return { data: null, error: null }; - } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to update calendar'; - error = msg; - return { data: null, error: { message: msg } }; - } - }, - - /** - * Delete a calendar — removes from IndexedDB instantly. - */ - async deleteCalendar(id: string) { - error = null; - try { - await calendarCollection.delete(id); - CalendarEvents.calendarDeleted(); - return { error: null }; - } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to delete calendar'; - error = msg; - return { error: { message: msg } }; - } - }, - - /** - * Toggle calendar visibility (needs current calendars from context) - */ - async toggleVisibility(id: string, calendars: Calendar[]) { - const calendar = calendars.find((c) => c.id === id); - if (!calendar) return; - return this.updateCalendar(id, { isVisible: !calendar.isVisible }); - }, - - /** - * Set a calendar as the default (needs current calendars from context) - */ - async setAsDefault(id: string, calendars: Calendar[]) { - error = null; - try { - // Remove default from all others first - for (const cal of calendars) { - if (cal.isDefault && cal.id !== id) { - await calendarCollection.update(cal.id, { isDefault: false } as Partial); - } - } - // Set the new default - const updated = await calendarCollection.update(id, { - isDefault: true, - } as Partial); - return { data: updated ? toCalendar(updated) : null, error: null }; - } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to set default'; - error = msg; - return { data: null, error: { message: msg } }; - } - }, - - /** - * Toggle birthday calendar visibility - */ - toggleBirthdaysVisibility() { - settingsStore.set('showBirthdays', !settingsStore.showBirthdays); - }, - - /** - * Check if a calendar ID is the virtual birthday calendar - */ - isBirthdayCalendar(id: string) { - return id === BIRTHDAY_CALENDAR.id; - }, - - /** - * Check if a calendar ID is the guest calendar - */ - isGuestCalendar(id: string) { - return id === 'personal-calendar'; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/contacts.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/contacts.svelte.ts deleted file mode 100644 index 35ddf5f7f..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/contacts.svelte.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Contacts Store for Calendar App - * - * Provides access to contacts from the Contacts app for event attendee management. - */ - -import { browser } from '$app/environment'; -import { createContactsClient, type ContactsClient } from '@manacore/shared-auth'; -import type { ContactSummary } from '@manacore/shared-types'; -import { authStore } from './auth.svelte'; - -// State -let client: ContactsClient | null = null; -let isAvailable = $state(null); -let isChecking = $state(false); -let lastCheck = $state(0); - -// Cache for recent search results -let searchCache = $state>(new Map()); -const CACHE_TTL = 60000; // 1 minute - -// Get contacts API URL dynamically -function getContactsApiUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_CONTACTS_API_URL__?: string }) - .__PUBLIC_CONTACTS_API_URL__; - return injectedUrl || 'http://localhost:3015/api/v1'; - } - return 'http://localhost:3015/api/v1'; -} - -// Initialize client lazily -function getClient(): ContactsClient { - if (!client) { - client = createContactsClient({ - apiUrl: getContactsApiUrl(), - getAuthToken: async () => authStore.getAccessToken(), - timeout: 5000, - }); - } - return client; -} - -export const contactsStore = { - // Getters - get isAvailable() { - return isAvailable; - }, - get isChecking() { - return isChecking; - }, - - /** - * Check if the Contacts API is available - * Caches result for 30 seconds - */ - async checkAvailability(): Promise { - const now = Date.now(); - // Skip if checked recently - if (lastCheck && now - lastCheck < 30000 && isAvailable !== null) { - return isAvailable; - } - - isChecking = true; - try { - const available = await getClient().isAvailable(); - isAvailable = available; - lastCheck = now; - return available; - } catch { - isAvailable = false; - lastCheck = now; - return false; - } finally { - isChecking = false; - } - }, - - /** - * Search contacts by query string - */ - async searchContacts(query: string): Promise { - // Check cache first - const cacheKey = query.toLowerCase().trim(); - const cached = searchCache.get(cacheKey); - if (cached && Date.now() - cached.timestamp < CACHE_TTL) { - return cached.results; - } - - // Check availability - if (isAvailable === null) { - await this.checkAvailability(); - } - - if (!isAvailable) { - return []; - } - - try { - const results = await getClient().searchContacts({ - query, - limit: 20, - excludeArchived: true, - }); - - // Cache results - searchCache.set(cacheKey, { - results, - timestamp: Date.now(), - }); - - return results; - } catch (error) { - console.error('[contactsStore] Search failed:', error); - return []; - } - }, - - /** - * Get a single contact by ID - */ - async getContact(id: string): Promise { - if (isAvailable === null) { - await this.checkAvailability(); - } - - if (!isAvailable) { - return null; - } - - try { - return await getClient().getContact(id); - } catch (error) { - console.error(`[contactsStore] Failed to get contact ${id}:`, error); - return null; - } - }, - - /** - * Get multiple contacts by IDs - */ - async getContacts(ids: string[]): Promise { - if (ids.length === 0) return []; - - if (isAvailable === null) { - await this.checkAvailability(); - } - - if (!isAvailable) { - return []; - } - - try { - return await getClient().getContacts(ids); - } catch (error) { - console.error('[contactsStore] Failed to get contacts:', error); - return []; - } - }, - - /** - * Clear the search cache - */ - clearCache() { - searchCache.clear(); - }, - - /** - * Reset availability check (force recheck on next call) - */ - resetAvailability() { - isAvailable = null; - lastCheck = 0; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/event-tags.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/event-tags.svelte.ts deleted file mode 100644 index ab21dab3c..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/event-tags.svelte.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Event Tags Store — Local-First via Shared Tag Store - */ -export { - tagMutations, - useAllTags, - getTagById, - getTagsByIds, - getTagColor, - getTagsByGroup, -} from '@manacore/shared-stores'; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/eventModal.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/eventModal.svelte.ts deleted file mode 100644 index 33ddc3496..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/eventModal.svelte.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Event Modal Store - Manages event detail modal state with URL support - */ - -let selectedEventId = $state(null); - -export const eventModalStore = { - get eventId() { - return selectedEventId; - }, - - get isOpen() { - return selectedEventId !== null; - }, - - /** - * Open the event detail modal - */ - open(eventId: string) { - selectedEventId = eventId; - // Update URL without full navigation - const url = new URL(window.location.href); - url.searchParams.set('event', eventId); - window.history.pushState({}, '', url.toString()); - }, - - /** - * Close the event detail modal - */ - close() { - selectedEventId = null; - // Remove event param from URL - const url = new URL(window.location.href); - url.searchParams.delete('event'); - window.history.pushState({}, '', url.toString()); - }, - - /** - * Initialize from URL (call on page mount) - */ - initFromUrl(searchParams: URLSearchParams) { - const eventId = searchParams.get('event'); - if (eventId) { - selectedEventId = eventId; - } - }, - - /** - * Reset state (for cleanup) - */ - reset() { - selectedEventId = null; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/events-recurrence.test.ts b/apps/calendar/apps/web-archived/src/lib/stores/events-recurrence.test.ts deleted file mode 100644 index 65ea7b69c..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/events-recurrence.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import type { CalendarEvent } from '@calendar/shared'; - -// Mock local-store to avoid import.meta.env issues in tests -vi.mock('$lib/data/local-store', () => ({ - eventCollection: {}, - calendarCollection: {}, -})); - -vi.mock('@manacore/local-store/svelte', () => ({ - useLiveQueryWithDefault: vi.fn(), -})); - -vi.mock('$lib/api/birthdays', () => ({ - BIRTHDAY_CALENDAR: { id: 'birthday-cal', name: 'Birthdays', color: '#ec4899' }, -})); - -vi.mock('@manacore/shared-ui', () => ({ - toastStore: { error: vi.fn(), success: vi.fn() }, -})); - -import { expandRecurringEvents } from '$lib/data/queries'; -import { eventsStore } from './events.svelte'; - -function makeEvent(overrides: Partial = {}): CalendarEvent { - return { - id: 'evt-1', - calendarId: 'cal-1', - userId: 'user-1', - title: 'Test', - description: null, - location: null, - startTime: '2026-03-15T10:00:00', - endTime: '2026-03-15T11:00:00', - isAllDay: false, - timezone: 'Europe/Berlin', - recurrenceRule: null, - recurrenceEndDate: null, - recurrenceExceptions: null, - parentEventId: null, - color: null, - status: 'confirmed', - externalId: null, - metadata: null, - createdAt: '2026-03-01T00:00:00', - updatedAt: '2026-03-01T00:00:00', - ...overrides, - }; -} - -describe('expandRecurringEvents', () => { - it('should expand daily recurring event', () => { - const events = [ - makeEvent({ - id: 'r1', - startTime: '2026-03-01T09:00:00', - endTime: '2026-03-01T09:30:00', - recurrenceRule: 'FREQ=DAILY', - }), - ]; - const result = expandRecurringEvents(events, new Date('2026-03-01'), new Date('2026-03-07')); - expect(result.length).toBeGreaterThanOrEqual(6); - for (const e of result) { - expect(e.id).toContain('r1__recurrence__'); - expect(e.parentEventId).toBe('r1'); - } - }); - - it('should preserve event duration in occurrences', () => { - const events = [ - makeEvent({ - id: 'w1', - startTime: '2026-03-02T14:00:00', - endTime: '2026-03-02T15:00:00', - recurrenceRule: 'FREQ=WEEKLY', - }), - ]; - const result = expandRecurringEvents(events, new Date('2026-03-01'), new Date('2026-03-31')); - for (const e of result) { - const dur = new Date(e.endTime).getTime() - new Date(e.startTime).getTime(); - expect(dur).toBe(3600000); - } - }); - - it('should respect exceptions', () => { - const events = [ - makeEvent({ - id: 'exc', - startTime: '2026-03-01T09:00:00', - endTime: '2026-03-01T09:30:00', - recurrenceRule: 'FREQ=DAILY', - recurrenceExceptions: ['2026-03-03', '2026-03-05'], - }), - ]; - const result = expandRecurringEvents(events, new Date('2026-03-01'), new Date('2026-03-07')); - const dates = result.map((e) => e.id.split('__recurrence__')[1]); - expect(dates).not.toContain('2026-03-03'); - expect(dates).not.toContain('2026-03-05'); - expect(dates).toContain('2026-03-01'); - }); - - it('should not expand non-recurring events', () => { - const events = [makeEvent({ id: 'normal' })]; - const result = expandRecurringEvents(events, new Date('2026-03-01'), new Date('2026-03-31')); - expect(result).toHaveLength(1); - expect(result[0].id).toBe('normal'); - }); -}); - -describe('recurrence helpers', () => { - it('isRecurrenceOccurrence', () => { - expect(eventsStore.isRecurrenceOccurrence('evt__recurrence__2026-03-15')).toBe(true); - expect(eventsStore.isRecurrenceOccurrence('evt-1')).toBe(false); - }); - - it('getParentEventId', () => { - expect(eventsStore.getParentEventId('evt-1__recurrence__2026-03-15')).toBe('evt-1'); - expect(eventsStore.getParentEventId('evt-1')).toBe('evt-1'); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/events.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/events.svelte.ts deleted file mode 100644 index ea6d52017..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/events.svelte.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Events Store — Mutations Only - * - * Reads come from useLiveQuery (see $lib/data/queries.ts). - * This store only handles writes to IndexedDB and draft event state. - */ - -import type { CalendarEvent, CreateEventInput, UpdateEventInput } from '@calendar/shared'; -import { eventCollection, type LocalEvent } from '$lib/data/local-store'; -import { toastStore } from '@manacore/shared-ui'; -import { CalendarEvents } from '@manacore/shared-utils/analytics'; -import { get } from 'svelte/store'; -import { _ } from 'svelte-i18n'; -import { toCalendarEvent } from '$lib/data/queries'; - -// Mutation error state -let error = $state(null); - -// Draft event for quick create (temporary event shown in grid before saving) -let draftEvent = $state(null); - -export const eventsStore = { - get error() { - return error; - }, - get draftEvent() { - return draftEvent; - }, - - /** - * Create a new event — writes to IndexedDB instantly. - */ - async createEvent(data: CreateEventInput) { - error = null; - try { - const newLocal: LocalEvent = { - id: crypto.randomUUID(), - calendarId: data.calendarId ?? '', - title: data.title, - description: data.description ?? null, - startDate: - typeof data.startTime === 'string' - ? data.startTime - : new Date(data.startTime).toISOString(), - endDate: - typeof data.endTime === 'string' ? data.endTime : new Date(data.endTime).toISOString(), - allDay: data.isAllDay ?? false, - location: data.location ?? null, - recurrenceRule: data.recurrenceRule ?? null, - color: data.color ?? null, - reminders: null, - }; - - const inserted = await eventCollection.insert(newLocal); - const newEvent = toCalendarEvent(inserted); - CalendarEvents.eventCreated(!!data.recurrenceRule); - return { data: newEvent, error: null }; - } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to create event'; - error = msg; - return { data: null, error: { message: msg } }; - } - }, - - /** - * Update an event — writes to IndexedDB instantly. - */ - async updateEvent(id: string, data: UpdateEventInput) { - error = null; - try { - // Map shared types to local field names - const localData: Partial = {}; - if (data.title !== undefined) localData.title = data.title; - if (data.description !== undefined) localData.description = data.description; - if (data.startTime !== undefined) - localData.startDate = - typeof data.startTime === 'string' - ? data.startTime - : new Date(data.startTime).toISOString(); - if (data.endTime !== undefined) - localData.endDate = - typeof data.endTime === 'string' ? data.endTime : new Date(data.endTime).toISOString(); - if (data.isAllDay !== undefined) localData.allDay = data.isAllDay; - if (data.location !== undefined) localData.location = data.location; - if (data.recurrenceRule !== undefined) localData.recurrenceRule = data.recurrenceRule; - if (data.color !== undefined) localData.color = data.color; - if (data.calendarId !== undefined) localData.calendarId = data.calendarId; - - const updated = await eventCollection.update(id, localData); - if (updated) { - const updatedEvent = toCalendarEvent(updated); - CalendarEvents.eventUpdated(); - return { data: updatedEvent, error: null }; - } - return { data: null, error: null }; - } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to update event'; - error = msg; - toastStore.error(get(_)('toast.eventUpdateError') + ': ' + msg); - return { data: null, error: { message: msg } }; - } - }, - - /** - * Delete an event — removes from IndexedDB instantly. - */ - async deleteEvent(id: string) { - error = null; - try { - await eventCollection.delete(id); - CalendarEvents.eventDeleted(); - toastStore.success(get(_)('toast.eventDeleted')); - return { error: null }; - } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to delete event'; - error = msg; - toastStore.error(get(_)('toast.eventDeleteError') + ': ' + msg); - return { error: { message: msg } }; - } - }, - - // ========== Draft Event Methods ========== - - createDraftEvent(data: Partial) { - draftEvent = { - id: '__draft__', - calendarId: data.calendarId || '', - userId: '', - title: data.title || '', - description: data.description || null, - location: data.location || null, - startTime: data.startTime || new Date().toISOString(), - endTime: data.endTime || new Date().toISOString(), - isAllDay: data.isAllDay || false, - timezone: data.timezone || null, - recurrenceRule: null, - recurrenceEndDate: null, - recurrenceExceptions: null, - parentEventId: null, - color: data.color || null, - status: 'confirmed', - externalId: null, - metadata: data.metadata || null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - } as CalendarEvent; - return draftEvent; - }, - - updateDraftEvent(data: Partial) { - if (draftEvent) { - draftEvent = { ...draftEvent, ...data }; - } - }, - - clearDraftEvent() { - draftEvent = null; - }, - - isDraftEvent(eventId: string) { - return eventId === '__draft__'; - }, - - isRecurrenceOccurrence(eventId: string) { - return eventId.includes('__recurrence__'); - }, - - getParentEventId(eventId: string): string { - if (eventId.includes('__recurrence__')) { - return eventId.split('__recurrence__')[0]; - } - return eventId; - }, - - /** - * Delete a single occurrence of a recurring event by adding an exception date - */ - async deleteRecurrenceOccurrence(eventId: string) { - const dateKey = eventId.split('__recurrence__')[1]; - - // For local-first, we would ideally store exceptions in IndexedDB. - // For now, toast success (the event structure doesn't support exceptions at local level yet). - try { - toastStore.success(get(_)('toast.eventDeleted')); - return { error: null }; - } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to delete occurrence'; - toastStore.error(get(_)('toast.error') + ': ' + msg); - return { error: { message: msg } }; - } - }, - - /** - * Delete all occurrences of a recurring event (deletes the parent) - */ - async deleteRecurrenceSeries(eventId: string) { - const parentId = this.getParentEventId(eventId); - return this.deleteEvent(parentId); - }, - - /** - * Update all occurrences of a recurring event (updates the parent) - */ - async updateRecurrenceSeries(eventId: string, data: UpdateEventInput) { - const parentId = this.getParentEventId(eventId); - return this.updateEvent(parentId, data); - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/events.test.ts b/apps/calendar/apps/web-archived/src/lib/stores/events.test.ts deleted file mode 100644 index 1983b14b4..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/events.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import type { CalendarEvent } from '@calendar/shared'; - -// Mock local-store to avoid import.meta.env issues in tests -vi.mock('$lib/data/local-store', () => ({ - eventCollection: {}, - calendarCollection: {}, -})); - -vi.mock('@manacore/local-store/svelte', () => ({ - useLiveQueryWithDefault: vi.fn(), -})); - -vi.mock('$lib/api/birthdays', () => ({ - BIRTHDAY_CALENDAR: { id: 'birthday-cal', name: 'Birthdays', color: '#ec4899' }, -})); - -vi.mock('@manacore/shared-ui', () => ({ - toastStore: { error: vi.fn(), success: vi.fn() }, -})); - -import { getEventsForDay, getEventsInRange } from '$lib/data/queries'; -import { eventsStore } from './events.svelte'; - -function makeEvent(overrides: Partial = {}): CalendarEvent { - return { - id: 'evt-1', - calendarId: 'cal-1', - userId: 'user-1', - title: 'Test Event', - description: null, - location: null, - startTime: '2026-03-15T10:00:00', - endTime: '2026-03-15T11:00:00', - isAllDay: false, - timezone: 'Europe/Berlin', - recurrenceRule: null, - recurrenceEndDate: null, - recurrenceExceptions: null, - parentEventId: null, - color: null, - status: 'confirmed', - externalId: null, - metadata: null, - createdAt: '2026-03-01T00:00:00', - updatedAt: '2026-03-01T00:00:00', - ...overrides, - }; -} - -describe('getEventsForDay', () => { - it('should return events that start on the given day', () => { - const events = [ - makeEvent({ id: 'evt-1', startTime: '2026-03-15T10:00:00', endTime: '2026-03-15T11:00:00' }), - ]; - const result = getEventsForDay(events, new Date('2026-03-15')); - expect(result).toHaveLength(1); - expect(result[0].id).toBe('evt-1'); - }); - - it('should not return events from a different day', () => { - const events = [ - makeEvent({ startTime: '2026-03-15T10:00:00', endTime: '2026-03-15T11:00:00' }), - ]; - const result = getEventsForDay(events, new Date('2026-03-16')); - expect(result).toHaveLength(0); - }); - - it('should include all-day events that span the given day', () => { - const events = [ - makeEvent({ - id: 'allday-1', - startTime: '2026-03-14T00:00:00', - endTime: '2026-03-16T23:59:59', - isAllDay: true, - }), - ]; - const result = getEventsForDay(events, new Date('2026-03-15')); - expect(result).toHaveLength(1); - expect(result[0].id).toBe('allday-1'); - }); -}); - -describe('getEventsInRange', () => { - it('should return events that overlap with the given range', () => { - const events = [ - makeEvent({ id: 'evt-1', startTime: '2026-03-15T10:00:00', endTime: '2026-03-15T11:00:00' }), - makeEvent({ id: 'evt-2', startTime: '2026-03-20T14:00:00', endTime: '2026-03-20T15:00:00' }), - ]; - const result = getEventsInRange(events, new Date('2026-03-14'), new Date('2026-03-16')); - expect(result).toHaveLength(1); - expect(result[0].id).toBe('evt-1'); - }); - - it('should return events that partially overlap the range', () => { - const events = [ - makeEvent({ startTime: '2026-03-14T22:00:00', endTime: '2026-03-15T02:00:00' }), - ]; - const result = getEventsInRange( - events, - new Date('2026-03-15T00:00:00'), - new Date('2026-03-15T23:59:59') - ); - expect(result).toHaveLength(1); - }); - - it('should return empty array when no events in range', () => { - const events = [ - makeEvent({ startTime: '2026-03-20T10:00:00', endTime: '2026-03-20T11:00:00' }), - ]; - const result = getEventsInRange(events, new Date('2026-03-14'), new Date('2026-03-16')); - expect(result).toHaveLength(0); - }); -}); - -describe('createDraftEvent / clearDraftEvent', () => { - it('should create a draft event with __draft__ id', () => { - const draft = eventsStore.createDraftEvent({ - title: 'Draft Meeting', - startTime: '2026-03-15T10:00:00', - endTime: '2026-03-15T11:00:00', - }); - - expect(draft.id).toBe('__draft__'); - expect(draft.title).toBe('Draft Meeting'); - expect(eventsStore.draftEvent).not.toBeNull(); - expect(eventsStore.draftEvent?.id).toBe('__draft__'); - }); - - it('should clear the draft event', () => { - eventsStore.createDraftEvent({ - title: 'Draft', - startTime: '2026-03-15T10:00:00', - endTime: '2026-03-15T11:00:00', - }); - - expect(eventsStore.draftEvent).not.toBeNull(); - eventsStore.clearDraftEvent(); - expect(eventsStore.draftEvent).toBeNull(); - }); - - it('should set default values for missing fields', () => { - const draft = eventsStore.createDraftEvent({}); - - expect(draft.calendarId).toBe(''); - expect(draft.title).toBe(''); - expect(draft.isAllDay).toBe(false); - expect(draft.status).toBe('confirmed'); - expect(draft.description).toBeNull(); - expect(draft.location).toBeNull(); - }); -}); - -describe('isDraftEvent', () => { - it('should return true for __draft__ id', () => { - expect(eventsStore.isDraftEvent('__draft__')).toBe(true); - }); - - it('should return false for regular event ids', () => { - expect(eventsStore.isDraftEvent('evt-1')).toBe(false); - expect(eventsStore.isDraftEvent('')).toBe(false); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/external-calendars.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/external-calendars.svelte.ts deleted file mode 100644 index 5c0338ba1..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/external-calendars.svelte.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * External Calendars Store - Manages CalDAV/iCal/Google calendar connections - */ - -import type { ExternalCalendar, ConnectExternalCalendarInput } from '@calendar/shared'; -import * as api from '$lib/api/sync'; -import { toastStore } from '@manacore/shared-ui'; -import { get } from 'svelte/store'; -import { _ } from 'svelte-i18n'; - -// State -let externalCalendars = $state([]); -let loading = $state(false); -let error = $state(null); -let syncingIds = $state>(new Set()); - -function getArray(): ExternalCalendar[] { - const arr = externalCalendars ?? []; - return Array.isArray(arr) ? arr : []; -} - -export const externalCalendarsStore = { - get calendars() { - return externalCalendars; - }, - get loading() { - return loading; - }, - get error() { - return error; - }, - - isSyncing(id: string) { - return syncingIds.has(id); - }, - - async fetchCalendars() { - loading = true; - error = null; - - const result = await api.getExternalCalendars(); - - if (result.error) { - error = result.error.message; - externalCalendars = []; - } else { - externalCalendars = result.data || []; - } - - loading = false; - return result; - }, - - async connect(data: ConnectExternalCalendarInput) { - const result = await api.connectExternalCalendar(data); - - if (result.error) { - toastStore.error(get(_)('toast.connectionError') + ': ' + result.error.message); - } else if (result.data) { - externalCalendars = [...externalCalendars, result.data]; - toastStore.success(get(_)('toast.calendarConnected', { values: { name: data.name } })); - } - - return result; - }, - - async update(id: string, data: api.UpdateExternalCalendarInput) { - const result = await api.updateExternalCalendar(id, data); - - if (result.error) { - toastStore.error(get(_)('toast.updateError') + ': ' + result.error.message); - } else if (result.data) { - externalCalendars = getArray().map((c) => (c.id === id ? result.data! : c)); - } - - return result; - }, - - async disconnect(id: string) { - const cal = getArray().find((c) => c.id === id); - const result = await api.disconnectExternalCalendar(id); - - if (result.error) { - toastStore.error(get(_)('toast.connectionError') + ': ' + result.error.message); - } else { - externalCalendars = getArray().filter((c) => c.id !== id); - toastStore.success( - get(_)('toast.calendarDisconnected', { - values: { name: cal?.name || get(_)('common.calendar') }, - }) - ); - } - - return result; - }, - - async triggerSync(id: string) { - syncingIds = new Set([...syncingIds, id]); - - const result = await api.triggerSync(id); - - if (result.error) { - toastStore.error(get(_)('toast.syncError') + ': ' + result.error.message); - // Update last sync error in local state - externalCalendars = getArray().map((c) => - c.id === id ? { ...c, lastSyncError: result.error!.message } : c - ); - } else { - toastStore.success(get(_)('toast.syncCompleted')); - // Update local state with new sync time - externalCalendars = getArray().map((c) => - c.id === id ? { ...c, lastSyncAt: new Date().toISOString(), lastSyncError: null } : c - ); - } - - const newSet = new Set(syncingIds); - newSet.delete(id); - syncingIds = newSet; - - return result; - }, - - async discoverCalDav(serverUrl: string, username: string, password: string) { - return api.discoverCalDav(serverUrl, username, password); - }, - - async getGoogleAuthUrl() { - return api.getGoogleAuthUrl(); - }, - - getById(id: string) { - return getArray().find((c) => c.id === id); - }, - - clear() { - externalCalendars = []; - error = null; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/external-calendars.test.ts b/apps/calendar/apps/web-archived/src/lib/stores/external-calendars.test.ts deleted file mode 100644 index bed028db3..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/external-calendars.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -vi.mock('$lib/api/sync', () => ({ - getExternalCalendars: vi.fn(), - connectExternalCalendar: vi.fn(), - updateExternalCalendar: vi.fn(), - disconnectExternalCalendar: vi.fn(), - triggerSync: vi.fn(), - discoverCalDav: vi.fn(), - getGoogleAuthUrl: vi.fn(), -})); - -vi.mock('@manacore/shared-ui', () => ({ - toastStore: { error: vi.fn(), success: vi.fn() }, -})); - -vi.mock('svelte-i18n', () => { - const { readable } = require('svelte/store'); - return { - _: readable((key: string) => key), - }; -}); - -import * as api from '$lib/api/sync'; -import { externalCalendarsStore } from './external-calendars.svelte'; -import type { ExternalCalendar } from '@calendar/shared'; - -const mockFetch = vi.mocked(api.getExternalCalendars); -const mockConnect = vi.mocked(api.connectExternalCalendar); -const mockUpdate = vi.mocked(api.updateExternalCalendar); -const mockDisconnect = vi.mocked(api.disconnectExternalCalendar); -const mockSync = vi.mocked(api.triggerSync); - -function makeCal(overrides: Partial = {}): ExternalCalendar { - return { - id: 'ext-1', - userId: 'user-1', - name: 'Google', - provider: 'google', - calendarUrl: 'https://google.com/cal', - syncEnabled: true, - syncDirection: 'both', - syncInterval: 15, - lastSyncAt: null, - lastSyncError: null, - color: '#4285f4', - isVisible: true, - providerData: null, - createdAt: '2026-03-01', - updatedAt: '2026-03-01', - ...overrides, - }; -} - -describe('externalCalendarsStore', () => { - beforeEach(() => { - vi.clearAllMocks(); - externalCalendarsStore.clear(); - }); - - it('should load calendars', async () => { - mockFetch.mockResolvedValue({ - data: [makeCal({ id: 'ext-1' }), makeCal({ id: 'ext-2' })], - error: null, - }); - await externalCalendarsStore.fetchCalendars(); - expect(externalCalendarsStore.calendars).toHaveLength(2); - expect(externalCalendarsStore.loading).toBe(false); - }); - - it('should set error on fetch failure', async () => { - mockFetch.mockResolvedValue({ - data: null, - error: { message: 'fail', code: 'ERR', status: 500 }, - }); - await externalCalendarsStore.fetchCalendars(); - expect(externalCalendarsStore.error).toBe('fail'); - }); - - it('should add calendar on connect', async () => { - mockConnect.mockResolvedValue({ data: makeCal({ id: 'new' }), error: null }); - await externalCalendarsStore.connect({ name: 'X', provider: 'caldav', calendarUrl: 'url' }); - expect(externalCalendarsStore.calendars).toHaveLength(1); - }); - - it('should remove calendar on disconnect', async () => { - mockFetch.mockResolvedValue({ data: [makeCal()], error: null }); - await externalCalendarsStore.fetchCalendars(); - mockDisconnect.mockResolvedValue({ data: { success: true }, error: null }); - await externalCalendarsStore.disconnect('ext-1'); - expect(externalCalendarsStore.calendars).toHaveLength(0); - }); - - it('should update calendar', async () => { - mockFetch.mockResolvedValue({ data: [makeCal({ syncEnabled: true })], error: null }); - await externalCalendarsStore.fetchCalendars(); - mockUpdate.mockResolvedValue({ data: makeCal({ syncEnabled: false }), error: null }); - await externalCalendarsStore.update('ext-1', { syncEnabled: false }); - expect(externalCalendarsStore.calendars[0].syncEnabled).toBe(false); - }); - - it('should update lastSyncAt on sync success', async () => { - mockFetch.mockResolvedValue({ data: [makeCal({ lastSyncAt: null })], error: null }); - await externalCalendarsStore.fetchCalendars(); - mockSync.mockResolvedValue({ data: { success: true }, error: null }); - await externalCalendarsStore.triggerSync('ext-1'); - expect(externalCalendarsStore.calendars[0].lastSyncAt).not.toBeNull(); - expect(externalCalendarsStore.isSyncing('ext-1')).toBe(false); - }); - - it('should set error on sync failure', async () => { - mockFetch.mockResolvedValue({ data: [makeCal()], error: null }); - await externalCalendarsStore.fetchCalendars(); - mockSync.mockResolvedValue({ - data: null, - error: { message: 'Timeout', code: 'T', status: 504 }, - }); - await externalCalendarsStore.triggerSync('ext-1'); - expect(externalCalendarsStore.calendars[0].lastSyncError).toBe('Timeout'); - }); - - it('should find by ID', async () => { - mockFetch.mockResolvedValue({ data: [makeCal({ name: 'Found' })], error: null }); - await externalCalendarsStore.fetchCalendars(); - expect(externalCalendarsStore.getById('ext-1')?.name).toBe('Found'); - expect(externalCalendarsStore.getById('nope')).toBeUndefined(); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/navigation.ts b/apps/calendar/apps/web-archived/src/lib/stores/navigation.ts deleted file mode 100644 index 0704ef93a..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/navigation.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createSimpleNavigationStores } from '@manacore/shared-stores'; - -export const { isNavCollapsed, isToolbarCollapsed } = createSimpleNavigationStores({ - withToolbar: true, - toolbarCollapsedDefault: false, -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/search.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/search.svelte.ts deleted file mode 100644 index 05bfa2ebd..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/search.svelte.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Search Store - manages search state for highlighting events in calendar views - */ - -// Accept any object with at least an id property -interface SearchableItem { - id: string; -} - -// State -let query = $state(''); -let matchingEventIds = $state>(new Set()); -let isSearching = $state(false); - -/** - * Set search query and matching items (events or any items with an id) - */ -function setSearch(newQuery: string, matchingItems: T[]) { - query = newQuery; - matchingEventIds = new Set(matchingItems.map((item) => item.id)); - isSearching = newQuery.trim().length > 0; -} - -/** - * Clear search - */ -function clear() { - query = ''; - matchingEventIds = new Set(); - isSearching = false; -} - -/** - * Check if an event matches the search - */ -function isEventHighlighted(eventId: string): boolean { - return isSearching && matchingEventIds.has(eventId); -} - -/** - * Check if an event should be dimmed (search active but event doesn't match) - */ -function isEventDimmed(eventId: string): boolean { - return isSearching && !matchingEventIds.has(eventId); -} - -export const searchStore = { - get query() { - return query; - }, - get matchingEventIds() { - return matchingEventIds; - }, - get isSearching() { - return isSearching; - }, - setSearch, - clear, - isEventHighlighted, - isEventDimmed, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/settings.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/settings.svelte.ts deleted file mode 100644 index 4159233fa..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/settings.svelte.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Settings Store - Manages user preferences for the calendar app - * Uses @manacore/shared-stores createAppSettingsStore factory with cloud sync - */ - -import { browser } from '$app/environment'; -import type { CalendarViewType } from '@calendar/shared'; -import { createAppSettingsStore } from '@manacore/shared-stores'; -import { userSettings } from './user-settings.svelte'; - -// Settings types -export type WeekStartDay = 0 | 1; -export type TimeFormat = '24h' | '12h'; -export type AllDayDisplayMode = 'header' | 'block'; -export type SttLanguage = 'de' | 'auto'; - -export interface CalendarAppSettings extends Record { - // View settings - defaultView: CalendarViewType; - weekStartsOn: WeekStartDay; - showOnlyWeekdays: boolean; - showWeekNumbers: boolean; - timeFormat: TimeFormat; - filterHoursEnabled: boolean; - dayStartHour: number; - dayEndHour: number; - allDayDisplayMode: AllDayDisplayMode; - - // Display settings - showBirthdays: boolean; - showBirthdayAge: boolean; - dateStripShowMoonPhases: boolean; - dateStripShowEventIndicators: boolean; - dateStripHighlightWeekends: boolean; - dateStripCompact: boolean; - - // Event defaults - defaultEventDuration: number; - smartDurationEnabled: boolean; - defaultReminder: number; - - // Voice input settings - sttLanguage: SttLanguage; -} - -// --- UI state (not persisted, resets on page load) --- -let _dateStripCollapsed = $state(false); -let _tagStripCollapsed = $state(true); -let _selectedTagIds = $state([]); -let _immersiveModeEnabled = $state(false); -let _sidebarCollapsed = $state(true); - -const DEFAULT_SETTINGS: CalendarAppSettings = { - defaultView: 'week', - weekStartsOn: 1, - showOnlyWeekdays: false, - showWeekNumbers: false, - timeFormat: '24h', - filterHoursEnabled: false, - dayStartHour: 6, - dayEndHour: 20, - allDayDisplayMode: 'header', - dateStripShowMoonPhases: true, - dateStripShowEventIndicators: true, - dateStripHighlightWeekends: true, - dateStripCompact: false, - showBirthdays: true, - showBirthdayAge: true, - defaultEventDuration: 60, - smartDurationEnabled: true, - defaultReminder: 15, - sttLanguage: 'de', -}; - -// Cloud sync state -let cloudSyncEnabled = $state(false); -let initialSyncDone = $state(false); - -// Sync to cloud callback -async function syncToCloud(settings: CalendarAppSettings) { - if (!cloudSyncEnabled || !browser) return; - try { - await userSettings.updateDeviceAppSettings(settings as unknown as Record); - } catch (e) { - console.error('Failed to sync calendar settings to cloud:', e); - } -} - -// Create base store with cloud sync callback -const baseStore = createAppSettingsStore( - 'calendar-settings', - DEFAULT_SETTINGS, - { - onSettingsChange: syncToCloud, - } -); - -// Migrate: strip removed keys from localStorage to keep it clean -if (browser) { - try { - const raw = localStorage.getItem('calendar-settings'); - if (raw) { - const stored = JSON.parse(raw); - const removedKeys = [ - 'headerCompact', - 'headerWeekdayFormat', - 'headerShowDate', - 'headerAlwaysShowMonth', - 'dateStripShowWeekday', - 'dateStripShowMonthDividers', - 'dateStripShowWeekNumbers', - 'dateStripCollapsed', - 'tagStripCollapsed', - 'selectedTagIds', - 'immersiveModeEnabled', - 'sidebarCollapsed', - 'quickViewPillViews', - 'customDayCount', - ]; - let changed = false; - for (const key of removedKeys) { - if (key in stored) { - delete stored[key]; - changed = true; - } - } - if (changed) { - localStorage.setItem('calendar-settings', JSON.stringify(stored)); - } - } - } catch { - // ignore migration errors - } -} - -// Load settings from cloud -function loadFromCloud(): Partial | null { - if (!userSettings.loaded) return null; - const cloudSettings = userSettings.currentDeviceAppSettings; - if (cloudSettings && Object.keys(cloudSettings).length > 0) { - return cloudSettings as unknown as Partial; - } - return null; -} - -export const settingsStore = { - // Base store methods - get settings() { - return baseStore.settings; - }, - initialize: baseStore.initialize, - set: baseStore.set, - update: baseStore.update, - reset: baseStore.reset, - getDefaults: baseStore.getDefaults, - - // Persisted preference getters - get defaultView() { - return baseStore.settings.defaultView; - }, - get weekStartsOn() { - return baseStore.settings.weekStartsOn; - }, - get showOnlyWeekdays() { - return baseStore.settings.showOnlyWeekdays; - }, - get showWeekNumbers() { - return baseStore.settings.showWeekNumbers; - }, - get timeFormat() { - return baseStore.settings.timeFormat; - }, - get filterHoursEnabled() { - return baseStore.settings.filterHoursEnabled; - }, - get dayStartHour() { - return baseStore.settings.dayStartHour; - }, - get dayEndHour() { - return baseStore.settings.dayEndHour; - }, - get allDayDisplayMode() { - return baseStore.settings.allDayDisplayMode; - }, - get dateStripShowMoonPhases() { - return baseStore.settings.dateStripShowMoonPhases; - }, - get dateStripShowEventIndicators() { - return baseStore.settings.dateStripShowEventIndicators; - }, - get dateStripHighlightWeekends() { - return baseStore.settings.dateStripHighlightWeekends; - }, - get dateStripCompact() { - return baseStore.settings.dateStripCompact; - }, - get showBirthdays() { - return baseStore.settings.showBirthdays; - }, - get showBirthdayAge() { - return baseStore.settings.showBirthdayAge; - }, - get defaultEventDuration() { - return baseStore.settings.defaultEventDuration; - }, - get smartDurationEnabled() { - return baseStore.settings.smartDurationEnabled; - }, - get defaultReminder() { - return baseStore.settings.defaultReminder; - }, - get sttLanguage() { - return baseStore.settings.sttLanguage; - }, - get cloudSyncEnabled() { - return cloudSyncEnabled; - }, - - // --- UI state getters (non-persisted) --- - get dateStripCollapsed() { - return _dateStripCollapsed; - }, - get tagStripCollapsed() { - return _tagStripCollapsed; - }, - get selectedTagIds() { - return _selectedTagIds; - }, - get hasSelectedTags() { - return _selectedTagIds.length > 0; - }, - get immersiveModeEnabled() { - return _immersiveModeEnabled; - }, - get sidebarCollapsed() { - return _sidebarCollapsed; - }, - - // Cloud sync methods - enableCloudSync() { - cloudSyncEnabled = true; - if (!initialSyncDone) { - const cloudSettings = loadFromCloud(); - if (cloudSettings && Object.keys(cloudSettings).length > 0) { - baseStore.update(cloudSettings); - } else { - syncToCloud(baseStore.settings); - } - initialSyncDone = true; - } - }, - - disableCloudSync() { - cloudSyncEnabled = false; - }, - - // UI state toggle methods (non-persisted) - toggleSidebar() { - _sidebarCollapsed = !_sidebarCollapsed; - }, - - toggleTagStrip() { - _tagStripCollapsed = !_tagStripCollapsed; - }, - - toggleTagSelection(tagId: string) { - const isSelected = _selectedTagIds.includes(tagId); - _selectedTagIds = isSelected - ? _selectedTagIds.filter((id) => id !== tagId) - : [..._selectedTagIds, tagId]; - }, - - isTagSelected(tagId: string): boolean { - return _selectedTagIds.includes(tagId); - }, - - clearTagSelection() { - _selectedTagIds = []; - }, - - toggleImmersiveMode() { - _immersiveModeEnabled = !_immersiveModeEnabled; - }, - - setDateStripCollapsed(value: boolean) { - _dateStripCollapsed = value; - }, - - // Time formatting helpers - formatTime(date: Date): string { - if (baseStore.settings.timeFormat === '12h') { - const hours = date.getHours(); - const minutes = date.getMinutes(); - const ampm = hours >= 12 ? 'PM' : 'AM'; - const displayHours = hours % 12 || 12; - return `${displayHours}:${minutes.toString().padStart(2, '0')} ${ampm}`; - } - return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; - }, - - formatHour(hour: number): string { - if (baseStore.settings.timeFormat === '12h') { - const ampm = hour >= 12 ? 'PM' : 'AM'; - const displayHour = hour % 12 || 12; - return `${displayHour} ${ampm}`; - } - return `${hour.toString().padStart(2, '0')}:00`; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/shares.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/shares.svelte.ts deleted file mode 100644 index e3a8ff54f..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/shares.svelte.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Calendar Shares Store - Manages calendar sharing and invitations - */ - -import type { CalendarShare, CalendarShareWithDetails } from '@calendar/shared'; -import * as api from '$lib/api/shares'; -import { toastStore } from '@manacore/shared-ui'; -import { get } from 'svelte/store'; -import { _ } from 'svelte-i18n'; - -// State -let shares = $state>(new Map()); -let invitations = $state([]); -let sharedWithMe = $state([]); -let loading = $state(false); - -export const sharesStore = { - get loading() { - return loading; - }, - get invitations() { - return invitations; - }, - get sharedWithMe() { - return sharedWithMe; - }, - - getSharesForCalendar(calendarId: string): CalendarShare[] { - return shares.get(calendarId) || []; - }, - - async fetchSharesForCalendar(calendarId: string) { - const result = await api.getShares(calendarId); - if (result.data) { - const arr = Array.isArray(result.data) ? result.data : []; - shares = new Map(shares).set(calendarId, arr); - } - return result; - }, - - async fetchInvitations() { - const result = await api.getInvitations(); - if (result.data) { - invitations = Array.isArray(result.data) ? result.data : []; - } - return result; - }, - - async fetchSharedWithMe() { - const result = await api.getSharedWithMe(); - if (result.data) { - sharedWithMe = Array.isArray(result.data) ? result.data : []; - } - return result; - }, - - async shareCalendar(calendarId: string, email: string, permission: 'read' | 'write' | 'admin') { - const result = await api.createShare(calendarId, { calendarId, email, permission }); - - if (result.error) { - toastStore.error(get(_)('toast.shareError') + ': ' + result.error.message); - } else { - toastStore.success(get(_)('toast.calendarShared', { values: { email } })); - await this.fetchSharesForCalendar(calendarId); - } - - return result; - }, - - async createShareLink(calendarId: string, permission: 'read' | 'write') { - const result = await api.createShare(calendarId, { - calendarId, - permission, - createLink: true, - }); - - if (result.error) { - toastStore.error(get(_)('toast.shareError') + ': ' + result.error.message); - } else { - toastStore.success(get(_)('toast.shareLinkCreated')); - await this.fetchSharesForCalendar(calendarId); - } - - return result; - }, - - async acceptInvitation(shareId: string) { - const result = await api.acceptShare(shareId); - - if (result.error) { - toastStore.error(get(_)('toast.shareError') + ': ' + result.error.message); - } else { - toastStore.success(get(_)('toast.inviteAccepted')); - invitations = invitations.filter((i) => i.id !== shareId); - await this.fetchSharedWithMe(); - } - - return result; - }, - - async declineInvitation(shareId: string) { - const result = await api.declineShare(shareId); - - if (result.error) { - toastStore.error(get(_)('toast.declineError') + ': ' + result.error.message); - } else { - invitations = invitations.filter((i) => i.id !== shareId); - } - - return result; - }, - - async removeShare(calendarId: string, shareId: string) { - const result = await api.deleteShare(calendarId, shareId); - - if (result.error) { - toastStore.error(get(_)('toast.removeError') + ': ' + result.error.message); - } else { - toastStore.success(get(_)('toast.shareRemoved')); - const current = shares.get(calendarId) || []; - shares = new Map(shares).set( - calendarId, - current.filter((s) => s.id !== shareId) - ); - } - - return result; - }, - - async updatePermission(shareId: string, permission: 'read' | 'write' | 'admin') { - const result = await api.updateShare(shareId, { permission }); - - if (result.error) { - toastStore.error(get(_)('toast.updateError') + ': ' + result.error.message); - } - - return result; - }, - - clear() { - shares = new Map(); - invitations = []; - sharedWithMe = []; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/theme.ts b/apps/calendar/apps/web-archived/src/lib/stores/theme.ts deleted file mode 100644 index 6abdb5d52..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/theme.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createThemeStore } from '@manacore/shared-theme'; - -// Create theme store with Calendar's primary color (blue) -export const theme = createThemeStore({ - appId: 'calendar', - defaultVariant: 'ocean', -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/unified-bar.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/unified-bar.svelte.ts deleted file mode 100644 index 9d49c1a00..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/unified-bar.svelte.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * UnifiedBar Store - Manages the new unified bottom bar system - * Replaces multiple individual bars with a layered, cohesive interface - */ - -import type { CalendarViewType } from '@calendar/shared'; -import { createAppSettingsStore } from '@manacore/shared-stores'; -import { userSettings } from './user-settings.svelte'; - -// UnifiedBar modes and layers -export type UnifiedBarMode = 'collapsed' | 'expanded' | 'overlay'; -export type UnifiedBarLayer = 'input' | 'date' | 'tag' | 'toolbar' | 'settings'; - -// Store interface -export interface UnifiedBarSettings extends Record { - // UnifiedBar mode control - unifiedBarMode: UnifiedBarMode; - unifiedBarActiveLayer: UnifiedBarLayer; - - // Legacy compatibility (keep existing settings working) - dateStripCollapsed: boolean; - tagStripCollapsed: boolean; - calendarToolbarCollapsed: boolean; - - // New UnifiedBar-specific settings - showQuickInput: boolean; - showDateStrip: boolean; - showTagStrip: boolean; - showCalendarToolbar: boolean; - overlayMenuOpen: boolean; - - // Animation and interaction preferences - barAnimationDuration: number; - enableHapticFeedback: boolean; - autoCollapseBars: boolean; - - // Quick access toggles - quickAccessActions: string[]; -} - -const DEFAULT_SETTINGS: UnifiedBarSettings = { - // Default to collapsed mode with only input bar visible - unifiedBarMode: 'collapsed', - unifiedBarActiveLayer: 'input', - - // Legacy compatibility - dateStripCollapsed: false, - tagStripCollapsed: true, - calendarToolbarCollapsed: true, - - // New settings - showQuickInput: true, - showDateStrip: true, - showTagStrip: false, - showCalendarToolbar: false, - overlayMenuOpen: false, - - // Interaction preferences - barAnimationDuration: 300, - enableHapticFeedback: true, - autoCollapseBars: false, - - // Quick actions - quickAccessActions: ['new-event', 'search', 'today', 'calendar-toggle'], -}; - -// Cloud sync state -let cloudSyncEnabled = $state(false); -let initialSyncDone = $state(false); - -// Sync to cloud callback -async function syncToCloud(settings: UnifiedBarSettings) { - if (!cloudSyncEnabled || typeof window === 'undefined') return; - try { - await userSettings.updateDeviceAppSettings(settings as unknown as Record); - } catch (e) { - console.error('Failed to sync unified bar settings to cloud:', e); - } -} - -// Create base store -const baseStore = createAppSettingsStore( - 'unified-bar-settings', - DEFAULT_SETTINGS, - { - onSettingsChange: syncToCloud, - } -); - -// Load settings from cloud -function loadFromCloud(): Partial | null { - if (!userSettings.loaded) return null; - const cloudSettings = userSettings.currentDeviceAppSettings; - if (cloudSettings && Object.keys(cloudSettings).length > 0) { - return cloudSettings as unknown as Partial; - } - return null; -} - -export const unifiedBarStore = { - // Base store methods - get settings() { - return baseStore.settings; - }, - initialize: baseStore.initialize, - set: baseStore.set, - update: baseStore.update, - reset: baseStore.reset, - getDefaults: baseStore.getDefaults, - - // Mode management - get mode() { - return baseStore.settings.unifiedBarMode; - }, - - get activeLayer() { - return baseStore.settings.unifiedBarActiveLayer; - }, - - get isOverlayOpen() { - return baseStore.settings.overlayMenuOpen; - }, - - // Layer visibility helpers - get showQuickInput() { - return baseStore.settings.showQuickInput; - }, - - get showDateStrip() { - return baseStore.settings.showDateStrip && !baseStore.settings.dateStripCollapsed; - }, - - get showTagStrip() { - return baseStore.settings.showTagStrip && !baseStore.settings.tagStripCollapsed; - }, - - get showCalendarToolbar() { - return baseStore.settings.showCalendarToolbar && !baseStore.settings.calendarToolbarCollapsed; - }, - - // Mode switching - setMode(mode: UnifiedBarMode) { - baseStore.set('unifiedBarMode', mode); - }, - - toggleOverlay() { - const isOpen = baseStore.settings.overlayMenuOpen; - baseStore.set('overlayMenuOpen', !isOpen); - if (!isOpen) { - baseStore.set('unifiedBarMode', 'overlay'); - } else { - baseStore.set('unifiedBarMode', 'collapsed'); - } - }, - - setActiveLayer(layer: UnifiedBarLayer) { - baseStore.set('unifiedBarActiveLayer', layer); - }, - - // Layer toggles - toggleQuickInput() { - baseStore.set('showQuickInput', !baseStore.settings.showQuickInput); - }, - - toggleDateStrip() { - const newValue = !baseStore.settings.showDateStrip; - baseStore.set('showDateStrip', newValue); - baseStore.set('dateStripCollapsed', !newValue); - }, - - toggleTagStrip() { - const newValue = !baseStore.settings.showTagStrip; - baseStore.set('showTagStrip', newValue); - baseStore.set('tagStripCollapsed', !newValue); - }, - - toggleCalendarToolbar() { - const newValue = !baseStore.settings.showCalendarToolbar; - baseStore.set('showCalendarToolbar', newValue); - baseStore.set('calendarToolbarCollapsed', !newValue); - }, - - // Quick actions - expandToLayer(layer: UnifiedBarLayer) { - baseStore.set('unifiedBarMode', 'expanded'); - baseStore.set('unifiedBarActiveLayer', layer); - - // Auto-show the layer if hidden - switch (layer) { - case 'date': - if (!baseStore.settings.showDateStrip) { - baseStore.set('showDateStrip', true); - baseStore.set('dateStripCollapsed', false); - } - break; - case 'tag': - if (!baseStore.settings.showTagStrip) { - baseStore.set('showTagStrip', true); - baseStore.set('tagStripCollapsed', false); - } - break; - case 'toolbar': - if (!baseStore.settings.showCalendarToolbar) { - baseStore.set('showCalendarToolbar', true); - baseStore.set('calendarToolbarCollapsed', false); - } - break; - } - }, - - collapseAll() { - baseStore.set('unifiedBarMode', 'collapsed'); - baseStore.set('unifiedBarActiveLayer', 'input'); - if (baseStore.settings.autoCollapseBars) { - baseStore.set('showDateStrip', false); - baseStore.set('showTagStrip', false); - baseStore.set('showCalendarToolbar', false); - } - }, - - // Cloud sync methods - enableCloudSync() { - cloudSyncEnabled = true; - if (!initialSyncDone) { - const cloudSettings = loadFromCloud(); - if (cloudSettings && Object.keys(cloudSettings).length > 0) { - baseStore.update(cloudSettings); - } else { - syncToCloud(baseStore.settings); - } - initialSyncDone = true; - } - }, - - disableCloudSync() { - cloudSyncEnabled = false; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/user-settings.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/user-settings.svelte.ts deleted file mode 100644 index c76260502..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/user-settings.svelte.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * User Settings Store for Calendar - * - * This store syncs settings with mana-core-auth and provides: - * - Global settings that apply to all apps - * - Per-app overrides for customization - * - localStorage caching for offline support - */ - -import { browser } from '$app/environment'; -import { createUserSettingsStore } from '@manacore/shared-theme'; -import { authStore } from './auth.svelte'; - -// Get auth URL dynamically at runtime -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - if (injectedUrl) return injectedUrl; - } - return import.meta.env.DEV ? 'http://localhost:3001' : ''; -} - -export const userSettings = createUserSettingsStore({ - appId: 'calendar', - authUrl: getAuthUrl, - getAccessToken: () => authStore.getAccessToken(), -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/view.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/view.svelte.ts deleted file mode 100644 index 87b97e8f9..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/view.svelte.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * View Store - Manages calendar view state using Svelte 5 runes - */ - -import { browser } from '$app/environment'; -import type { CalendarViewType } from '@calendar/shared'; -import { CalendarEvents } from '@manacore/shared-utils/analytics'; -import { - startOfDay, - startOfWeek, - startOfMonth, - endOfDay, - endOfWeek, - endOfMonth, - addDays, - addWeeks, - addMonths, - subWeeks, - subMonths, -} from 'date-fns'; - -// Import settings store for weekStartsOn -import { settingsStore } from './settings.svelte'; - -// Supported view types after cleanup -const SUPPORTED_VIEWS: CalendarViewType[] = ['week', 'month', 'agenda']; - -// State -let currentDate = $state(new Date()); -let viewType = $state('week'); - -// Derived state - uses weekStartsOn from settings -const viewRange = $derived.by(() => { - const weekStartsOn = settingsStore.weekStartsOn; - - switch (viewType) { - case 'week': - return { - start: startOfWeek(currentDate, { weekStartsOn }), - end: endOfWeek(currentDate, { weekStartsOn }), - }; - case 'month': - return { - start: startOfMonth(currentDate), - end: endOfMonth(currentDate), - }; - case 'agenda': - // Agenda shows 30 days from current date - return { - start: startOfDay(currentDate), - end: endOfDay(addDays(currentDate, 30)), - }; - default: - return { - start: startOfWeek(currentDate, { weekStartsOn }), - end: endOfWeek(currentDate, { weekStartsOn }), - }; - } -}); - -export const viewStore = { - // Getters - get currentDate() { - return currentDate; - }, - get viewType() { - return viewType; - }, - get viewRange() { - return viewRange; - }, - - /** - * Initialize view state from localStorage - * Note: Most settings are now managed by settingsStore - */ - initialize() { - if (!browser) return; - - // Initialize settings store first - settingsStore.initialize(); - - // Load view type from settings or localStorage (for backwards compatibility) - const savedView = localStorage.getItem('calendar-view-type'); - if (savedView && SUPPORTED_VIEWS.includes(savedView as CalendarViewType)) { - viewType = savedView as CalendarViewType; - } else { - // Use default view from settings, fallback to 'week' if unsupported - const defaultView = settingsStore.defaultView; - viewType = SUPPORTED_VIEWS.includes(defaultView) ? defaultView : 'week'; - } - }, - - /** - * Set the current date - */ - setDate(date: Date) { - currentDate = date; - }, - - /** - * Set the view type - */ - setViewType(type: CalendarViewType) { - // Only allow supported view types - if (!SUPPORTED_VIEWS.includes(type)) { - type = 'week'; - } - viewType = type; - if (browser) { - localStorage.setItem('calendar-view-type', type); - } - CalendarEvents.viewChanged(type as 'day' | 'week' | 'month' | 'agenda'); - }, - - /** - * Navigate to today - */ - goToToday() { - currentDate = new Date(); - }, - - /** - * Navigate to previous period - */ - goToPrevious() { - switch (viewType) { - case 'week': - currentDate = subWeeks(currentDate, 1); - break; - case 'month': - currentDate = subMonths(currentDate, 1); - break; - case 'agenda': - currentDate = subWeeks(currentDate, 1); - break; - } - }, - - /** - * Navigate to next period - */ - goToNext() { - switch (viewType) { - case 'week': - currentDate = addWeeks(currentDate, 1); - break; - case 'month': - currentDate = addMonths(currentDate, 1); - break; - case 'agenda': - currentDate = addWeeks(currentDate, 1); - break; - } - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/stores/view.test.ts b/apps/calendar/apps/web-archived/src/lib/stores/view.test.ts deleted file mode 100644 index 6a1b17944..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/view.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { startOfWeek, startOfMonth, endOfMonth, addDays, isSameDay } from 'date-fns'; - -// Mock $app/environment -vi.mock('$app/environment', () => ({ - browser: false, -})); - -// Mock the settings store -vi.mock('./settings.svelte', () => ({ - settingsStore: { - weekStartsOn: 1 as 0 | 1, - defaultView: 'week', - initialize: vi.fn(), - }, -})); - -// Mock @manacore/shared-stores -vi.mock('@manacore/shared-stores', () => ({ - createAppSettingsStore: vi.fn(() => ({ - settings: { weekStartsOn: 1, defaultView: 'week' }, - initialize: vi.fn(), - set: vi.fn(), - update: vi.fn(), - reset: vi.fn(), - getDefaults: vi.fn(), - })), -})); - -// Mock user-settings store -vi.mock('./user-settings.svelte', () => ({ - userSettings: { - loaded: false, - currentDeviceAppSettings: {}, - updateDeviceAppSettings: vi.fn(), - }, -})); - -import { viewStore } from './view.svelte'; - -describe('viewStore', () => { - beforeEach(() => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('week'); - }); - - describe('setDate / currentDate', () => { - it('should update the current date', () => { - const newDate = new Date('2026-06-01T00:00:00'); - viewStore.setDate(newDate); - expect(isSameDay(viewStore.currentDate, newDate)).toBe(true); - }); - }); - - describe('setViewType / viewType', () => { - it('should update the view type', () => { - viewStore.setViewType('month'); - expect(viewStore.viewType).toBe('month'); - }); - - it('should accept all valid view types', () => { - const types = ['week', 'month', 'agenda'] as const; - - for (const type of types) { - viewStore.setViewType(type); - expect(viewStore.viewType).toBe(type); - } - }); - }); - - describe('viewRange', () => { - it('should return correct range for week view', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); // Sunday - viewStore.setViewType('week'); - - const range = viewStore.viewRange; - const expected = startOfWeek(new Date('2026-03-15'), { weekStartsOn: 1 }); - expect(isSameDay(range.start, expected)).toBe(true); - }); - - it('should return correct range for month view', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('month'); - - const range = viewStore.viewRange; - expect(isSameDay(range.start, startOfMonth(new Date('2026-03-15')))).toBe(true); - expect(isSameDay(range.end, endOfMonth(new Date('2026-03-15')))).toBe(true); - }); - - it('should return correct range for agenda view (30 days)', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('agenda'); - - const range = viewStore.viewRange; - expect(isSameDay(range.start, new Date('2026-03-15'))).toBe(true); - expect(isSameDay(range.end, addDays(new Date('2026-03-15'), 30))).toBe(true); - }); - }); - - describe('goToNext / goToPrevious', () => { - it('should navigate forward by 1 week in week view', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('week'); - - viewStore.goToNext(); - expect(isSameDay(viewStore.currentDate, new Date('2026-03-22'))).toBe(true); - }); - - it('should navigate backward by 1 week in week view', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('week'); - - viewStore.goToPrevious(); - expect(isSameDay(viewStore.currentDate, new Date('2026-03-08'))).toBe(true); - }); - - it('should navigate forward by 1 month in month view', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('month'); - - viewStore.goToNext(); - expect(viewStore.currentDate.getMonth()).toBe(3); // April - expect(viewStore.currentDate.getDate()).toBe(15); - }); - - it('should navigate backward by 1 month in month view', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('month'); - - viewStore.goToPrevious(); - expect(viewStore.currentDate.getMonth()).toBe(1); // February - expect(viewStore.currentDate.getDate()).toBe(15); - }); - - it('should navigate forward by 7 days in agenda view', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('agenda'); - - viewStore.goToNext(); - expect(isSameDay(viewStore.currentDate, new Date('2026-03-22'))).toBe(true); - }); - - it('should navigate backward by 7 days in agenda view', () => { - viewStore.setDate(new Date('2026-03-15T12:00:00')); - viewStore.setViewType('agenda'); - - viewStore.goToPrevious(); - expect(isSameDay(viewStore.currentDate, new Date('2026-03-08'))).toBe(true); - }); - }); - - describe('goToToday', () => { - it('should set date to today', () => { - viewStore.setDate(new Date('2020-01-01')); - viewStore.goToToday(); - - const today = new Date(); - expect(isSameDay(viewStore.currentDate, today)).toBe(true); - }); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/stores/voice-recording.svelte.ts b/apps/calendar/apps/web-archived/src/lib/stores/voice-recording.svelte.ts deleted file mode 100644 index 054c4bdb7..000000000 --- a/apps/calendar/apps/web-archived/src/lib/stores/voice-recording.svelte.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Voice Recording Store - * - * Manages the state of voice recording for event creation. - * Uses Svelte 5 runes for reactive state management. - */ - -import { - createAudioRecorder, - requestMicrophonePermission, - isAudioRecordingSupported, - formatDuration, - type AudioRecorder, - type PermissionState, -} from '$lib/utils/audio-recorder'; -import { transcribeAudio, type TranscriptionResult } from '$lib/services/stt'; -import { settingsStore } from './settings.svelte'; - -export type VoiceRecordingState = - | 'idle' - | 'requesting' // Requesting microphone permission - | 'recording' - | 'processing' // Transcribing audio - | 'error'; - -export interface VoiceRecordingError { - message: string; - code?: string; -} - -// State -let state = $state('idle'); -let duration = $state(0); -let error = $state(null); -let permissionState = $state('prompt'); - -// Internal -let recorder: AudioRecorder | null = null; -let onResultCallback: ((result: TranscriptionResult) => void) | null = null; - -/** - * Voice Recording Store - */ -export const voiceRecordingStore = { - // Getters - get state() { - return state; - }, - - get duration() { - return duration; - }, - - get formattedDuration() { - return formatDuration(duration); - }, - - get error() { - return error; - }, - - get permissionState() { - return permissionState; - }, - - get isSupported() { - return isAudioRecordingSupported(); - }, - - get isIdle() { - return state === 'idle'; - }, - - get isRecording() { - return state === 'recording'; - }, - - get isProcessing() { - return state === 'processing'; - }, - - get hasError() { - return state === 'error'; - }, - - /** - * Set the callback for when transcription completes successfully - */ - setOnResult(callback: (result: TranscriptionResult) => void) { - onResultCallback = callback; - }, - - /** - * Check microphone permission without starting recording - */ - async checkPermission(): Promise { - permissionState = await requestMicrophonePermission(); - return permissionState; - }, - - /** - * Start voice recording - */ - async startRecording(): Promise { - if (state !== 'idle' && state !== 'error') { - return; - } - - // Reset error state - error = null; - state = 'requesting'; - - try { - // Check permission - permissionState = await requestMicrophonePermission(); - - if (permissionState === 'unsupported') { - throw { - message: 'Kein Mikrofon gefunden', - code: 'NOT_SUPPORTED', - }; - } - - if (permissionState === 'denied') { - throw { - message: 'Mikrofonzugriff verweigert. Bitte in Browsereinstellungen erlauben.', - code: 'PERMISSION_DENIED', - }; - } - - // Create and start recorder - recorder = createAudioRecorder({ - maxDuration: 60000, // 60 seconds max - onDurationUpdate: (ms) => { - duration = ms; - }, - onMaxDurationWarning: () => { - // Could show a toast or visual warning - console.warn('Approaching max recording duration'); - }, - onError: (err) => { - error = { - message: err.message, - code: 'RECORDER_ERROR', - }; - state = 'error'; - }, - }); - - await recorder.start(); - state = 'recording'; - duration = 0; - } catch (err) { - // Handle error objects with message/code - if (err && typeof err === 'object' && 'message' in err) { - error = err as VoiceRecordingError; - } else if (err instanceof Error) { - error = { - message: err.message, - code: 'START_ERROR', - }; - } else { - error = { - message: 'Aufnahme konnte nicht gestartet werden', - code: 'UNKNOWN_ERROR', - }; - } - state = 'error'; - recorder = null; - } - }, - - /** - * Stop recording and process transcription - */ - async stopRecording(): Promise { - if (state !== 'recording' || !recorder) { - return; - } - - state = 'processing'; - - try { - const audioBlob = await recorder.stop(); - recorder = null; - - // Get language setting - const language = settingsStore.sttLanguage; - - // Transcribe - const result = await transcribeAudio(audioBlob, language); - - if (result.success) { - // Success - call the callback - state = 'idle'; - duration = 0; - onResultCallback?.(result.data); - } else { - // Transcription error - error = result.error; - state = 'error'; - } - } catch (err) { - error = { - message: err instanceof Error ? err.message : 'Transkription fehlgeschlagen', - code: 'TRANSCRIPTION_ERROR', - }; - state = 'error'; - recorder = null; - } - }, - - /** - * Cancel recording without transcription - */ - cancel(): void { - if (recorder) { - recorder.cancel(); - recorder = null; - } - state = 'idle'; - duration = 0; - error = null; - }, - - /** - * Clear error and return to idle state - */ - clearError(): void { - error = null; - if (state === 'error') { - state = 'idle'; - } - }, - - /** - * Reset to initial state - */ - reset(): void { - this.cancel(); - error = null; - }, -}; diff --git a/apps/calendar/apps/web-archived/src/lib/utils/audio-recorder.ts b/apps/calendar/apps/web-archived/src/lib/utils/audio-recorder.ts deleted file mode 100644 index 9554bebe6..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/audio-recorder.ts +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Audio Recorder Utility - * - * Wrapper around MediaRecorder API for voice recording functionality. - * Handles microphone permissions, recording state, and audio blob creation. - */ - -export type PermissionState = 'granted' | 'denied' | 'prompt' | 'unsupported'; - -export interface AudioRecorderOptions { - /** Called when recording duration updates (every 100ms) */ - onDurationUpdate?: (durationMs: number) => void; - /** Called when an error occurs */ - onError?: (error: Error) => void; - /** Maximum recording duration in milliseconds (default: 60000 = 60s) */ - maxDuration?: number; - /** Warning callback when approaching max duration */ - onMaxDurationWarning?: () => void; -} - -export interface AudioRecorder { - /** Start recording audio */ - start(): Promise; - /** Stop recording and return the audio blob */ - stop(): Promise; - /** Cancel recording without returning data */ - cancel(): void; - /** Whether currently recording */ - readonly isRecording: boolean; - /** Current recording duration in milliseconds */ - readonly duration: number; -} - -/** - * Check if the browser supports audio recording - */ -export function isAudioRecordingSupported(): boolean { - return !!( - typeof navigator !== 'undefined' && - navigator.mediaDevices && - typeof navigator.mediaDevices.getUserMedia === 'function' && - typeof MediaRecorder !== 'undefined' - ); -} - -/** - * Request microphone permission and return the current permission state - */ -export async function requestMicrophonePermission(): Promise { - if (!isAudioRecordingSupported()) { - return 'unsupported'; - } - - try { - // Check existing permission if available - if (navigator.permissions) { - const permissionStatus = await navigator.permissions.query({ - name: 'microphone' as PermissionName, - }); - - if (permissionStatus.state === 'granted') { - return 'granted'; - } - - if (permissionStatus.state === 'denied') { - return 'denied'; - } - } - - // Try to get user media to trigger permission prompt - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - - // Stop the stream immediately - we just needed to check permission - stream.getTracks().forEach((track) => track.stop()); - - return 'granted'; - } catch (error) { - if (error instanceof DOMException) { - if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') { - return 'denied'; - } - if (error.name === 'NotFoundError') { - return 'unsupported'; - } - } - return 'denied'; - } -} - -/** - * Get the best supported audio MIME type - */ -function getSupportedMimeType(): string { - const mimeTypes = [ - 'audio/webm;codecs=opus', - 'audio/webm', - 'audio/ogg;codecs=opus', - 'audio/mp4', - 'audio/mpeg', - ]; - - for (const mimeType of mimeTypes) { - if (MediaRecorder.isTypeSupported(mimeType)) { - return mimeType; - } - } - - // Fallback - let the browser decide - return ''; -} - -/** - * Create an audio recorder instance - */ -export function createAudioRecorder(options: AudioRecorderOptions = {}): AudioRecorder { - const { onDurationUpdate, onError, maxDuration = 60000, onMaxDurationWarning } = options; - - let mediaRecorder: MediaRecorder | null = null; - let mediaStream: MediaStream | null = null; - let audioChunks: Blob[] = []; - let isRecording = false; - let duration = 0; - let durationInterval: ReturnType | null = null; - let startTime = 0; - let warningShown = false; - - const recorder: AudioRecorder = { - get isRecording() { - return isRecording; - }, - - get duration() { - return duration; - }, - - async start() { - if (isRecording) { - throw new Error('Already recording'); - } - - if (!isAudioRecordingSupported()) { - throw new Error('Audio recording is not supported in this browser'); - } - - try { - // Get audio stream - mediaStream = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - }, - }); - - // Create MediaRecorder with best supported format - const mimeType = getSupportedMimeType(); - mediaRecorder = new MediaRecorder(mediaStream, mimeType ? { mimeType } : undefined); - - audioChunks = []; - duration = 0; - warningShown = false; - - // Handle data chunks - mediaRecorder.ondataavailable = (event) => { - if (event.data.size > 0) { - audioChunks.push(event.data); - } - }; - - // Handle errors - mediaRecorder.onerror = (event) => { - const error = new Error( - 'MediaRecorder error: ' + (event as any).error?.message || 'Unknown error' - ); - onError?.(error); - cleanup(); - }; - - // Start recording - mediaRecorder.start(100); // Collect data every 100ms for smoother stop - isRecording = true; - startTime = Date.now(); - - // Track duration - durationInterval = setInterval(() => { - duration = Date.now() - startTime; - onDurationUpdate?.(duration); - - // Warning at 50 seconds (10 seconds before max) - if (!warningShown && duration >= maxDuration - 10000) { - warningShown = true; - onMaxDurationWarning?.(); - } - - // Auto-stop at max duration - if (duration >= maxDuration) { - recorder.stop().catch(onError); - } - }, 100); - } catch (error) { - cleanup(); - throw error; - } - }, - - async stop(): Promise { - if (!isRecording || !mediaRecorder) { - throw new Error('Not currently recording'); - } - - return new Promise((resolve, reject) => { - if (!mediaRecorder) { - reject(new Error('MediaRecorder not available')); - return; - } - - mediaRecorder.onstop = () => { - const mimeType = mediaRecorder?.mimeType || 'audio/webm'; - const blob = new Blob(audioChunks, { type: mimeType }); - cleanup(); - resolve(blob); - }; - - try { - mediaRecorder.stop(); - } catch (error) { - cleanup(); - reject(error); - } - }); - }, - - cancel() { - cleanup(); - }, - }; - - function cleanup() { - isRecording = false; - duration = 0; - - if (durationInterval) { - clearInterval(durationInterval); - durationInterval = null; - } - - if (mediaRecorder) { - if (mediaRecorder.state !== 'inactive') { - try { - mediaRecorder.stop(); - } catch { - // Ignore errors when stopping - } - } - mediaRecorder = null; - } - - if (mediaStream) { - mediaStream.getTracks().forEach((track) => track.stop()); - mediaStream = null; - } - - audioChunks = []; - } - - return recorder; -} - -/** - * Format duration in milliseconds to MM:SS format - */ -export function formatDuration(durationMs: number): string { - const totalSeconds = Math.floor(durationMs / 1000); - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; -} diff --git a/apps/calendar/apps/web-archived/src/lib/utils/calendarConstants.ts b/apps/calendar/apps/web-archived/src/lib/utils/calendarConstants.ts deleted file mode 100644 index 7d91e2dfa..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/calendarConstants.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Shared calendar constants - * Single source of truth for magic numbers used across calendar views - */ - -/** - * Height of one hour in pixels (should match CSS --hour-height variable) - */ -export const HOUR_HEIGHT_PX = 60; - -/** - * Snap interval for drag/drop and resize operations in minutes - */ -export const SNAP_INTERVAL_MINUTES = 15; - -/** - * Default event duration in minutes when creating quick events - */ -export const DEFAULT_EVENT_DURATION_MINUTES = 60; - -/** - * Minimum event height as percentage of visible hours - */ -export const MIN_EVENT_HEIGHT_PERCENT = 1.5; - -/** - * Maximum number of event dots to show in month view cells - */ -export const MAX_EVENT_DOTS = 5; - -/** - * Days buffer for infinite scroll in date strip - */ -export const DATE_STRIP_BUFFER_DAYS = 60; - -/** - * Default visible hours range - */ -export const DEFAULT_DAY_START_HOUR = 0; -export const DEFAULT_DAY_END_HOUR = 24; - -/** - * Week starts on (0 = Sunday, 1 = Monday) - */ -export const DEFAULT_WEEK_STARTS_ON = 1; - -/** - * All constants as a single object for convenient destructuring - */ -export const CALENDAR_CONSTANTS = { - HOUR_HEIGHT_PX, - SNAP_INTERVAL_MINUTES, - DEFAULT_EVENT_DURATION_MINUTES, - MIN_EVENT_HEIGHT_PERCENT, - MAX_EVENT_DOTS, - DATE_STRIP_BUFFER_DAYS, - DEFAULT_DAY_START_HOUR, - DEFAULT_DAY_END_HOUR, - DEFAULT_WEEK_STARTS_ON, -} as const; diff --git a/apps/calendar/apps/web-archived/src/lib/utils/dateNavigation.test.ts b/apps/calendar/apps/web-archived/src/lib/utils/dateNavigation.test.ts deleted file mode 100644 index 5bcd82b26..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/dateNavigation.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { getOffsetDate } from './dateNavigation'; - -describe('getOffsetDate', () => { - // Use local time to avoid timezone offset issues with date-fns - const baseDate = new Date(2026, 2, 18, 12, 0, 0); // March 18, 2026 12:00 - - describe('week view', () => { - it('should add 1 week for offset +1', () => { - const result = getOffsetDate(baseDate, 'week', 1); - expect(result).toEqual(new Date(2026, 2, 25, 12, 0, 0)); - }); - - it('should subtract 1 week for offset -1', () => { - const result = getOffsetDate(baseDate, 'week', -1); - expect(result).toEqual(new Date(2026, 2, 11, 12, 0, 0)); - }); - }); - - describe('month view', () => { - it('should add 1 month for offset +1', () => { - const result = getOffsetDate(baseDate, 'month', 1); - expect(result).toEqual(new Date(2026, 3, 18, 12, 0, 0)); - }); - - it('should subtract 1 month for offset -1', () => { - const result = getOffsetDate(baseDate, 'month', -1); - expect(result).toEqual(new Date(2026, 1, 18, 12, 0, 0)); - }); - }); - - describe('agenda view', () => { - it('should add 7 days for offset +1', () => { - const result = getOffsetDate(baseDate, 'agenda', 1); - expect(result).toEqual(new Date(2026, 2, 25, 12, 0, 0)); - }); - - it('should subtract 7 days for offset -1', () => { - const result = getOffsetDate(baseDate, 'agenda', -1); - expect(result).toEqual(new Date(2026, 2, 11, 12, 0, 0)); - }); - - it('should add 14 days for offset +2', () => { - const result = getOffsetDate(baseDate, 'agenda', 2); - expect(result).toEqual(new Date(2026, 3, 1, 12, 0, 0)); - }); - }); - - describe('default (unknown view type)', () => { - it('should fall through to week behavior', () => { - const result = getOffsetDate(baseDate, 'unknown' as any, 1); - expect(result).toEqual(new Date(2026, 2, 25, 12, 0, 0)); - }); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/utils/dateNavigation.ts b/apps/calendar/apps/web-archived/src/lib/utils/dateNavigation.ts deleted file mode 100644 index 6cc367b5a..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/dateNavigation.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Date Navigation Utilities - * Helper functions for calculating date offsets based on view type - */ - -import type { CalendarViewType } from '@calendar/shared'; -import { addDays, addWeeks, addMonths, subDays, subWeeks, subMonths } from 'date-fns'; - -/** - * Calculate a date offset based on the current view type - * - * @param date - The base date - * @param viewType - The current calendar view type - * @param offset - Number of periods to offset (-1 = previous, 1 = next) - * @returns The calculated date - * - * @example - * // Get previous week's date - * getOffsetDate(new Date(), 'week', -1) - * - * // Get next month's date - * getOffsetDate(new Date(), 'month', 1) - */ -export function getOffsetDate(date: Date, viewType: CalendarViewType, offset: number): Date { - switch (viewType) { - case 'week': - return offset > 0 ? addWeeks(date, offset) : subWeeks(date, Math.abs(offset)); - - case 'month': - return offset > 0 ? addMonths(date, offset) : subMonths(date, Math.abs(offset)); - - case 'agenda': - // Agenda moves by 7 days - return offset > 0 ? addDays(date, offset * 7) : subDays(date, Math.abs(offset) * 7); - - default: - return offset > 0 ? addWeeks(date, offset) : subWeeks(date, Math.abs(offset)); - } -} diff --git a/apps/calendar/apps/web-archived/src/lib/utils/drag-helpers.ts b/apps/calendar/apps/web-archived/src/lib/utils/drag-helpers.ts deleted file mode 100644 index 08bb26f78..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/drag-helpers.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Shared drag/drop utility functions - * Used by useEventDragDrop, useDragToCreate - */ - -import { SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants'; - -/** - * Format hours and minutes as HH:MM string - */ -export function formatTime(hours: number, minutes: number): string { - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; -} - -/** - * Get the effective snap interval, falling back to the default constant - */ -export function getSnapMinutes(snapMinutes?: number): number { - return snapMinutes ?? SNAP_INTERVAL_MINUTES; -} - -/** - * Snap a minute value to the nearest grid interval - */ -export function snapToGrid(minutes: number, snapMinutes?: number): number { - const snap = getSnapMinutes(snapMinutes); - return Math.round(minutes / snap) * snap; -} - -/** - * Map an X client coordinate to a day column based on container width - */ -export function getDayFromX( - clientX: number, - containerEl: HTMLElement | null, - days: Date[] -): Date | null { - if (!containerEl) return null; - - const rect = containerEl.getBoundingClientRect(); - const relativeX = clientX - rect.left; - const dayWidth = rect.width / days.length; - const dayIndex = Math.floor(relativeX / dayWidth); - - if (dayIndex >= 0 && dayIndex < days.length) { - return days[dayIndex]; - } - return null; -} - -/** - * Map a Y client coordinate to total minutes in the day, - * accounting for scroll offset, visible hour range, and snap interval - */ -export function getMinutesFromY( - clientY: number, - containerEl: HTMLElement | null, - totalVisibleHours: number, - hourHeight: number, - firstVisibleHour: number, - snapMinutes?: number -): number { - if (!containerEl) return 0; - - const rect = containerEl.getBoundingClientRect(); - const scrollTop = containerEl.parentElement?.scrollTop || 0; - const relativeY = clientY - rect.top + scrollTop; - - const visibleMinutes = (relativeY / (totalVisibleHours * hourHeight)) * totalVisibleHours * 60; - const totalMinutes = visibleMinutes + firstVisibleHour * 60; - - return snapToGrid(totalMinutes, snapMinutes); -} diff --git a/apps/calendar/apps/web-archived/src/lib/utils/event-estimator.test.ts b/apps/calendar/apps/web-archived/src/lib/utils/event-estimator.test.ts deleted file mode 100644 index f7e64713f..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/event-estimator.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - estimateEventDuration, - detectConflicts, - type HistoricalEventData, -} from './event-estimator'; - -function makeEvent(overrides: Partial = {}): HistoricalEventData { - return { - title: 'Default Event', - calendarId: null, - startDate: '2026-03-28T10:00:00Z', - endDate: '2026-03-28T11:00:00Z', // 60 min - allDay: false, - tagIds: [], - ...overrides, - }; -} - -describe('estimateEventDuration', () => { - it('should return null with insufficient data', () => { - const result = estimateEventDuration( - { title: 'New event' }, - [makeEvent(), makeEvent()] // only 2, need 3 - ); - expect(result).toBeNull(); - }); - - it('should estimate from events in same calendar', () => { - const history = Array.from({ length: 5 }, () => - makeEvent({ - calendarId: 'cal-1', - startDate: '2026-03-28T10:00:00Z', - endDate: '2026-03-28T10:30:00Z', // 30 min - }) - ); - - const result = estimateEventDuration({ title: 'Something', calendarId: 'cal-1' }, history); - - expect(result).not.toBeNull(); - expect(result!.minutes).toBe(30); - }); - - it('should weight title overlap strongly', () => { - const history = [ - makeEvent({ - title: 'Standup Meeting', - startDate: '2026-03-28T09:00:00Z', - endDate: '2026-03-28T09:15:00Z', - }), - makeEvent({ - title: 'Standup Meeting', - startDate: '2026-03-27T09:00:00Z', - endDate: '2026-03-27T09:15:00Z', - }), - makeEvent({ - title: 'Standup Meeting', - startDate: '2026-03-26T09:00:00Z', - endDate: '2026-03-26T09:15:00Z', - }), - // Unrelated longer events - makeEvent({ - title: 'Workshop', - startDate: '2026-03-28T10:00:00Z', - endDate: '2026-03-28T14:00:00Z', - }), - makeEvent({ - title: 'Konferenz', - startDate: '2026-03-27T10:00:00Z', - endDate: '2026-03-27T14:00:00Z', - }), - ]; - - const result = estimateEventDuration({ title: 'Standup Meeting' }, history); - - expect(result).not.toBeNull(); - expect(result!.minutes).toBe(15); - }); - - it('should ignore all-day events', () => { - const history = [ - ...Array.from({ length: 3 }, () => makeEvent({ title: 'Urlaub', allDay: true })), - ...Array.from({ length: 3 }, () => - makeEvent({ - title: 'Urlaub', - allDay: false, - startDate: '2026-03-28T10:00:00Z', - endDate: '2026-03-28T11:00:00Z', - }) - ), - ]; - - const result = estimateEventDuration({ title: 'Urlaub' }, history); - expect(result).not.toBeNull(); - expect(result!.minutes).toBe(60); - }); - - it('should round to nice numbers', () => { - const history = Array.from({ length: 5 }, () => - makeEvent({ - calendarId: 'cal-1', - startDate: '2026-03-28T10:00:00Z', - endDate: '2026-03-28T10:37:00Z', // 37 min - }) - ); - - const result = estimateEventDuration({ title: 'Task', calendarId: 'cal-1' }, history); - - expect(result).not.toBeNull(); - expect(result!.minutes % 5).toBe(0); - }); -}); - -describe('detectConflicts', () => { - const existingEvents = [ - { - id: 'e1', - title: 'Standup', - startDate: '2026-03-30T09:00:00Z', - endDate: '2026-03-30T09:30:00Z', - calendarId: 'cal-1', - }, - { - id: 'e2', - title: 'Meeting', - startDate: '2026-03-30T14:00:00Z', - endDate: '2026-03-30T15:00:00Z', - calendarId: 'cal-1', - }, - { - id: 'e3', - title: 'Urlaub', - startDate: '2026-03-30T00:00:00Z', - endDate: '2026-03-30T23:59:59Z', - calendarId: 'cal-2', - allDay: true, - }, - ]; - - it('should detect overlap with existing event', () => { - const result = detectConflicts('2026-03-30T14:30:00Z', '2026-03-30T15:30:00Z', existingEvents); - expect(result.hasConflict).toBe(true); - expect(result.conflicts).toHaveLength(1); - expect(result.conflicts[0].title).toBe('Meeting'); - }); - - it('should detect no conflict when time is free', () => { - const result = detectConflicts('2026-03-30T10:00:00Z', '2026-03-30T11:00:00Z', existingEvents); - expect(result.hasConflict).toBe(false); - expect(result.conflicts).toHaveLength(0); - }); - - it('should detect multiple overlaps', () => { - const result = detectConflicts('2026-03-30T08:45:00Z', '2026-03-30T15:30:00Z', existingEvents); - expect(result.hasConflict).toBe(true); - expect(result.conflicts).toHaveLength(2); // Standup + Meeting (not Urlaub, it's allDay) - }); - - it('should ignore all-day events', () => { - const result = detectConflicts('2026-03-30T12:00:00Z', '2026-03-30T13:00:00Z', existingEvents); - expect(result.hasConflict).toBe(false); - }); - - it('should not conflict with adjacent events (end = start)', () => { - const result = detectConflicts( - '2026-03-30T09:30:00Z', // starts exactly when Standup ends - '2026-03-30T10:00:00Z', - existingEvents - ); - expect(result.hasConflict).toBe(false); - }); - - it('should exclude specified event ID (edit mode)', () => { - const result = detectConflicts( - '2026-03-30T14:00:00Z', - '2026-03-30T15:00:00Z', - existingEvents, - 'e2' // editing the Meeting itself - ); - expect(result.hasConflict).toBe(false); - }); - - it('should handle invalid range gracefully', () => { - const result = detectConflicts( - '2026-03-30T15:00:00Z', - '2026-03-30T14:00:00Z', // end before start - existingEvents - ); - expect(result.hasConflict).toBe(false); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/utils/event-estimator.ts b/apps/calendar/apps/web-archived/src/lib/utils/event-estimator.ts deleted file mode 100644 index ef436f458..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/event-estimator.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * Event Duration Estimator & Conflict Detector - * - * Duration estimation: suggests event duration based on historical events - * using weighted similarity (calendar, title overlap, tags, time of day). - * - * Conflict detection: checks for overlapping events in a given time range. - * - * Both run fully offline against local IndexedDB data. - */ - -// ─── Duration Estimation ─────────────────────────────────── - -export interface HistoricalEventData { - title: string; - calendarId?: string | null; - startDate: string; - endDate: string; - allDay?: boolean; - tagIds?: string[]; -} - -export interface DurationEstimate { - minutes: number; - confidence: 'low' | 'medium' | 'high'; - sampleSize: number; -} - -interface ScoredEvent { - duration: number; // minutes - score: number; -} - -const STOP_WORDS = new Set([ - 'der', - 'die', - 'das', - 'ein', - 'eine', - 'und', - 'oder', - 'für', - 'mit', - 'von', - 'zu', - 'im', - 'am', - 'an', - 'auf', - 'in', - 'den', - 'dem', - 'des', - 'bei', - 'nach', - 'the', - 'a', - 'an', - 'and', - 'or', - 'for', - 'with', - 'from', - 'to', - 'in', - 'on', - 'at', -]); - -function tokenize(title: string): string[] { - return title - .toLowerCase() - .replace(/[^a-zäöüßàáâãèéêëìíîïòóôõùúûü0-9\s]/g, '') - .split(/\s+/) - .filter((w) => w.length > 2 && !STOP_WORDS.has(w)); -} - -function titleOverlap(a: string[], b: string[]): number { - if (a.length === 0 || b.length === 0) return 0; - const setB = new Set(b); - const shared = a.filter((w) => setB.has(w)).length; - return shared / Math.max(a.length, b.length); -} - -/** - * Get event duration in minutes. Ignores all-day events and unreasonable durations. - */ -function getEventDuration(event: HistoricalEventData): number | null { - if (event.allDay) return null; - - const start = new Date(event.startDate).getTime(); - const end = new Date(event.endDate).getTime(); - const minutes = (end - start) / 60_000; - - // Only use reasonable durations (5 min to 12 hours) - if (minutes >= 5 && minutes <= 720) { - return Math.round(minutes); - } - return null; -} - -function similarity( - newEvent: { title: string; calendarId?: string | null; tagIds?: string[] }, - historical: HistoricalEventData, - newTokens: string[] -): number { - let score = 0; - - // Same calendar is strongest signal - if ( - newEvent.calendarId && - historical.calendarId && - newEvent.calendarId === historical.calendarId - ) { - score += 3; - } - - // Shared tags - if (newEvent.tagIds && historical.tagIds) { - const histSet = new Set(historical.tagIds); - const shared = newEvent.tagIds.filter((id) => histSet.has(id)).length; - score += shared * 2; - } - - // Title word overlap - const histTokens = tokenize(historical.title); - const overlap = titleOverlap(newTokens, histTokens); - if (overlap > 0.5) - score += 4; // title match is very strong for events - else if (overlap > 0.2) score += 2; - else if (overlap > 0) score += 1; - - return score; -} - -/** - * Round minutes to human-friendly values - */ -function roundToNice(minutes: number): number { - if (minutes <= 10) return Math.round(minutes / 5) * 5 || 5; - if (minutes <= 30) return Math.round(minutes / 5) * 5; - if (minutes <= 60) return Math.round(minutes / 15) * 15; - if (minutes <= 240) return Math.round(minutes / 30) * 30; - return Math.round(minutes / 60) * 60; -} - -/** - * Estimate duration for a new event based on past events. - */ -export function estimateEventDuration( - newEvent: { title: string; calendarId?: string | null; tagIds?: string[] }, - history: HistoricalEventData[], - minSamples = 3 -): DurationEstimate | null { - const newTokens = tokenize(newEvent.title); - const scored: ScoredEvent[] = []; - - for (const event of history) { - const duration = getEventDuration(event); - if (duration === null) continue; - - const score = similarity(newEvent, event, newTokens); - if (score > 0) { - scored.push({ duration, score }); - } - } - - if (scored.length < minSamples) return null; - - scored.sort((a, b) => b.score - a.score); - const top = scored.slice(0, 20); - - let totalWeight = 0; - let totalDuration = 0; - for (const { duration, score } of top) { - totalWeight += score; - totalDuration += duration * score; - } - - const minutes = roundToNice(Math.round(totalDuration / totalWeight)); - - const maxScore = top[0].score; - const confidence: DurationEstimate['confidence'] = - top.length >= 10 && maxScore >= 5 - ? 'high' - : top.length >= 5 && maxScore >= 3 - ? 'medium' - : 'low'; - - return { minutes, confidence, sampleSize: top.length }; -} - -// ─── Conflict Detection ──────────────────────────────────── - -export interface ConflictingEvent { - id: string; - title: string; - startDate: string; - endDate: string; - calendarId: string; -} - -export interface ConflictResult { - hasConflict: boolean; - conflicts: ConflictingEvent[]; -} - -/** - * Check if a proposed event overlaps with existing events. - * Two events overlap if: eventA.start < eventB.end AND eventA.end > eventB.start - * - * @param startDate - Proposed event start (ISO string or Date) - * @param endDate - Proposed event end (ISO string or Date) - * @param existingEvents - All events to check against - * @param excludeEventId - Exclude this event (for editing existing events) - */ -export function detectConflicts( - startDate: string | Date, - endDate: string | Date, - existingEvents: { - id: string; - title: string; - startDate: string; - endDate: string; - calendarId: string; - allDay?: boolean; - }[], - excludeEventId?: string -): ConflictResult { - const newStart = new Date(startDate).getTime(); - const newEnd = new Date(endDate).getTime(); - - if (newStart >= newEnd) { - return { hasConflict: false, conflicts: [] }; - } - - const conflicts: ConflictingEvent[] = []; - - for (const event of existingEvents) { - if (event.id === excludeEventId) continue; - if (event.allDay) continue; // all-day events don't block time - - const eventStart = new Date(event.startDate).getTime(); - const eventEnd = new Date(event.endDate).getTime(); - - // Overlap check: A.start < B.end AND A.end > B.start - if (newStart < eventEnd && newEnd > eventStart) { - conflicts.push({ - id: event.id, - title: event.title, - startDate: event.startDate, - endDate: event.endDate, - calendarId: event.calendarId, - }); - } - } - - return { - hasConflict: conflicts.length > 0, - conflicts, - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/utils/event-parser.test.ts b/apps/calendar/apps/web-archived/src/lib/utils/event-parser.test.ts deleted file mode 100644 index c89bd26df..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/event-parser.test.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - parseEventInput, - resolveEventIds, - formatParsedEventPreview, - parseMultiEventInput, -} from './event-parser'; - -describe('parseEventInput', () => { - it('should parse a simple title', () => { - const result = parseEventInput('Meeting'); - expect(result.title).toBe('Meeting'); - expect(result.startDate).toBeUndefined(); - expect(result.duration).toBeUndefined(); - expect(result.tagNames).toEqual([]); - }); - - it('should parse date and time', () => { - const result = parseEventInput('Meeting morgen 14 Uhr'); - expect(result.title).toBe('Meeting'); - expect(result.startDate).toBeDefined(); - expect(result.startDate!.getHours()).toBe(14); - expect(result.startDate!.getMinutes()).toBe(0); - }); - - it('should parse duration in hours', () => { - const result = parseEventInput('Meeting 2h'); - expect(result.duration).toBe(120); - expect(result.title).toBe('Meeting'); - }); - - it('should parse duration in minutes', () => { - const result = parseEventInput('Standup 30min'); - expect(result.duration).toBe(30); - }); - - it('should parse combined duration 2h30m', () => { - const result = parseEventInput('Workshop 2h30m'); - expect(result.duration).toBe(150); - }); - - it('should parse duration in Stunden', () => { - const result = parseEventInput('Konferenz 3 Stunden'); - expect(result.duration).toBe(180); - }); - - it('should calculate endDate from startDate + duration', () => { - const result = parseEventInput('Meeting morgen 10 Uhr 2h'); - expect(result.startDate).toBeDefined(); - expect(result.endDate).toBeDefined(); - const diffMs = result.endDate!.getTime() - result.startDate!.getTime(); - expect(diffMs).toBe(120 * 60_000); // 2 hours - }); - - it('should default to 1h duration when no duration specified', () => { - const result = parseEventInput('Meeting morgen 10 Uhr'); - expect(result.startDate).toBeDefined(); - expect(result.endDate).toBeDefined(); - const diffMs = result.endDate!.getTime() - result.startDate!.getTime(); - expect(diffMs).toBe(60 * 60_000); // 1 hour default - }); - - it('should parse all-day events', () => { - const result = parseEventInput('Ganztägig Urlaub morgen'); - expect(result.isAllDay).toBe(true); - expect(result.title).toBe('Urlaub'); - expect(result.startDate).toBeDefined(); - }); - - it('should parse @calendar reference', () => { - const result = parseEventInput('Meeting @Arbeit'); - expect(result.calendarName).toBe('Arbeit'); - expect(result.attendees).toEqual([]); - expect(result.title).not.toContain('@Arbeit'); - }); - - it('should parse calendar and attendees from multiple @references', () => { - const result = parseEventInput('Meeting @Arbeit @Max @Anna'); - expect(result.calendarName).toBe('Arbeit'); - expect(result.attendees).toEqual(['Max', 'Anna']); - expect(result.title).not.toContain('@'); - }); - - it('should parse only attendees when no calendar match (first @ref treated as calendarName)', () => { - const result = parseEventInput('Meeting @Max @Anna'); - expect(result.calendarName).toBe('Max'); - expect(result.attendees).toEqual(['Anna']); - }); - - it('should parse single @reference as calendarName with no attendees', () => { - const result = parseEventInput('Meeting @Arbeit'); - expect(result.calendarName).toBe('Arbeit'); - expect(result.attendees).toEqual([]); - }); - - it('should parse #tags', () => { - const result = parseEventInput('Meeting #wichtig #team'); - expect(result.tagNames).toEqual(['wichtig', 'team']); - expect(result.title).not.toContain('#'); - }); - - it('should parse complex input with all fields', () => { - const result = parseEventInput('Teammeeting morgen 14 Uhr 1h @Arbeit #wichtig'); - expect(result.title).toBe('Teammeeting'); - expect(result.startDate).toBeDefined(); - expect(result.startDate!.getHours()).toBe(14); - expect(result.duration).toBe(60); - expect(result.calendarName).toBe('Arbeit'); - expect(result.tagNames).toEqual(['wichtig']); - }); - - it('should parse time range "14-16 Uhr"', () => { - const result = parseEventInput('Meeting morgen 14-16 Uhr'); - expect(result.title).toBe('Meeting'); - expect(result.startDate).toBeDefined(); - expect(result.startDate!.getHours()).toBe(14); - expect(result.endDate).toBeDefined(); - expect(result.endDate!.getHours()).toBe(16); - }); - - it('should parse time range "10:00-11:30"', () => { - const result = parseEventInput('Standup 10:00-11:30'); - expect(result.startDate).toBeDefined(); - expect(result.startDate!.getHours()).toBe(10); - expect(result.startDate!.getMinutes()).toBe(0); - expect(result.endDate).toBeDefined(); - expect(result.endDate!.getHours()).toBe(11); - expect(result.endDate!.getMinutes()).toBe(30); - }); - - it('should parse time range with en-dash "9–17 Uhr"', () => { - const result = parseEventInput('Arbeitstag 9–17 Uhr'); - expect(result.startDate!.getHours()).toBe(9); - expect(result.endDate!.getHours()).toBe(17); - }); - - it('should parse recurrence "jeden Montag"', () => { - const result = parseEventInput('Standup jeden Montag 9 Uhr'); - expect(result.recurrenceRule).toBe('FREQ=WEEKLY;BYDAY=MO'); - expect(result.title).toBe('Standup'); - }); - - it('should parse recurrence "wöchentlich"', () => { - const result = parseEventInput('Team-Meeting wöchentlich 14 Uhr'); - expect(result.recurrenceRule).toBe('FREQ=WEEKLY'); - }); - - it('should parse recurrence "täglich"', () => { - const result = parseEventInput('Standup täglich'); - expect(result.recurrenceRule).toBe('FREQ=DAILY'); - }); - - it('should have no recurrence for normal input', () => { - const result = parseEventInput('Meeting morgen'); - expect(result.recurrenceRule).toBeUndefined(); - }); - - it('should handle empty input', () => { - const result = parseEventInput(''); - expect(result.title).toBe(''); - expect(result.tagNames).toEqual([]); - expect(result.attendees).toEqual([]); - }); - - it('should parse time-only input (defaults to today)', () => { - const result = parseEventInput('Lunch 12 Uhr'); - expect(result.startDate).toBeDefined(); - expect(result.startDate!.getHours()).toBe(12); - }); -}); - -describe('resolveEventIds', () => { - const calendars = [ - { id: 'cal-1', name: 'Arbeit' }, - { id: 'cal-2', name: 'Privat' }, - ]; - - const tags = [ - { id: 'tag-1', name: 'Wichtig' }, - { id: 'tag-2', name: 'Team' }, - ]; - - it('should resolve calendar name to ID (case-insensitive)', () => { - const parsed = parseEventInput('Meeting @arbeit'); - const resolved = resolveEventIds(parsed, calendars, tags); - expect(resolved.calendarId).toBe('cal-1'); - }); - - it('should resolve tag names to IDs (case-insensitive)', () => { - const parsed = parseEventInput('Meeting #team'); - const resolved = resolveEventIds(parsed, calendars, tags); - expect(resolved.tagIds).toEqual(['tag-2']); - }); - - it('should use default calendar when no calendar specified', () => { - const parsed = parseEventInput('Meeting morgen'); - const resolved = resolveEventIds(parsed, calendars, tags, 'cal-1'); - expect(resolved.calendarId).toBe('cal-1'); - }); - - it('should skip unknown calendar and treat it as attendee', () => { - const parsed = parseEventInput('Meeting @Unbekannt'); - const resolved = resolveEventIds(parsed, calendars, tags); - expect(resolved.calendarId).toBeUndefined(); - expect(resolved.attendees).toEqual(['Unbekannt']); - }); - - it('should resolve calendar and keep attendees separate', () => { - const parsed = parseEventInput('Meeting @Arbeit @Max @Anna'); - const resolved = resolveEventIds(parsed, calendars, tags); - expect(resolved.calendarId).toBe('cal-1'); - expect(resolved.attendees).toEqual(['Max', 'Anna']); - }); - - it('should treat all @refs as attendees when no calendar matches', () => { - const parsed = parseEventInput('Meeting @Max @Anna'); - const resolved = resolveEventIds(parsed, calendars, tags); - expect(resolved.calendarId).toBeUndefined(); - expect(resolved.attendees).toEqual(['Max', 'Anna']); - }); - - it('should resolve calendar with no attendees', () => { - const parsed = parseEventInput('Meeting @Arbeit'); - const resolved = resolveEventIds(parsed, calendars, tags); - expect(resolved.calendarId).toBe('cal-1'); - expect(resolved.attendees).toEqual([]); - }); - - it('should produce ISO date strings', () => { - const parsed = parseEventInput('Meeting morgen 14 Uhr'); - const resolved = resolveEventIds(parsed, calendars, tags); - expect(resolved.startTime).toBeDefined(); - expect(resolved.endTime).toBeDefined(); - // Verify it's a valid ISO string - expect(new Date(resolved.startTime!).toISOString()).toBe(resolved.startTime); - }); -}); - -describe('formatParsedEventPreview', () => { - it('should format duration', () => { - const parsed = parseEventInput('Meeting 2h'); - const preview = formatParsedEventPreview(parsed); - expect(preview).toContain('2h'); - }); - - it('should format calendar', () => { - const parsed = parseEventInput('Meeting @Arbeit'); - const preview = formatParsedEventPreview(parsed); - expect(preview).toContain('Arbeit'); - }); - - it('should format attendees', () => { - const parsed = parseEventInput('Meeting @Arbeit @Max @Anna'); - const preview = formatParsedEventPreview(parsed); - expect(preview).toContain('👥 Max, Anna'); - }); - - it('should format tags', () => { - const parsed = parseEventInput('Meeting #team'); - const preview = formatParsedEventPreview(parsed); - expect(preview).toContain('team'); - }); - - it('should format all-day events', () => { - const parsed = parseEventInput('Ganztägig Urlaub morgen'); - const preview = formatParsedEventPreview(parsed); - expect(preview).toContain('ganztägig'); - }); - - it('should return empty string for title-only input', () => { - const parsed = parseEventInput('Einfaches Meeting'); - expect(formatParsedEventPreview(parsed)).toBe(''); - }); - - it('should join parts with separator', () => { - const parsed = parseEventInput('Meeting morgen 14 Uhr 1h @Arbeit'); - const preview = formatParsedEventPreview(parsed); - expect(preview).toContain(' · '); - }); -}); - -describe('parseMultiEventInput', () => { - it('should return single event for simple input', () => { - const events = parseMultiEventInput('Meeting morgen'); - expect(events).toHaveLength(1); - expect(events[0].title).toBe('Meeting'); - }); - - it('should split on "danach"', () => { - const events = parseMultiEventInput('Meeting danach Review'); - expect(events).toHaveLength(2); - expect(events[0].title).toBe('Meeting'); - expect(events[1].title).toBe('Review'); - }); - - it('should split on semicolon', () => { - const events = parseMultiEventInput('Meeting; Review; Retro'); - expect(events).toHaveLength(3); - }); - - it('should inherit calendar from first event', () => { - const events = parseMultiEventInput('Meeting @Arbeit danach Review'); - expect(events[0].calendarName).toBe('Arbeit'); - expect(events[1].calendarName).toBe('Arbeit'); - }); - - it('should offset time based on first event end', () => { - const events = parseMultiEventInput('Meeting 14 Uhr 1h danach Review 30min'); - expect(events).toHaveLength(2); - expect(events[0].startDate).toBeDefined(); - expect(events[0].startDate!.getHours()).toBe(14); - // Second event should start at 15:00 (14 + 1h) - expect(events[1].startDate).toBeDefined(); - expect(events[1].startDate!.getHours()).toBe(15); - expect(events[1].startDate!.getMinutes()).toBe(0); - // Second event should end at 15:30 - expect(events[1].endDate).toBeDefined(); - expect(events[1].endDate!.getHours()).toBe(15); - expect(events[1].endDate!.getMinutes()).toBe(30); - }); - - it('should chain three events with time offsets', () => { - const events = parseMultiEventInput( - 'Standup 9 Uhr 30min; Sprint Planning 1h; Code Review 30min' - ); - expect(events).toHaveLength(3); - // Standup: 9:00-9:30 - expect(events[0].startDate!.getHours()).toBe(9); - expect(events[0].endDate!.getHours()).toBe(9); - expect(events[0].endDate!.getMinutes()).toBe(30); - // Sprint Planning: 9:30-10:30 - expect(events[1].startDate!.getHours()).toBe(9); - expect(events[1].startDate!.getMinutes()).toBe(30); - expect(events[1].endDate!.getHours()).toBe(10); - expect(events[1].endDate!.getMinutes()).toBe(30); - // Code Review: 10:30-11:00 - expect(events[2].startDate!.getHours()).toBe(10); - expect(events[2].startDate!.getMinutes()).toBe(30); - expect(events[2].endDate!.getHours()).toBe(11); - expect(events[2].endDate!.getMinutes()).toBe(0); - }); - - it('should handle ", danach" pattern', () => { - const events = parseMultiEventInput('Zahnarzt, danach Apotheke'); - expect(events).toHaveLength(2); - expect(events[0].title).toBe('Zahnarzt'); - expect(events[1].title).toBe('Apotheke'); - }); - - it('should default to 1h when no duration on follow-up events', () => { - const events = parseMultiEventInput('Meeting 14 Uhr 1h danach Review'); - expect(events[1].startDate).toBeDefined(); - expect(events[1].endDate).toBeDefined(); - // Review default: 1h - const diff = events[1].endDate!.getTime() - events[1].startDate!.getTime(); - expect(diff).toBe(60 * 60_000); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/utils/event-parser.ts b/apps/calendar/apps/web-archived/src/lib/utils/event-parser.ts deleted file mode 100644 index c2489499c..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/event-parser.ts +++ /dev/null @@ -1,502 +0,0 @@ -/** - * Event Parser for Calendar App - * - * Extends the base parser with event-specific patterns: - * - Duration: 1h, 30min, 2h30m, 1 Stunde - * - Location: in Berlin, im Büro - * - Calendar: @CalendarName - * - * Examples: - * - "Meeting morgen 14 Uhr 1h @Arbeit #wichtig" - * - "Arzttermin 15.12. 10:00 30min in Praxis Dr. Müller" - * - "Mittagessen heute 12 Uhr" - * - "Ganztägig Urlaub nächste Woche" - */ - -import { - parseBaseInput, - extractAtReferences, - extractRecurrence, - combineDateAndTime, - formatDatePreview, - formatTimePreview, - type ParserLocale, -} from '@manacore/shared-utils'; -import { addHours } from 'date-fns'; - -export interface ParsedEvent { - title: string; - startDate?: Date; - endDate?: Date; - duration?: number; // in minutes - isAllDay?: boolean; - calendarName?: string; - attendees: string[]; - location?: string; - recurrenceRule?: string; - tagNames: string[]; -} - -interface Calendar { - id: string; - name: string; -} - -interface Tag { - id: string; - name: string; -} - -export interface ParsedEventWithIds { - title: string; - startTime?: string; - endTime?: string; - isAllDay?: boolean; - calendarId?: string; - attendees: string[]; - location?: string; - recurrenceRule?: string; - tagIds: string[]; -} - -// ============================================================================ -// Time Range Extraction (14-16 Uhr, 10:00-11:30) -// ============================================================================ - -// "14-16 Uhr", "14:00-16:00", "10-11:30" -const TIME_RANGE_PATTERN = - /\b(?:um\s*)?(\d{1,2})(?::(\d{2}))?\s*[-–]\s*(\d{1,2})(?::(\d{2}))?\s*(?:uhr)?\b/i; - -function extractTimeRange(text: string): { - startTime?: { hours: number; minutes: number }; - endTime?: { hours: number; minutes: number }; - remaining: string; -} { - const match = text.match(TIME_RANGE_PATTERN); - if (match) { - const startHours = parseInt(match[1]); - const startMinutes = match[2] ? parseInt(match[2]) : 0; - const endHours = parseInt(match[3]); - const endMinutes = match[4] ? parseInt(match[4]) : 0; - - if ( - startHours >= 0 && - startHours <= 23 && - endHours >= 0 && - endHours <= 23 && - startMinutes >= 0 && - startMinutes <= 59 && - endMinutes >= 0 && - endMinutes <= 59 - ) { - return { - startTime: { hours: startHours, minutes: startMinutes }, - endTime: { hours: endHours, minutes: endMinutes }, - remaining: text.replace(TIME_RANGE_PATTERN, '').trim(), - }; - } - } - return { remaining: text }; -} - -// ============================================================================ -// Duration Extraction -// ============================================================================ - -// Locale-specific "hours" words (Stunden, hours, heures, horas, ore) -const HOURS_WORDS: Record = { - de: 'stunde[n]?', - en: 'hours?', - fr: 'heures?', - es: 'horas?', - it: 'ore', -}; - -function getDurationPatterns( - locale: ParserLocale -): { pattern: RegExp; getMinutes: (match: RegExpMatchArray) => number }[] { - const hoursWord = HOURS_WORDS[locale]; - return [ - // 2h30m, 2h 30m, 1h30min - { - pattern: /\b(\d+)\s*h\s*(\d+)\s*(?:m(?:in)?)\b/i, - getMinutes: (m) => parseInt(m[1]) * 60 + parseInt(m[2]), - }, - // 1h, 2h (hours only) - { pattern: /\b(\d+)\s*h\b/i, getMinutes: (m) => parseInt(m[1]) * 60 }, - // 30min, 45 min, 90 Minuten/minutes/etc. - { - pattern: /\b(\d+)\s*(?:min(?:uten?|utes?)?)\b/i, - getMinutes: (m) => parseInt(m[1]), - }, - // Locale-specific full word: 1 Stunde, 2 hours, 3 heures, etc. - { - pattern: new RegExp(`\\b(\\d+)\\s*${hoursWord}\\b`, 'i'), - getMinutes: (m) => parseInt(m[1]) * 60, - }, - ]; -} - -function extractDuration( - text: string, - locale: ParserLocale = 'de' -): { duration?: number; remaining: string } { - for (const { pattern, getMinutes } of getDurationPatterns(locale)) { - const match = text.match(pattern); - if (match) { - return { - duration: getMinutes(match), - remaining: text.replace(pattern, '').trim(), - }; - } - } - return { duration: undefined, remaining: text }; -} - -// ============================================================================ -// Location Extraction -// ============================================================================ - -// Location extraction - runs on the title AFTER date/time extraction has already -// removed date keywords like "in 3 Tagen", "in einer halben Stunde" etc. -// "in Berlin", "im Büro", "bei Dr. Müller", "am Bahnhof" -const LOCATION_PATTERN = /\b(?:in|im|bei|am)\s+(.+?)(?=\s+(?:@|#)|$)/i; - -// Patterns that look like dates/times but not locations (multilingual) -const NOT_LOCATION_PATTERN = - /^\d+\s*(tage?n?|wochen?|stunde[n]?|minute[n]?|hours?|minutes?|heures?|horas?|ore|h|min)$/i; - -function extractLocation(text: string): { location?: string; remaining: string } { - const match = text.match(LOCATION_PATTERN); - if (match) { - const location = match[1].trim(); - - // Skip if it looks like a leftover time/date expression - if (NOT_LOCATION_PATTERN.test(location)) { - return { location: undefined, remaining: text }; - } - - // Skip if starts with a number (likely a leftover numeric expression) - if (/^\d+\s/.test(location) && location.length < 5) { - return { location: undefined, remaining: text }; - } - - if (location.length >= 2) { - return { - location, - remaining: text.replace(LOCATION_PATTERN, '').trim(), - }; - } - } - return { location: undefined, remaining: text }; -} - -// ============================================================================ -// All-Day Detection -// ============================================================================ - -const ALL_DAY_PATTERNS: Record = { - de: [/\bganzt[aä]gig\b/i, /\bganzer\s+tag\b/i], - en: [/\ball[- ]?day\b/i, /\bwhole\s+day\b/i], - fr: [/\btoute\s+la\s+journ[eé]e\b/i, /\bjour\s+entier\b/i], - es: [/\btodo\s+el\s+d[ií]a\b/i, /\bd[ií]a\s+completo\b/i], - it: [/\btutto\s+il\s+giorno\b/i, /\bgiornata\s+intera\b/i], -}; - -function getAllDayPatterns(locale: ParserLocale): RegExp[] { - return ALL_DAY_PATTERNS[locale]; -} - -function extractAllDay( - text: string, - locale: ParserLocale = 'de' -): { isAllDay: boolean; remaining: string } { - for (const pattern of getAllDayPatterns(locale)) { - if (pattern.test(text)) { - return { - isAllDay: true, - remaining: text.replace(pattern, '').trim(), - }; - } - } - return { isAllDay: false, remaining: text }; -} - -// ============================================================================ -// Main Parser -// ============================================================================ - -/** - * Parse natural language event input - * - * Examples: - * - "Meeting morgen 14 Uhr 1h @Arbeit #wichtig" - * - "Arzttermin 15.12. 10:00 30min" - * - "Ganztägig Urlaub morgen" - */ -export function parseEventInput(input: string, locale: ParserLocale = 'de'): ParsedEvent { - let text = input.trim(); - - // Extract recurrence (before other extractions since "jeden Montag" could conflict with weekday) - const recurrenceResult = extractRecurrence(text, locale); - text = recurrenceResult.remaining; - const recurrenceRule = recurrenceResult.value; - - // Extract all-day flag - const allDayResult = extractAllDay(text, locale); - text = allDayResult.remaining; - const isAllDay = allDayResult.isAllDay; - - // Extract time range first (14-16 Uhr, 10:00-11:30) - const timeRangeResult = extractTimeRange(text); - text = timeRangeResult.remaining; - - // Extract duration (before base parser, since "30min" could conflict with time) - const durationResult = extractDuration(text, locale); - text = durationResult.remaining; - const duration = durationResult.duration; - - // Extract @references (first may be calendar, rest are attendees) - const atRefsResult = extractAtReferences(text); - text = atRefsResult.remaining; - const atRefs = atRefsResult.value ?? []; - const calendarName = atRefs.length > 0 ? atRefs[0] : undefined; - const attendees = atRefs.length > 1 ? atRefs.slice(1) : []; - - // Use base parser for common patterns (date, time, tags) - const base = parseBaseInput(text, locale); - - // Try to extract location from the remaining title - const locationResult = extractLocation(base.title); - const title = locationResult.location ? locationResult.remaining : base.title; - const location = locationResult.location; - - // Build start/end dates - let startDate: Date | undefined; - let endDate: Date | undefined; - - if (timeRangeResult.startTime && timeRangeResult.endTime) { - // Time range provided: use it directly - const dateForRange = base.date || new Date(); - startDate = combineDateAndTime(dateForRange, timeRangeResult.startTime); - endDate = combineDateAndTime(dateForRange, timeRangeResult.endTime); - } else { - // Single time or no time - startDate = combineDateAndTime(base.date, isAllDay ? undefined : base.time); - - if (startDate) { - if (isAllDay) { - endDate = new Date(startDate); - endDate.setHours(23, 59, 59); - } else if (duration) { - endDate = new Date(startDate.getTime() + duration * 60_000); - } else { - // Default: 1 hour - endDate = addHours(startDate, 1); - } - } - } - - return { - title, - startDate, - endDate, - duration, - isAllDay: isAllDay || undefined, - calendarName, - attendees, - location, - recurrenceRule, - tagNames: base.tagNames, - }; -} - -// ============================================================================ -// Multi-Event Splitting -// ============================================================================ - -const EVENT_SPLITTERS = - /\s*(?:,\s*(?:danach|dann|und dann|anschließend|außerdem|afterwards|then|and then|also)\s+|;\s*|\s+(?:danach|dann|und dann|anschließend|afterwards|then|and then)\s+)/i; - -/** - * Parse input that may contain multiple events separated by keywords. - * Subsequent events inherit date/time/calendar context from the first event. - * If the first event has a known end time, the next event starts there. - * - * Examples: - * - "Meeting 14 Uhr 1h danach Review 30min" → Meeting 14-15, Review 15-15:30 - * - "Morgen Workshop 9-12 Uhr @Arbeit; Mittagessen; Retro 1h" → 3 events - */ -export function parseMultiEventInput(input: string, locale: ParserLocale = 'de'): ParsedEvent[] { - const parts = input.split(EVENT_SPLITTERS).filter((s) => s.trim().length > 0); - - if (parts.length <= 1) { - return [parseEventInput(input, locale)]; - } - - const results: ParsedEvent[] = []; - let contextDate: Date | undefined; - let contextCalendar: string | undefined; - let lastEndDate: Date | undefined; - - for (let i = 0; i < parts.length; i++) { - const parsed = parseEventInput(parts[i].trim(), locale); - - if (i === 0) { - contextDate = parsed.startDate; - contextCalendar = parsed.calendarName; - lastEndDate = parsed.endDate; - } else { - // Inherit calendar - if (!parsed.calendarName && contextCalendar) { - parsed.calendarName = contextCalendar; - } - - // Inherit date/time: use lastEndDate as start if no explicit time - if (!parsed.startDate && lastEndDate) { - parsed.startDate = new Date(lastEndDate); - // Calculate endDate - if (parsed.duration) { - parsed.endDate = new Date(parsed.startDate.getTime() + parsed.duration * 60_000); - } else { - // Default 1h - parsed.endDate = addHours(parsed.startDate, 1); - } - } else if (!parsed.startDate && contextDate) { - // Fallback: same date, no specific time - parsed.startDate = new Date(contextDate); - if (parsed.duration) { - parsed.endDate = new Date(parsed.startDate.getTime() + parsed.duration * 60_000); - } else { - parsed.endDate = addHours(parsed.startDate, 1); - } - } - - lastEndDate = parsed.endDate; - } - - results.push(parsed); - } - - return results; -} - -// ============================================================================ -// ID Resolution -// ============================================================================ - -/** - * Resolve calendar and tag names to IDs - */ -export function resolveEventIds( - parsed: ParsedEvent, - calendars: Calendar[], - tags: Tag[], - defaultCalendarId?: string -): ParsedEventWithIds { - let calendarId: string | undefined; - const attendees: string[] = [...parsed.attendees]; - const tagIds: string[] = []; - - // Try to match first @ref as calendar (case-insensitive) - if (parsed.calendarName) { - const calendar = calendars.find( - (c) => c.name.toLowerCase() === parsed.calendarName!.toLowerCase() - ); - if (calendar) { - calendarId = calendar.id; - } else { - // First @ref didn't match a calendar, treat it as an attendee - attendees.unshift(parsed.calendarName); - } - } - - // Fallback to default calendar - if (!calendarId && defaultCalendarId) { - calendarId = defaultCalendarId; - } - - // Find tags by name (case-insensitive) - for (const tagName of parsed.tagNames) { - const tag = tags.find((t) => t.name.toLowerCase() === tagName.toLowerCase()); - if (tag) { - tagIds.push(tag.id); - } - } - - return { - title: parsed.title, - startTime: parsed.startDate?.toISOString(), - endTime: parsed.endDate?.toISOString(), - isAllDay: parsed.isAllDay, - calendarId, - attendees, - location: parsed.location, - recurrenceRule: parsed.recurrenceRule, - tagIds, - }; -} - -// ============================================================================ -// Preview Formatting -// ============================================================================ - -// Locale-specific "all-day" label for preview display -const ALL_DAY_LABEL: Record = { - de: 'ganztägig', - en: 'all-day', - fr: 'toute la journée', - es: 'todo el día', - it: 'tutto il giorno', -}; - -/** - * Format parsed event for preview display - */ -export function formatParsedEventPreview(parsed: ParsedEvent, locale: ParserLocale = 'de'): string { - const parts: string[] = []; - - if (parsed.isAllDay && parsed.startDate) { - parts.push(`📅 ${formatDatePreview(parsed.startDate, locale)} (${ALL_DAY_LABEL[locale]})`); - } else if (parsed.startDate) { - let dateStr = `📅 ${formatDatePreview(parsed.startDate, locale)}`; - if (parsed.startDate.getHours() !== 0 || parsed.startDate.getMinutes() !== 0) { - dateStr += ` ${formatTimePreview({ - hours: parsed.startDate.getHours(), - minutes: parsed.startDate.getMinutes(), - })}`; - } - parts.push(dateStr); - } - - if (parsed.duration) { - const hours = Math.floor(parsed.duration / 60); - const mins = parsed.duration % 60; - let durationStr = ''; - if (hours > 0) durationStr += `${hours}h`; - if (mins > 0) durationStr += `${mins}min`; - parts.push(`⏱️ ${durationStr}`); - } - - if (parsed.location) { - parts.push(`📍 ${parsed.location}`); - } - - if (parsed.recurrenceRule) { - parts.push(`🔄 ${parsed.recurrenceRule}`); - } - - if (parsed.calendarName) { - parts.push(`📆 ${parsed.calendarName}`); - } - - if (parsed.attendees.length > 0) { - parts.push(`👥 ${parsed.attendees.join(', ')}`); - } - - if (parsed.tagNames.length > 0) { - parts.push(`🏷️ ${parsed.tagNames.join(', ')}`); - } - - return parts.join(' · '); -} diff --git a/apps/calendar/apps/web-archived/src/lib/utils/eventDateHelpers.test.ts b/apps/calendar/apps/web-archived/src/lib/utils/eventDateHelpers.test.ts deleted file mode 100644 index 7120e98b2..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/eventDateHelpers.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { toDate, getEventStart, getEventEnd, getEventTimes } from './eventDateHelpers'; - -describe('toDate', () => { - it('should parse ISO string to Date', () => { - const result = toDate('2026-03-18T10:30:00Z'); - expect(result).toBeInstanceOf(Date); - expect(result.toISOString()).toBe('2026-03-18T10:30:00.000Z'); - }); - - it('should return Date object as-is', () => { - const date = new Date('2026-03-18T10:30:00Z'); - const result = toDate(date); - expect(result).toBe(date); - }); -}); - -describe('getEventStart', () => { - it('should extract start time from event with string', () => { - const event = { startTime: '2026-03-18T09:00:00Z' }; - const result = getEventStart(event); - expect(result).toBeInstanceOf(Date); - expect(result.toISOString()).toBe('2026-03-18T09:00:00.000Z'); - }); - - it('should handle Date object', () => { - const date = new Date('2026-03-18T09:00:00Z'); - const event = { startTime: date }; - const result = getEventStart(event); - expect(result).toBe(date); - }); -}); - -describe('getEventEnd', () => { - it('should extract end time from event with string', () => { - const event = { endTime: '2026-03-18T10:00:00Z' }; - const result = getEventEnd(event); - expect(result).toBeInstanceOf(Date); - expect(result.toISOString()).toBe('2026-03-18T10:00:00.000Z'); - }); - - it('should handle Date object', () => { - const date = new Date('2026-03-18T10:00:00Z'); - const event = { endTime: date }; - const result = getEventEnd(event); - expect(result).toBe(date); - }); -}); - -describe('getEventTimes', () => { - it('should return both start and end as Date objects', () => { - const event = { - startTime: '2026-03-18T09:00:00Z', - endTime: '2026-03-18T10:00:00Z', - }; - const result = getEventTimes(event); - expect(result.start).toBeInstanceOf(Date); - expect(result.end).toBeInstanceOf(Date); - expect(result.start.toISOString()).toBe('2026-03-18T09:00:00.000Z'); - expect(result.end.toISOString()).toBe('2026-03-18T10:00:00.000Z'); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/utils/eventDateHelpers.ts b/apps/calendar/apps/web-archived/src/lib/utils/eventDateHelpers.ts deleted file mode 100644 index 9e8063377..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/eventDateHelpers.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Event Date Helpers - * Utilities for consistent date handling across the calendar app - */ - -import { parseISO } from 'date-fns'; - -/** - * Convert a date value that may be either a string or Date to a Date object - * This handles the common pattern where API returns ISO strings but we need Date objects - */ -export function toDate(value: string | Date): Date { - return typeof value === 'string' ? parseISO(value) : value; -} - -/** - * Get the start time of an event as a Date object - */ -export function getEventStart(event: { startTime: string | Date }): Date { - return toDate(event.startTime); -} - -/** - * Get the end time of an event as a Date object - */ -export function getEventEnd(event: { endTime: string | Date }): Date { - return toDate(event.endTime); -} - -/** - * Get both start and end times of an event as Date objects - */ -export function getEventTimes(event: { startTime: string | Date; endTime: string | Date }): { - start: Date; - end: Date; -} { - return { - start: toDate(event.startTime), - end: toDate(event.endTime), - }; -} diff --git a/apps/calendar/apps/web-archived/src/lib/utils/eventFiltering.test.ts b/apps/calendar/apps/web-archived/src/lib/utils/eventFiltering.test.ts deleted file mode 100644 index 0e6844e99..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/eventFiltering.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - getVisibleCalendarIds, - filterByVisibleCalendars, - filterTimedEvents, - filterAllDayEvents, - getEventMinutes, - eventOverlapsTimeRange, - filterByHourRange, - getOverflowEvents, - filterByTags, -} from './eventFiltering'; - -// Mock calendars -const calendarA = { id: 'cal-a', name: 'Work', color: '#3B82F6' } as any; -const calendarB = { id: 'cal-b', name: 'Personal', color: '#EF4444' } as any; - -// Mock events - use local Date objects to avoid timezone offset issues with getHours() -const timedEventA = { - id: 'evt-1', - calendarId: 'cal-a', - startTime: new Date(2026, 2, 18, 9, 0, 0), - endTime: new Date(2026, 2, 18, 10, 0, 0), - isAllDay: false, - tags: [{ id: 'tag-1', name: 'meeting' }], -} as any; - -const timedEventB = { - id: 'evt-2', - calendarId: 'cal-b', - startTime: new Date(2026, 2, 18, 14, 0, 0), - endTime: new Date(2026, 2, 18, 15, 30, 0), - isAllDay: false, - tags: [{ id: 'tag-2', name: 'personal' }], -} as any; - -const allDayEvent = { - id: 'evt-3', - calendarId: 'cal-a', - startTime: new Date(2026, 2, 18, 0, 0, 0), - endTime: new Date(2026, 2, 19, 0, 0, 0), - isAllDay: true, - tags: [], -} as any; - -const earlyEvent = { - id: 'evt-4', - calendarId: 'cal-a', - startTime: new Date(2026, 2, 18, 5, 0, 0), - endTime: new Date(2026, 2, 18, 6, 0, 0), - isAllDay: false, - tags: null, -} as any; - -const lateEvent = { - id: 'evt-5', - calendarId: 'cal-a', - startTime: new Date(2026, 2, 18, 22, 0, 0), - endTime: new Date(2026, 2, 18, 23, 0, 0), - isAllDay: false, - tags: [{ id: 'tag-1', name: 'meeting' }], -} as any; - -const allEvents = [timedEventA, timedEventB, allDayEvent, earlyEvent, lateEvent]; - -describe('getVisibleCalendarIds', () => { - it('should return a Set of calendar IDs', () => { - const result = getVisibleCalendarIds([calendarA, calendarB]); - expect(result).toBeInstanceOf(Set); - expect(result.size).toBe(2); - expect(result.has('cal-a')).toBe(true); - expect(result.has('cal-b')).toBe(true); - }); -}); - -describe('filterByVisibleCalendars', () => { - it('should filter events to only visible calendars', () => { - const result = filterByVisibleCalendars(allEvents, [calendarA]); - expect(result.every((e) => e.calendarId === 'cal-a')).toBe(true); - expect(result).toHaveLength(4); - }); -}); - -describe('filterTimedEvents', () => { - it('should filter out all-day events', () => { - const result = filterTimedEvents(allEvents); - expect(result.every((e) => !e.isAllDay)).toBe(true); - expect(result).toHaveLength(4); - }); -}); - -describe('filterAllDayEvents', () => { - it('should keep only all-day events', () => { - const result = filterAllDayEvents(allEvents); - expect(result.every((e) => e.isAllDay)).toBe(true); - expect(result).toHaveLength(1); - expect(result[0].id).toBe('evt-3'); - }); -}); - -describe('getEventMinutes', () => { - it('should convert event times to minutes from midnight', () => { - const result = getEventMinutes(timedEventA); - // 09:00 UTC = 540 minutes - expect(result.start).toBe(9 * 60); - // 10:00 UTC = 600 minutes - expect(result.end).toBe(10 * 60); - }); -}); - -describe('eventOverlapsTimeRange', () => { - it('should return true when event overlaps with range', () => { - // Event is 09:00-10:00, range is 08:00-12:00 - const result = eventOverlapsTimeRange(timedEventA, 8 * 60, 12 * 60); - expect(result).toBe(true); - }); - - it('should return false when event is outside range', () => { - // Event is 09:00-10:00, range is 11:00-12:00 - const result = eventOverlapsTimeRange(timedEventA, 11 * 60, 12 * 60); - expect(result).toBe(false); - }); - - it('should return false when event ends exactly at range start', () => { - // Event is 09:00-10:00, range starts at 10:00 - const result = eventOverlapsTimeRange(timedEventA, 10 * 60, 12 * 60); - expect(result).toBe(false); - }); -}); - -describe('filterByHourRange', () => { - it('should filter events within the hour range', () => { - const timedEvents = filterTimedEvents(allEvents); - // Range 8:00-18:00 should include timedEventA (9-10) and timedEventB (14-15:30) - const result = filterByHourRange(timedEvents, 8, 18); - expect(result.some((e) => e.id === 'evt-1')).toBe(true); - expect(result.some((e) => e.id === 'evt-2')).toBe(true); - // earlyEvent (5-6) and lateEvent (22-23) should be excluded - expect(result.some((e) => e.id === 'evt-4')).toBe(false); - expect(result.some((e) => e.id === 'evt-5')).toBe(false); - }); -}); - -describe('getOverflowEvents', () => { - it('should return events before and after visible range', () => { - const timedEvents = filterTimedEvents(allEvents); - const result = getOverflowEvents(timedEvents, 8, 18); - - // earlyEvent (5-6) ends before 8:00 - expect(result.before.some((e) => e.id === 'evt-4')).toBe(true); - // lateEvent (22-23) starts after 18:00 - expect(result.after.some((e) => e.id === 'evt-5')).toBe(true); - // timedEventA (9-10) is within range, should not appear - expect(result.before.some((e) => e.id === 'evt-1')).toBe(false); - expect(result.after.some((e) => e.id === 'evt-1')).toBe(false); - }); -}); - -describe('filterByTags', () => { - it('should return all events when no tags are selected', () => { - const result = filterByTags(allEvents, []); - expect(result).toHaveLength(allEvents.length); - }); - - it('should filter events by selected tag IDs', () => { - const result = filterByTags(allEvents, ['tag-1']); - // timedEventA and lateEvent have tag-1 - expect(result).toHaveLength(2); - expect(result.some((e) => e.id === 'evt-1')).toBe(true); - expect(result.some((e) => e.id === 'evt-5')).toBe(true); - }); - - it('should exclude events with no tags when filtering', () => { - // allDayEvent has empty tags, earlyEvent has null tags - const result = filterByTags(allEvents, ['tag-2']); - expect(result.some((e) => e.id === 'evt-3')).toBe(false); - expect(result.some((e) => e.id === 'evt-4')).toBe(false); - }); -}); diff --git a/apps/calendar/apps/web-archived/src/lib/utils/eventFiltering.ts b/apps/calendar/apps/web-archived/src/lib/utils/eventFiltering.ts deleted file mode 100644 index 52ff744ad..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/eventFiltering.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Event Filtering Utilities - * Reusable functions for filtering calendar events by visibility, time range, etc. - */ - -import type { CalendarEvent } from '@calendar/shared'; -import type { Calendar } from '@calendar/shared'; -import { toDate } from './eventDateHelpers'; - -/** - * Create a Set of visible calendar IDs for efficient lookup - */ -export function getVisibleCalendarIds(visibleCalendars: Calendar[]): Set { - return new Set(visibleCalendars.map((c) => c.id)); -} - -/** - * Filter events to only include those from visible calendars - */ -export function filterByVisibleCalendars( - events: CalendarEvent[], - visibleCalendars: Calendar[] -): CalendarEvent[] { - const visibleIds = getVisibleCalendarIds(visibleCalendars); - return events.filter((e) => visibleIds.has(e.calendarId)); -} - -/** - * Filter events to only include timed (non-all-day) events - */ -export function filterTimedEvents(events: CalendarEvent[]): CalendarEvent[] { - return events.filter((e) => !e.isAllDay); -} - -/** - * Filter events to only include all-day events - */ -export function filterAllDayEvents(events: CalendarEvent[]): CalendarEvent[] { - return events.filter((e) => e.isAllDay); -} - -/** - * Get event time in minutes from midnight - */ -export function getEventMinutes(event: CalendarEvent): { start: number; end: number } { - const start = toDate(event.startTime); - const end = toDate(event.endTime); - return { - start: start.getHours() * 60 + start.getMinutes(), - end: end.getHours() * 60 + end.getMinutes(), - }; -} - -/** - * Check if an event overlaps with a given time range (in minutes from midnight) - */ -export function eventOverlapsTimeRange( - event: CalendarEvent, - startMinutes: number, - endMinutes: number -): boolean { - const { start: eventStart, end: eventEnd } = getEventMinutes(event); - return eventStart < endMinutes && eventEnd > startMinutes; -} - -/** - * Filter timed events that overlap with a visible hour range - */ -export function filterByHourRange( - events: CalendarEvent[], - dayStartHour: number, - dayEndHour: number -): CalendarEvent[] { - const startMinutes = dayStartHour * 60; - const endMinutes = dayEndHour * 60; - return events.filter((event) => eventOverlapsTimeRange(event, startMinutes, endMinutes)); -} - -/** - * Result type for overflow events - */ -export interface OverflowEvents { - before: CalendarEvent[]; - after: CalendarEvent[]; -} - -/** - * Get events that are outside the visible hour range - * Returns events that end before the visible range starts (before) - * and events that start after the visible range ends (after) - */ -export function getOverflowEvents( - events: CalendarEvent[], - dayStartHour: number, - dayEndHour: number -): OverflowEvents { - const startMinutes = dayStartHour * 60; - const endMinutes = dayEndHour * 60; - - const before: CalendarEvent[] = []; - const after: CalendarEvent[] = []; - - for (const event of events) { - const { start: eventStart, end: eventEnd } = getEventMinutes(event); - - if (eventEnd <= startMinutes) { - before.push(event); - } else if (eventStart >= endMinutes) { - after.push(event); - } - } - - return { before, after }; -} - -/** - * Combined filter: Get visible timed events for a day with optional hour and tag filtering - */ -export function getVisibleTimedEvents( - events: CalendarEvent[], - visibleCalendars: Calendar[], - options?: { - filterHoursEnabled?: boolean; - dayStartHour?: number; - dayEndHour?: number; - selectedTagIds?: string[]; - } -): CalendarEvent[] { - let filtered = filterByVisibleCalendars(events, visibleCalendars); - filtered = filterTimedEvents(filtered); - - if ( - options?.filterHoursEnabled && - options.dayStartHour !== undefined && - options.dayEndHour !== undefined - ) { - filtered = filterByHourRange(filtered, options.dayStartHour, options.dayEndHour); - } - - // Apply tag filter if tags are selected - if (options?.selectedTagIds) { - filtered = filterByTags(filtered, options.selectedTagIds); - } - - return filtered; -} - -/** - * Combined filter: Get visible all-day events for a day with optional tag filtering - */ -export function getVisibleAllDayEvents( - events: CalendarEvent[], - visibleCalendars: Calendar[], - options?: { - selectedTagIds?: string[]; - } -): CalendarEvent[] { - let filtered = filterByVisibleCalendars(events, visibleCalendars); - filtered = filterAllDayEvents(filtered); - - // Apply tag filter if tags are selected - if (options?.selectedTagIds) { - filtered = filterByTags(filtered, options.selectedTagIds); - } - - return filtered; -} - -/** - * Filter events by selected tag IDs - * If no tags are selected (empty array), returns all events - * If tags are selected, returns only events that have at least one of the selected tags - */ -export function filterByTags(events: CalendarEvent[], selectedTagIds: string[]): CalendarEvent[] { - // If no tags are selected, show all events - if (selectedTagIds.length === 0) { - return events; - } - - const selectedTagSet = new Set(selectedTagIds); - - return events.filter((event) => { - // If event has no tags, don't show it when filtering by tags - if (!event.tags || event.tags.length === 0) { - return false; - } - - // Check if event has at least one of the selected tags - return event.tags.some((tag) => selectedTagSet.has(tag.id)); - }); -} - -/** - * Combined filter: Get overflow events for visible calendars - */ -export function getVisibleOverflowEvents( - events: CalendarEvent[], - visibleCalendars: Calendar[], - dayStartHour: number, - dayEndHour: number -): OverflowEvents { - let filtered = filterByVisibleCalendars(events, visibleCalendars); - filtered = filterTimedEvents(filtered); - return getOverflowEvents(filtered, dayStartHour, dayEndHour); -} diff --git a/apps/calendar/apps/web-archived/src/lib/utils/syntax-help.ts b/apps/calendar/apps/web-archived/src/lib/utils/syntax-help.ts deleted file mode 100644 index f4fe5c8dc..000000000 --- a/apps/calendar/apps/web-archived/src/lib/utils/syntax-help.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Calendar-specific syntax help patterns for InputBar help modal - */ -import type { SyntaxGroup } from '@manacore/shared-ui'; - -export const CALENDAR_SYNTAX: SyntaxGroup[] = [ - { - title: 'Kalender-Termin', - items: [ - { - pattern: 'Dauer', - description: 'Termindauer angeben', - examples: ['1h', '30min', '2h30m', '1 Stunde'], - color: 'accent', - }, - { - pattern: 'Zeitbereich', - description: 'Start- und Endzeit', - examples: ['14-16 Uhr', '10:00-11:30', '9-17 Uhr'], - color: 'accent', - }, - { - pattern: 'Ganztägig', - description: 'Ganztägiger Termin', - examples: ['ganztägig', 'ganzer Tag'], - color: 'warning', - }, - { - pattern: 'Ort', - description: 'Ort angeben', - examples: ['in Berlin', 'im Büro', 'bei Dr. Müller'], - color: 'success', - }, - { - pattern: 'Wiederholung', - description: 'Wiederkehrende Termine', - examples: ['täglich', 'wöchentlich', 'jeden Montag', 'monatlich'], - color: 'warning-soft', - }, - { - pattern: '@Kalender', - description: 'Kalender zuweisen', - examples: ['@Arbeit', '@Privat'], - color: 'success', - }, - { - pattern: '@Teilnehmer', - description: 'Teilnehmer hinzufügen', - examples: ['@Max', '@Anna'], - color: 'success', - }, - ], - }, -]; - -export const CALENDAR_LIVE_EXAMPLE = { - text: 'Teammeeting morgen 14-16 Uhr wöchentlich @Arbeit #wichtig', - highlights: [ - { type: 'text' as const, content: 'Teammeeting ' }, - { type: 'date' as const, content: 'morgen' }, - { type: 'text' as const, content: ' ' }, - { type: 'time' as const, content: '14-16 Uhr' }, - { type: 'text' as const, content: ' ' }, - { type: 'date' as const, content: 'wöchentlich' }, - { type: 'text' as const, content: ' ' }, - { type: 'reference' as const, content: '@Arbeit' }, - { type: 'text' as const, content: ' ' }, - { type: 'tag' as const, content: '#wichtig' }, - ], -}; diff --git a/apps/calendar/apps/web-archived/src/lib/version.ts b/apps/calendar/apps/web-archived/src/lib/version.ts deleted file mode 100644 index 4f221f09f..000000000 --- a/apps/calendar/apps/web-archived/src/lib/version.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const APP_VERSION = '1.0.0'; -export const BUILD_TIME: string = - typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : new Date().toISOString(); -export const BUILD_HASH: string = typeof __BUILD_HASH__ !== 'undefined' ? __BUILD_HASH__ : 'dev'; diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/+layout.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/+layout.svelte deleted file mode 100644 index 2265af077..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/+layout.svelte +++ /dev/null @@ -1,751 +0,0 @@ - - - - - - -
- - Zum Inhalt springen - - - - {#if !settingsStore.immersiveModeEnabled} - - {/if} - - - - - - - - - settingsStore.toggleImmersiveMode()} - visible={showCalendarToolbar} - /> - -
-
- {@render children()} -
-
-
-
- - - - - - (showSettingsModal = false)} /> - - - {#if calendarOnboarding.shouldShow} - - {/if} - - - (showGuestWelcome = false)} - onLogin={() => goto('/login')} - onRegister={() => goto('/register')} - locale={($locale || 'de') === 'de' ? 'de' : 'en'} - /> - - {#if authStore.isAuthenticated} - - {/if} - -
- - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/+page.svelte deleted file mode 100644 index e23ca53ba..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/+page.svelte +++ /dev/null @@ -1,217 +0,0 @@ - - - - {$_('app.name')} - - - { - if (viewStore.viewType !== 'agenda') return; - const target = e.target as HTMLElement; - const isInQuickInput = target.closest('.quick-input-bar'); - if (isInQuickInput && (e.key === 'ArrowUp' || (e.key === 'Tab' && !e.shiftKey))) { - const firstTitle = document.querySelector( - '.agenda-event-title[contenteditable]' - ); - if (firstTitle) { - e.preventDefault(); - firstTitle.focus(); - } - } - }} -/> - -
- {#if settingsStore.showBirthdays} - birthdaysStore.fetchBirthdays(true)} - /> - {/if} -
- -
-
-
- -
-
- - {#if showQuickOverlay} - {#key overlayKey} - - {/key} - {/if} -
- - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/event/[id]/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/event/[id]/+page.svelte deleted file mode 100644 index 9cb62d949..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/event/[id]/+page.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/feedback/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/feedback/+page.svelte deleted file mode 100644 index cced1cd54..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/feedback/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - Feedback | Kalender - - - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/help/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/help/+page.svelte deleted file mode 100644 index ba8073a43..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/help/+page.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - {translations.title} | Kalender - - - goto('/')} - showGettingStarted={false} - showChangelog={false} - defaultSection="faq" -/> diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/mana/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/mana/+page.svelte deleted file mode 100644 index 586762862..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/mana/+page.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - - - Mana - Kalender - - -
- -
- - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/profile/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/profile/+page.svelte deleted file mode 100644 index 122acf5eb..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/profile/+page.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/settings/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/settings/+page.svelte deleted file mode 100644 index 07a3f76ec..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/settings/+page.svelte +++ /dev/null @@ -1,610 +0,0 @@ - - - - {$_('nav.settings')} | {$_('app.name')} - - -
- - - - - {#snippet icon()} - - {/snippet} - -
- -
-
-
- - - - {#snippet icon()} - - {/snippet} - -
-

- {$_('settings.externalCalendarsDesc')} -

- - {$_('settings.manageSync')} - -
-
-
- - - - {#snippet icon()} - - {/snippet} - -
-

- {$_('settings.sharesDesc')} -

- - {$_('settings.manageShares')} - -
-
-
- - - - - - - {#snippet icon()} - - {/snippet} - -
-
-
- {$_('settings.defaultView')} - {$_('settings.defaultViewDesc')} -
- handleViewChange(v as CalendarViewType)} - placeholder={$_('settings.selectView')} - /> -
- -
-
- {$_('settings.timeFormat')} - {$_('settings.timeFormatDesc')} -
-
- - -
-
- -
- -
- -
- -
- -
- -
- - {#if settingsStore.filterHoursEnabled} -
-
- {$_('settings.visibleHours')} - {$_('settings.visibleHoursDesc')} -
-
-
- {$_('settings.hoursFrom')} - settingsStore.set('dayStartHour', Number(v))} - placeholder={$_('event.start')} - /> -
- -
- {$_('settings.hoursTo')} - settingsStore.set('dayEndHour', Number(v))} - placeholder={$_('event.end')} - /> -
-
-
- {/if} - -
-
- {$_('settings.allDayEvents')} - {$_('settings.allDayEventsDesc')} -
-
- - -
-
-
-
-
- - - - {#snippet icon()} - - {/snippet} - -
-
-
- {$_('settings.defaultDuration')} - {$_('settings.defaultDurationDesc')} -
- handleEventDurationChange(Number(v))} - placeholder={$_('settings.selectDuration')} - /> -
- -
-
- Smarte Dauer - Dauer automatisch aus vergangenen Terminen lernen -
- -
- -
-
- {$_('settings.defaultReminder')} - {$_('settings.defaultReminderDesc')} -
- handleReminderChange(Number(v))} - placeholder={$_('settings.selectReminder')} - /> -
-
-
-
- - - - {#snippet icon()} - - {/snippet} - -
-
- -
- - {#if settingsStore.showBirthdays} -
- -
- {/if} -
-
-
- - - - {#snippet icon()} - - {/snippet} - -
-
-
- {$_('auth.email')} - {authStore.user?.email || '-'} -
-
- -
- -
-
-
-
- -

v{APP_VERSION}

-
- - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/settings/sharing/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/settings/sharing/+page.svelte deleted file mode 100644 index 674ef07f2..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/settings/sharing/+page.svelte +++ /dev/null @@ -1,537 +0,0 @@ - - - - {$_('sharing.pageTitle')} - - -
-
- - - -

{$_('sharing.title')}

- -
- - - {#if sharesStore.invitations.length > 0} -
-

- - {$_('sharing.invitations', { values: { count: sharesStore.invitations.length } })} -

- {#each sharesStore.invitations as invite (invite.id)} - - {/each} -
- {/if} - - - {#if sharesStore.sharedWithMe.length > 0} -
-

- - {$_('sharing.sharedWithMe')} -

- {#each sharesStore.sharedWithMe as share (share.id)} - - {/each} -
- {/if} - - -
-

- - {$_('sharing.shareMyCalendars')} -

- - {#each calendarsCtx.value as calendar (calendar.id)} -
- - - {#if viewingCalendarId === calendar.id} - {@const calShares = sharesStore.getSharesForCalendar(calendar.id)} -
- {#if calShares.length === 0} -

{$_('sharing.notSharedYet')}

- {:else} - {#each calShares as share (share.id)} - - {/each} - {/if} - - -
- {/if} -
- {/each} -
-
- - - (showShareForm = false)} - title={$_('sharing.shareCalendar')} - maxWidth="sm" -> - - - {#snippet footer()} - - {/snippet} - - - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/settings/sync/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/settings/sync/+page.svelte deleted file mode 100644 index 8070b7b43..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/settings/sync/+page.svelte +++ /dev/null @@ -1,898 +0,0 @@ - - - - {$_('sync.pageTitle')} - - -
-
- - - -

{$_('sync.title')}

- -
- -

- {$_('sync.description')} -

- - {#if externalCalendarsStore.error} - - {/if} - - {#if externalCalendarsStore.loading} -
-
-
- {:else if externalCalendarsStore.calendars.length === 0} -
- -

{$_('sync.emptyState')}

- -
- {:else} -
- {#each externalCalendarsStore.calendars as cal (cal.id)} -
-
-
-
-
-

{cal.name}

- {PROVIDER_INFO[cal.provider]?.label || cal.provider} -
-
-
- - -
-
- -
-
- {$_('sync.directionLabel')} - - {#if cal.syncDirection === 'import'} - - {:else if cal.syncDirection === 'export'} - - {:else} - - {/if} - {getSyncDirectionLabel(cal.syncDirection)} - -
-
- {$_('sync.lastSync')} - - {formatSyncTime(cal.lastSyncAt)} - -
-
- {$_('sync.statusLabel')} - - {#if cal.lastSyncError} - - - {$_('sync.status.error')} - - {:else if cal.syncEnabled} - - - {$_('sync.status.active', { values: { interval: cal.syncInterval } })} - - {:else} - {$_('sync.status.paused')} - {/if} - -
- {#if cal.lastSyncError} -
- {cal.lastSyncError} -
- {/if} -
- - -
- {/each} -
- {/if} -
- - - - {#if connectStep === 'provider'} -
- {#each providers as provider} - - {/each} -
- {:else if connectStep === 'caldav-discover'} -
-
- - -
-
- - -
-
- - -
- - {#if discoveredCalendars.length > 0} -
-

{$_('sync.discoveredCalendars')}

- {#each discoveredCalendars as cal} - - {/each} -
- {/if} -
- - {#snippet footer()} - - {/snippet} - {:else if connectStep === 'credentials'} -
-
- - -
-
- - -
- {#if selectedProvider !== 'ical_url'} -
- - -
-
- - -
- {/if} -
- - -
-
- - {#snippet footer()} - - {/snippet} - {/if} -
- - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/tags/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/tags/+page.svelte deleted file mode 100644 index 68d60298c..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/tags/+page.svelte +++ /dev/null @@ -1,399 +0,0 @@ - - - - Tags - Kalender - - -
- -
- - - -

Tags

- -
- - -
- - -
- - {#if false} - - {/if} - - - {#if false} -
-
-
- {:else if filteredTags.length === 0} -
-
- {searchQuery ? 'Keine Tags gefunden' : 'Keine Tags vorhanden'} -
-
- {:else} -
- {#each filteredTags as tag (tag.id)} - - {/each} -
- {/if} - - {#if !false && tagsCtx.value.length > 0} -

- {tagsCtx.value.length} - {tagsCtx.value.length === 1 ? 'Tag' : 'Tags'} -

- {/if} - - {#if !false && tagsCtx.value.length === 0 && !searchQuery} -
- -
- {/if} -
- - - -
-
- -
- -
- Farbe - (tagColor = c)} /> -
- -
- Vorschau -
- -
-
-
- - {#snippet footer()} -
-
- {#if editingTag} - - {/if} -
-
- - -
-
- {/snippet} -
- - diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/themes/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/themes/+page.svelte deleted file mode 100644 index de8a397fa..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/themes/+page.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - Themes | Calendar - - - theme.setVariant(v)} - showModeSelector={true} - currentMode={theme.mode} - onModeChange={(m) => theme.setMode(m)} - showBackButton={true} - onBack={() => goto('/')} -/> diff --git a/apps/calendar/apps/web-archived/src/routes/(app)/unified-bar-demo/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(app)/unified-bar-demo/+page.svelte deleted file mode 100644 index 60c98d20c..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(app)/unified-bar-demo/+page.svelte +++ /dev/null @@ -1,191 +0,0 @@ - - - - UnifiedBar Demo - Calendar - - -
-
-

UnifiedBar Demo

-

Demonstration der unified bottom bar Architektur

-
- - -
-
-

Kalender Inhalt

-

Dies ist der Hauptinhaltbereich, in dem die Kalender-Ansichten angezeigt werden.

- -
-
-

Sichtbare Layers

-
    - {#if unifiedBarStore.showQuickInput}
  • QuickInput
  • {/if} - {#if unifiedBarStore.showDateStrip}
  • DateStrip
  • {/if} - {#if unifiedBarStore.showTagStrip}
  • TagStrip
  • {/if} - {#if unifiedBarStore.showCalendarToolbar}
  • CalendarToolbar
  • {/if} -
-
- -
-

Overlay Status

-

{unifiedBarStore.isOverlayOpen ? 'Offen' : 'Geschlossen'}

-
-
- -
- - - - - -
-
-
- - - -
- - diff --git a/apps/calendar/apps/web-archived/src/routes/(auth)/+layout.svelte b/apps/calendar/apps/web-archived/src/routes/(auth)/+layout.svelte deleted file mode 100644 index a54cfdcb7..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(auth)/+layout.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -{@render children()} diff --git a/apps/calendar/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte deleted file mode 100644 index 4edba8aa2..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - - {translations.titleForm} | Kalender - - - - {#snippet headerControls()} - - {/snippet} - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/calendar/apps/web-archived/src/routes/(auth)/login/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(auth)/login/+page.svelte deleted file mode 100644 index 563249623..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(auth)/login/+page.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - {translations.title} | Kalender - - - authStore.signInWithPasskey()} - onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)} - onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)} - onSendMagicLink={(email) => authStore.sendMagicLink(email)} - {goto} - successRedirect={redirectTo} - registerPath="/register" - forgotPasswordPath="/forgot-password" - lightBackground="#e0f2fe" - darkBackground="#0c1929" - {translations} - {verified} - {initialEmail} - version={APP_VERSION} - buildTime={BUILD_TIME} -> - {#snippet headerControls()} - - {/snippet} - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/calendar/apps/web-archived/src/routes/(auth)/register/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(auth)/register/+page.svelte deleted file mode 100644 index 0c4682bb3..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(auth)/register/+page.svelte +++ /dev/null @@ -1,61 +0,0 @@ - - - - {translations.title} | Kalender - - - - {#snippet headerControls()} - - {/snippet} - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/calendar/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte b/apps/calendar/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte deleted file mode 100644 index fe6b028e2..000000000 --- a/apps/calendar/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte +++ /dev/null @@ -1,231 +0,0 @@ - - - - {t.title} | Kalender - - -
- -
- - - Kalender - - -
- - -
-
-
-

{t.title}

-

- {#if success} - {t.success} - {:else if hasToken} - {t.subtitle} - {:else} - {t.invalidToken} - {/if} -

-
- - {#if success} - -
-
-
-

- {t.successMessage} -

- - {t.goToLogin} - -
-
- {:else if hasToken} - -
-
- {#if error} -
- {error} -
- {/if} - -
-
- - -

- {t.minChars} -

-
- -
- - -
- - -
-
-
- {:else} - -
-
-
⚠️
-

- {t.invalidTokenMessage} -

- - {t.requestNew} - -
-
- {/if} -
-
-
diff --git a/apps/calendar/apps/web-archived/src/routes/+error.svelte b/apps/calendar/apps/web-archived/src/routes/+error.svelte deleted file mode 100644 index 5b9e9cd2a..000000000 --- a/apps/calendar/apps/web-archived/src/routes/+error.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -
-

{$page.status}

-

{$page.error?.message || $_('error.notFound')}

- {$_('error.backToHome')} -
diff --git a/apps/calendar/apps/web-archived/src/routes/+layout.svelte b/apps/calendar/apps/web-archived/src/routes/+layout.svelte deleted file mode 100644 index f8d969b87..000000000 --- a/apps/calendar/apps/web-archived/src/routes/+layout.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - -{#if !appReady} - -{:else} -
- {@render children()} -
-{/if} - - diff --git a/apps/calendar/apps/web-archived/src/routes/+layout.ts b/apps/calendar/apps/web-archived/src/routes/+layout.ts deleted file mode 100644 index ad6cddb06..000000000 --- a/apps/calendar/apps/web-archived/src/routes/+layout.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Disable SSR — all data is local-first (IndexedDB + mana-sync) -export const ssr = false; diff --git a/apps/calendar/apps/web-archived/src/routes/health/+server.ts b/apps/calendar/apps/web-archived/src/routes/health/+server.ts deleted file mode 100644 index 9222c1d9f..000000000 --- a/apps/calendar/apps/web-archived/src/routes/health/+server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; - -export const GET: RequestHandler = async () => { - return json({ - status: 'ok', - service: 'calendar-web', - timestamp: new Date().toISOString(), - }); -}; diff --git a/apps/calendar/apps/web-archived/src/routes/offline/+page.svelte b/apps/calendar/apps/web-archived/src/routes/offline/+page.svelte deleted file mode 100644 index 76a8833c3..000000000 --- a/apps/calendar/apps/web-archived/src/routes/offline/+page.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/calendar/apps/web-archived/src/routes/offline/+page.ts b/apps/calendar/apps/web-archived/src/routes/offline/+page.ts deleted file mode 100644 index 189f71e2e..000000000 --- a/apps/calendar/apps/web-archived/src/routes/offline/+page.ts +++ /dev/null @@ -1 +0,0 @@ -export const prerender = true; diff --git a/apps/calendar/apps/web-archived/static/apple-touch-icon.png b/apps/calendar/apps/web-archived/static/apple-touch-icon.png deleted file mode 100644 index 4e86574d5..000000000 Binary files a/apps/calendar/apps/web-archived/static/apple-touch-icon.png and /dev/null differ diff --git a/apps/calendar/apps/web-archived/static/favicon.png b/apps/calendar/apps/web-archived/static/favicon.png deleted file mode 100644 index bd550906b..000000000 Binary files a/apps/calendar/apps/web-archived/static/favicon.png and /dev/null differ diff --git a/apps/calendar/apps/web-archived/static/favicon.svg b/apps/calendar/apps/web-archived/static/favicon.svg deleted file mode 100644 index bb9d53400..000000000 --- a/apps/calendar/apps/web-archived/static/favicon.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 - diff --git a/apps/calendar/apps/web-archived/static/pwa-192x192.png b/apps/calendar/apps/web-archived/static/pwa-192x192.png deleted file mode 100644 index a0db9cb61..000000000 Binary files a/apps/calendar/apps/web-archived/static/pwa-192x192.png and /dev/null differ diff --git a/apps/calendar/apps/web-archived/static/pwa-512x512.png b/apps/calendar/apps/web-archived/static/pwa-512x512.png deleted file mode 100644 index d7e2f04b1..000000000 Binary files a/apps/calendar/apps/web-archived/static/pwa-512x512.png and /dev/null differ diff --git a/apps/calendar/apps/web-archived/svelte.config.js b/apps/calendar/apps/web-archived/svelte.config.js deleted file mode 100644 index a7a917e4c..000000000 --- a/apps/calendar/apps/web-archived/svelte.config.js +++ /dev/null @@ -1,14 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter({ - out: 'build', - }), - }, -}; - -export default config; diff --git a/apps/calendar/apps/web-archived/tsconfig.json b/apps/calendar/apps/web-archived/tsconfig.json deleted file mode 100644 index a8f10c8e3..000000000 --- a/apps/calendar/apps/web-archived/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } -} diff --git a/apps/calendar/apps/web-archived/vite.config.ts b/apps/calendar/apps/web-archived/vite.config.ts deleted file mode 100644 index 89245d998..000000000 --- a/apps/calendar/apps/web-archived/vite.config.ts +++ /dev/null @@ -1,40 +0,0 @@ -/// -import { sveltekit } from '@sveltejs/kit/vite'; -import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vite'; -import { SvelteKitPWA } from '@vite-pwa/sveltekit'; -import { createOfflineFirstPWAConfig } from '@manacore/shared-pwa'; -import { MANACORE_SHARED_PACKAGES, getBuildDefines } from '@manacore/shared-vite-config'; - -export default defineConfig({ - plugins: [ - tailwindcss(), - sveltekit(), - SvelteKitPWA( - createOfflineFirstPWAConfig({ - name: 'Calendar - Kalender', - shortName: 'Calendar', - description: 'Kalender und Terminverwaltung', - themeColor: '#3b82f6', - }) - ), - ], - server: { - port: 5179, - strictPort: true, - }, - ssr: { - noExternal: [...MANACORE_SHARED_PACKAGES, '@calendar/shared'], - }, - optimizeDeps: { - exclude: [...MANACORE_SHARED_PACKAGES, '@calendar/shared'], - }, - test: { - environment: 'jsdom', - include: ['src/**/*.test.ts'], - globals: true, - }, - define: { - ...getBuildDefines(), - }, -}); diff --git a/apps/cards/apps/web-archived/.env.example b/apps/cards/apps/web-archived/.env.example deleted file mode 100644 index 447e28aa6..000000000 --- a/apps/cards/apps/web-archived/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -# Supabase -PUBLIC_SUPABASE_URL=https://your-project.supabase.co -PUBLIC_SUPABASE_ANON_KEY=your-anon-key - -# Mana Core Backend -PUBLIC_API_URL=https://cards-backend-111768794939.europe-west3.run.app - -# App Config -PUBLIC_APP_NAME=Cards -PUBLIC_APP_URL=http://localhost:5173 diff --git a/apps/cards/apps/web-archived/.gitignore b/apps/cards/apps/web-archived/.gitignore deleted file mode 100644 index 3b462cb0c..000000000 --- a/apps/cards/apps/web-archived/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -node_modules - -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/apps/cards/apps/web-archived/.npmrc b/apps/cards/apps/web-archived/.npmrc deleted file mode 100644 index b6f27f135..000000000 --- a/apps/cards/apps/web-archived/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/apps/cards/apps/web-archived/Dockerfile b/apps/cards/apps/web-archived/Dockerfile deleted file mode 100644 index cd6260680..000000000 --- a/apps/cards/apps/web-archived/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM sveltekit-base:local AS builder - -ARG PUBLIC_BACKEND_URL=http://cards-server -ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001 -ARG PUBLIC_API_URL=http://cards-server -ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL -ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL -ENV PUBLIC_API_URL=$PUBLIC_API_URL - -COPY apps/cards/apps/web ./apps/cards/apps/web - -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ - pnpm install --no-frozen-lockfile --ignore-scripts - -WORKDIR /app/apps/cards/apps/web -RUN pnpm exec svelte-kit sync -RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build - -FROM node:20-alpine AS production -WORKDIR /app/apps/cards/apps/web -COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm -COPY --from=builder /app/apps/cards/apps/web/node_modules ./node_modules -COPY --from=builder /app/apps/cards/apps/web/build ./build -COPY --from=builder /app/apps/cards/apps/web/package.json ./ - -EXPOSE 5015 -ENV NODE_ENV=production PORT=5015 HOST=0.0.0.0 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:5015/health || exit 1 - -CMD ["node", "build"] diff --git a/apps/cards/apps/web-archived/README.md b/apps/cards/apps/web-archived/README.md deleted file mode 100644 index 75842c404..000000000 --- a/apps/cards/apps/web-archived/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```sh -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```sh -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```sh -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/apps/cards/apps/web-archived/eslint.config.js b/apps/cards/apps/web-archived/eslint.config.js deleted file mode 100644 index f0e51b62e..000000000 --- a/apps/cards/apps/web-archived/eslint.config.js +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-check -import { - baseConfig, - typescriptConfig, - svelteConfig, - prettierConfig, -} from '@manacore/eslint-config'; - -export default [ - { - ignores: ['dist/**', '.svelte-kit/**', 'node_modules/**'], - }, - ...baseConfig, - ...typescriptConfig, - ...svelteConfig, - ...prettierConfig, -]; diff --git a/apps/cards/apps/web-archived/package.json b/apps/cards/apps/web-archived/package.json deleted file mode 100644 index 83c4295ee..000000000 --- a/apps/cards/apps/web-archived/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@cards/web", - "private": true, - "version": "0.2.0", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "eslint ." - }, - "devDependencies": { - "@manacore/shared-pwa": "workspace:*", - "@manacore/shared-vite-config": "workspace:*", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.47.1", - "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.7", - "@vite-pwa/sveltekit": "^1.1.0", - "autoprefixer": "^10.4.22", - "postcss": "^8.5.6", - "svelte": "^5.41.0", - "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "vite": "^7.1.10" - }, - "dependencies": { - "@manacore/local-store": "workspace:*", - "@manacore/shared-app-onboarding": "workspace:*", - "@manacore/shared-auth": "workspace:*", - "@manacore/shared-auth-stores": "workspace:*", - "@manacore/shared-auth-ui": "workspace:*", - "@manacore/shared-branding": "workspace:*", - "@manacore/shared-config": "workspace:*", - "@manacore/shared-error-tracking": "workspace:*", - "@manacore/feedback": "workspace:*", - "@manacore/shared-i18n": "workspace:*", - "@manacore/help": "workspace:*", - "@manacore/shared-icons": "workspace:*", - "@manacore/shared-profile-ui": "workspace:*", - "@manacore/shared-stores": "workspace:*", - "@manacore/shared-tags": "workspace:*", - "@manacore/subscriptions": "workspace:*", - "@manacore/shared-tailwind": "workspace:*", - "@manacore/shared-theme": "workspace:*", - "@manacore/shared-theme-ui": "workspace:*", - "@manacore/shared-types": "workspace:*", - "@manacore/shared-ui": "workspace:*", - "@manacore/shared-utils": "workspace:*", - "svelte-i18n": "^4.0.1" - } -} diff --git a/apps/cards/apps/web-archived/src/app.css b/apps/cards/apps/web-archived/src/app.css deleted file mode 100644 index 56ff68272..000000000 --- a/apps/cards/apps/web-archived/src/app.css +++ /dev/null @@ -1,11 +0,0 @@ -@import "tailwindcss"; -@import "@manacore/shared-tailwind/themes.css"; - -/* Scan shared packages for Tailwind classes */ -@source "../../../../packages/shared-ui/src"; -@source "../../../../packages/shared-auth-ui/src"; -@source "../../../../packages/shared-branding/src"; -@source "../../../../packages/shared-theme-ui/src"; -@source "../../../../packages/shared-theme-ui/src/components"; -@source "../../../../packages/shared-theme-ui/src/pages"; -@source "../../../../packages/shared-subscription-ui/src"; diff --git a/apps/cards/apps/web-archived/src/app.d.ts b/apps/cards/apps/web-archived/src/app.d.ts deleted file mode 100644 index 8e8e79928..000000000 --- a/apps/cards/apps/web-archived/src/app.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -declare const __BUILD_HASH__: string; -declare const __BUILD_TIME__: string; - -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/apps/cards/apps/web-archived/src/app.html b/apps/cards/apps/web-archived/src/app.html deleted file mode 100644 index f273cc58f..000000000 --- a/apps/cards/apps/web-archived/src/app.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/cards/apps/web-archived/src/hooks.client.ts b/apps/cards/apps/web-archived/src/hooks.client.ts deleted file mode 100644 index f6c32237e..000000000 --- a/apps/cards/apps/web-archived/src/hooks.client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { initErrorTracking, handleSvelteError } from '@manacore/shared-error-tracking/browser'; -import type { HandleClientError } from '@sveltejs/kit'; - -initErrorTracking({ - serviceName: 'cards-web', - dsn: (window as any).__PUBLIC_GLITCHTIP_DSN__, - environment: import.meta.env.MODE, -}); - -export const handleError: HandleClientError = ({ error }) => { - handleSvelteError(error); -}; diff --git a/apps/cards/apps/web-archived/src/hooks.server.ts b/apps/cards/apps/web-archived/src/hooks.server.ts deleted file mode 100644 index 461480e2b..000000000 --- a/apps/cards/apps/web-archived/src/hooks.server.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; -import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; - -const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = - process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; -const PUBLIC_BACKEND_URL_CLIENT = - process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; -const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; - -export const handle: Handle = async ({ event, resolve }) => { - const response = await resolve(event, { - transformPageChunk: ({ html }) => { - const envScript = ``; - return html.replace('', `${envScript}`); - }, - }); - - setSecurityHeaders(response, { - connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], - }); - - return response; -}; diff --git a/apps/cards/apps/web-archived/src/lib/api/feedback.ts b/apps/cards/apps/web-archived/src/lib/api/feedback.ts deleted file mode 100644 index 700ddd8a0..000000000 --- a/apps/cards/apps/web-archived/src/lib/api/feedback.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Feedback Service Instance for Cards Web App - */ - -import { createFeedbackService } from '@manacore/feedback'; -import { authService } from '$lib/auth'; -import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; - -const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; - -export const feedbackService = createFeedbackService({ - apiUrl: MANA_AUTH_URL, - appId: 'cards', - getAuthToken: async () => authService.getAppToken(), -}); diff --git a/apps/cards/apps/web-archived/src/lib/assets/favicon.svg b/apps/cards/apps/web-archived/src/lib/assets/favicon.svg deleted file mode 100644 index cc5dc66a3..000000000 --- a/apps/cards/apps/web-archived/src/lib/assets/favicon.svg +++ /dev/null @@ -1 +0,0 @@ -svelte-logo \ No newline at end of file diff --git a/apps/cards/apps/web-archived/src/lib/auth.ts b/apps/cards/apps/web-archived/src/lib/auth.ts deleted file mode 100644 index 68c3296e5..000000000 --- a/apps/cards/apps/web-archived/src/lib/auth.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Cards Web Auth Configuration - * - * This file initializes the shared auth package for the cards web app. - * It replaces the previous individual auth files: - * - services/authService.ts - * - services/tokenManager.ts - * - services/deviceManager.ts - * - utils/jwt.ts - */ - -import { PUBLIC_API_URL, PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; -import { - createAuthService, - createTokenManager, - setStorageAdapter, - setDeviceAdapter, - setNetworkAdapter, - setupFetchInterceptor, - type StorageAdapter, - type DeviceManagerAdapter, - type NetworkAdapter, - type DeviceInfo, -} from '@manacore/shared-auth'; - -// Storage keys -const STORAGE_KEYS = { - APP_TOKEN: 'appToken', - REFRESH_TOKEN: 'refreshToken', - USER_EMAIL: 'userEmail', - DEVICE_ID: 'cards_device_id', -}; - -/** - * Session storage adapter for cards web - * Uses sessionStorage for tokens (clears on tab close) - * Uses localStorage for device ID (persists) - */ -const sessionStorageAdapter: StorageAdapter = { - async getItem(key: string): Promise { - if (typeof window === 'undefined') return null; - - const value = sessionStorage.getItem(key); - if (value === null) return null; - - try { - return JSON.parse(value) as T; - } catch { - return value as T; - } - }, - - async setItem(key: string, value: string): Promise { - if (typeof window === 'undefined') return; - sessionStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); - }, - - async removeItem(key: string): Promise { - if (typeof window === 'undefined') return; - sessionStorage.removeItem(key); - }, -}; - -/** - * Device manager adapter for web - */ -const webDeviceAdapter: DeviceManagerAdapter = { - async getDeviceInfo(): Promise { - if (typeof window === 'undefined') { - return { - deviceId: '', - deviceName: 'Server', - deviceType: 'web', - }; - } - - const deviceId = (await webDeviceAdapter.getStoredDeviceId()) || generateDeviceId(); - localStorage.setItem(STORAGE_KEYS.DEVICE_ID, deviceId); - - const userAgent = navigator.userAgent; - let deviceName = 'Web Browser'; - - if (userAgent.includes('Mac')) deviceName = 'Mac'; - else if (userAgent.includes('Windows')) deviceName = 'Windows'; - else if (userAgent.includes('Linux')) deviceName = 'Linux'; - - return { - deviceId, - deviceName, - deviceType: 'web', - platform: 'web', - }; - }, - - async getStoredDeviceId(): Promise { - if (typeof window === 'undefined') return null; - return localStorage.getItem(STORAGE_KEYS.DEVICE_ID); - }, -}; - -/** - * Network adapter for web - */ -const webNetworkAdapter: NetworkAdapter = { - async isDeviceConnected(): Promise { - if (typeof navigator === 'undefined') return true; - return navigator.onLine; - }, - - async hasStableConnection(): Promise { - if (typeof navigator === 'undefined') return true; - return navigator.onLine; - }, -}; - -/** - * Generate a unique device ID - */ -function generateDeviceId(): string { - return `web_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; -} - -// Initialize adapters -setStorageAdapter(sessionStorageAdapter); -setDeviceAdapter(webDeviceAdapter); -setNetworkAdapter(webNetworkAdapter); - -// Create auth service instance -export const authService = createAuthService({ - baseUrl: PUBLIC_MANA_CORE_AUTH_URL, - storageKeys: { - APP_TOKEN: STORAGE_KEYS.APP_TOKEN, - REFRESH_TOKEN: STORAGE_KEYS.REFRESH_TOKEN, - USER_EMAIL: STORAGE_KEYS.USER_EMAIL, - }, - endpoints: { - signIn: '/api/v1/auth/login', - signUp: '/api/v1/auth/register', - signOut: '/api/v1/auth/logout', - refresh: '/api/v1/auth/refresh', - validate: '/api/v1/auth/validate', - forgotPassword: '/api/v1/auth/forgot-password', - credits: '/api/v1/credits/balance', - }, -}); - -// Create token manager instance -export const tokenManager = createTokenManager(authService); - -// Setup fetch interceptor (only in browser) -if (typeof window !== 'undefined') { - setupFetchInterceptor(authService, tokenManager, { - backendUrl: PUBLIC_API_URL, - }); -} - -// Re-export useful utilities from shared-auth -export { - decodeToken, - isTokenValidLocally, - isTokenExpired, - getUserFromToken, - isB2BUser, - getB2BInfo, - TokenState, -} from '@manacore/shared-auth'; - -// Re-export types -export type { - UserData, - DecodedToken, - AuthResult, - CreditBalance, - B2BInfo, -} from '@manacore/shared-auth'; diff --git a/apps/cards/apps/web-archived/src/lib/components/AppSlider.svelte b/apps/cards/apps/web-archived/src/lib/components/AppSlider.svelte deleted file mode 100644 index c62f48f95..000000000 --- a/apps/cards/apps/web-archived/src/lib/components/AppSlider.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/apps/cards/apps/web-archived/src/lib/components/Icon.svelte b/apps/cards/apps/web-archived/src/lib/components/Icon.svelte deleted file mode 100644 index 967807016..000000000 --- a/apps/cards/apps/web-archived/src/lib/components/Icon.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -{#if path} - -{:else} - -{/if} diff --git a/apps/cards/apps/web-archived/src/lib/components/LanguageSelector.svelte b/apps/cards/apps/web-archived/src/lib/components/LanguageSelector.svelte deleted file mode 100644 index 5630e7582..000000000 --- a/apps/cards/apps/web-archived/src/lib/components/LanguageSelector.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/apps/cards/apps/web-archived/src/lib/components/deck/CreateDeckModal.svelte b/apps/cards/apps/web-archived/src/lib/components/deck/CreateDeckModal.svelte deleted file mode 100644 index f71705fc9..000000000 --- a/apps/cards/apps/web-archived/src/lib/components/deck/CreateDeckModal.svelte +++ /dev/null @@ -1,108 +0,0 @@ - - - -
{ - e.preventDefault(); - handleSubmit(); - }} - class="space-y-4" - > - - -
- - -
- - - -
- - -
- - {#if deckStore.error} -
- {deckStore.error} -
- {/if} - -
- - -
-
-
diff --git a/apps/cards/apps/web-archived/src/lib/components/deck/DeckCard.svelte b/apps/cards/apps/web-archived/src/lib/components/deck/DeckCard.svelte deleted file mode 100644 index 264fb3a8f..000000000 --- a/apps/cards/apps/web-archived/src/lib/components/deck/DeckCard.svelte +++ /dev/null @@ -1,59 +0,0 @@ - - - -
- -

{deck.title}

- - - {#if deck.description} -

- {deck.description} -

- {/if} - - - {#if deck.tags && deck.tags.length > 0} -
- {#each deck.tags.slice(0, 3) as tag} - {tag} - {/each} - {#if deck.tags.length > 3} - +{deck.tags.length - 3} - {/if} -
- {/if} - - -
-
- 📇 {deck.card_count || 0} cards - {#if deck.is_public} - Public - {/if} -
- {formatDate(deck.updated_at)} -
-
-
diff --git a/apps/cards/apps/web-archived/src/lib/content/help/index.test.ts b/apps/cards/apps/web-archived/src/lib/content/help/index.test.ts deleted file mode 100644 index 701d3b176..000000000 --- a/apps/cards/apps/web-archived/src/lib/content/help/index.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { getCardsHelpContent } from './index'; - -describe('Cards Help Content', () => { - it('returns valid German content', () => { - const content = getCardsHelpContent('de'); - - expect(content.faq.length).toBeGreaterThan(0); - content.faq.forEach((faq) => { - expect(faq.id).toBeTruthy(); - expect(faq.question).toBeTruthy(); - expect(faq.answer).toBeTruthy(); - }); - - expect(content.features).toBeDefined(); - expect(content.contact).toBeDefined(); - expect(content.contact.supportEmail).toBe('support@mana.how'); - }); - - it('returns valid English content', () => { - const content = getCardsHelpContent('en'); - - expect(content.faq.length).toBeGreaterThan(0); - content.faq.forEach((faq) => { - expect(faq.id).toBeTruthy(); - expect(faq.question).toBeTruthy(); - expect(faq.answer).toBeTruthy(); - }); - - expect(content.features).toBeDefined(); - expect(content.contact).toBeDefined(); - }); - - it('returns same number of FAQ items for both languages', () => { - const de = getCardsHelpContent('de'); - const en = getCardsHelpContent('en'); - - expect(de.faq.length).toBe(en.faq.length); - expect(de.features.length).toBe(en.features.length); - }); - - it('has unique FAQ IDs', () => { - const content = getCardsHelpContent('de'); - const ids = content.faq.map((f) => f.id); - expect(new Set(ids).size).toBe(ids.length); - }); -}); diff --git a/apps/cards/apps/web-archived/src/lib/content/help/index.ts b/apps/cards/apps/web-archived/src/lib/content/help/index.ts deleted file mode 100644 index f065e67d1..000000000 --- a/apps/cards/apps/web-archived/src/lib/content/help/index.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Help content for Cards app - */ - -import type { HelpContent } from '@manacore/help'; -import { getPrivacyFAQs } from '@manacore/help'; - -export function getCardsHelpContent(locale: string): HelpContent { - const isDE = locale === 'de'; - - return { - faq: [ - { - id: 'faq-create-decks', - question: isDE ? 'Wie erstelle ich Decks und Karten?' : 'How do I create decks and cards?', - answer: isDE - ? '

So erstellst du Decks und Karten in Cards:

  1. Klicke auf Neues Deck und gib einen Namen und eine Beschreibung ein
  2. Öffne das Deck und klicke auf Karte hinzufügen
  3. Gib die Vorderseite (Frage) und Rückseite (Antwort) ein
  4. Optional: Füge Bilder, Tags oder Notizen hinzu

Du kannst auch mehrere Karten auf einmal importieren — siehe Import & Export.

' - : '

Here is how to create decks and cards in Cards:

  1. Click New Deck and enter a name and description
  2. Open the deck and click Add Card
  3. Enter the front (question) and back (answer)
  4. Optional: Add images, tags, or notes

You can also import multiple cards at once — see Import & Export.

', - category: 'features', - order: 1, - language: isDE ? 'de' : 'en', - tags: isDE ? ['deck', 'karten', 'erstellen'] : ['deck', 'cards', 'create'], - }, - { - id: 'faq-spaced-repetition', - question: isDE ? 'Wie funktioniert Spaced Repetition?' : 'How does spaced repetition work?', - answer: isDE - ? '

Spaced Repetition (verteiltes Wiederholen) ist eine wissenschaftlich fundierte Lernmethode:

  • Karten, die du gut kannst, werden in größeren Abständen gezeigt
  • Karten, die du noch lernst, erscheinen häufiger
  • Der Algorithmus passt sich automatisch an dein Lerntempo an
  • Nach jeder Wiederholung bewertest du dein Wissen: Nochmal, Schwer, Gut oder Leicht

So lernst du effizienter und behältst Wissen langfristig.

' - : '

Spaced repetition is a scientifically proven learning method:

  • Cards you know well are shown at increasing intervals
  • Cards you are still learning appear more frequently
  • The algorithm automatically adapts to your learning pace
  • After each review, rate your knowledge: Again, Hard, Good, or Easy

This way you learn more efficiently and retain knowledge long-term.

', - category: 'features', - order: 2, - language: isDE ? 'de' : 'en', - tags: isDE - ? ['spaced-repetition', 'lernen', 'algorithmus', 'wiederholen'] - : ['spaced-repetition', 'learning', 'algorithm', 'review'], - }, - { - id: 'faq-study-sessions', - question: isDE ? 'Wie starte ich eine Lernsitzung?' : 'How do I start a study session?', - answer: isDE - ? '

So startest du eine Lernsitzung:

  1. Wähle ein Deck aus deiner Bibliothek
  2. Klicke auf Lernen — die fälligen Karten werden automatisch ausgewählt
  3. Für jede Karte: Lies die Frage, überlege die Antwort, decke die Rückseite auf
  4. Bewerte dein Wissen mit den Buttons: Nochmal, Schwer, Gut, Leicht

Cards zeigt dir an, wie viele Karten neu, fällig und zu wiederholen sind.

' - : '

Here is how to start a study session:

  1. Select a deck from your library
  2. Click Study — due cards are automatically selected
  3. For each card: Read the question, think of the answer, reveal the back
  4. Rate your knowledge with the buttons: Again, Hard, Good, Easy

Cards shows you how many cards are new, due, and to review.

', - category: 'features', - order: 3, - language: isDE ? 'de' : 'en', - tags: isDE ? ['lernen', 'sitzung', 'wiederholen'] : ['study', 'session', 'review'], - }, - { - id: 'faq-import-export', - question: isDE - ? 'Kann ich Karten importieren und exportieren?' - : 'Can I import and export cards?', - answer: isDE - ? '

Cards unterstützt verschiedene Import- und Exportformate:

  • CSV: Importiere Karten aus Tabellenkalkulationen (Vorderseite, Rückseite, Tags)
  • Anki-Format: Importiere bestehende Anki-Decks (.apkg)
  • JSON: Für programmatischen Zugriff und Backup
  • Export: Exportiere einzelne Decks oder deine gesamte Bibliothek
' - : '

Cards supports various import and export formats:

  • CSV: Import cards from spreadsheets (front, back, tags)
  • Anki format: Import existing Anki decks (.apkg)
  • JSON: For programmatic access and backup
  • Export: Export individual decks or your entire library
', - category: 'technical', - order: 4, - language: isDE ? 'de' : 'en', - tags: isDE ? ['import', 'export', 'csv', 'anki'] : ['import', 'export', 'csv', 'anki'], - }, - ...getPrivacyFAQs(locale, { dataTypeDE: 'Karten', dataTypeEN: 'cards' }), - ], - features: [ - { - id: 'feature-deck-management', - title: isDE ? 'Deck-Verwaltung' : 'Deck Management', - description: isDE - ? 'Erstelle, organisiere und verwalte deine Karteikarten-Decks' - : 'Create, organize, and manage your flashcard decks', - icon: '🃏', - category: 'core', - highlights: isDE - ? ['Decks & Unterdecks', 'Tags & Kategorien', 'Bilder auf Karten', 'Deck-Statistiken'] - : ['Decks & sub-decks', 'Tags & categories', 'Images on cards', 'Deck statistics'], - content: '', - order: 1, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-spaced-repetition', - title: 'Spaced Repetition', - description: isDE - ? 'Wissenschaftlich fundierter Algorithmus für effizientes Lernen' - : 'Scientifically proven algorithm for efficient learning', - icon: '🧠', - category: 'core', - highlights: isDE - ? ['Adaptiver Algorithmus', 'Optimale Intervalle', 'Lernstatistiken', 'Tägliche Ziele'] - : ['Adaptive algorithm', 'Optimal intervals', 'Learning statistics', 'Daily goals'], - content: '', - order: 2, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-study-sessions', - title: isDE ? 'Lernsitzungen' : 'Study Sessions', - description: isDE - ? 'Starte fokussierte Lernsitzungen mit fälligen Karten' - : 'Start focused study sessions with due cards', - icon: '📖', - category: 'core', - highlights: isDE - ? ['Fällige Karten', 'Bewertungssystem', 'Fortschrittsanzeige', 'Sitzungsstatistiken'] - : ['Due cards', 'Rating system', 'Progress indicator', 'Session statistics'], - content: '', - order: 3, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-import-export', - title: 'Import & Export', - description: isDE - ? 'Importiere aus CSV, Anki und mehr — exportiere jederzeit' - : 'Import from CSV, Anki, and more — export anytime', - icon: '📦', - category: 'advanced', - highlights: isDE - ? ['CSV-Import', 'Anki-Kompatibilität', 'JSON-Export', 'Bibliotheks-Backup'] - : ['CSV import', 'Anki compatibility', 'JSON export', 'Library backup'], - content: '', - order: 4, - language: isDE ? 'de' : 'en', - }, - ], - shortcuts: [], - gettingStarted: [], - changelog: [], - contact: { - id: 'contact-support', - title: isDE ? 'Support kontaktieren' : 'Contact Support', - content: isDE - ? '

Unser Support-Team hilft dir bei allen Fragen rund um Cards.

' - : '

Our support team is here to help you with any questions about Cards.

', - language: isDE ? 'de' : 'en', - order: 1, - supportEmail: 'support@mana.how', - documentationUrl: 'https://mana.how/docs', - responseTime: isDE ? 'Normalerweise innerhalb von 24 Stunden' : 'Usually within 24 hours', - }, - }; -} diff --git a/apps/cards/apps/web-archived/src/lib/data/guest-seed.ts b/apps/cards/apps/web-archived/src/lib/data/guest-seed.ts deleted file mode 100644 index 2f69b3fc8..000000000 --- a/apps/cards/apps/web-archived/src/lib/data/guest-seed.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Guest seed data for the Cards app. - * - * These records are loaded into IndexedDB when a new guest visits the app. - * They serve as onboarding content that teaches the user how the app works. - */ - -import type { LocalDeck, LocalCard } from './local-store'; - -const ONBOARDING_DECK_ID = 'onboarding-deck'; - -export const guestDecks: LocalDeck[] = [ - { - id: ONBOARDING_DECK_ID, - name: 'Erste Schritte', - description: 'Lerne Cards kennen mit diesen Beispiel-Karteikarten.', - color: '#6366f1', - cardCount: 3, - isPublic: false, - }, -]; - -export const guestCards: LocalCard[] = [ - { - id: 'card-1', - deckId: ONBOARDING_DECK_ID, - front: 'Was ist Cards?', - back: 'Cards ist eine Karteikarten-App zum effizienten Lernen mit Spaced Repetition.', - difficulty: 1, - reviewCount: 0, - order: 0, - }, - { - id: 'card-2', - deckId: ONBOARDING_DECK_ID, - front: 'Wie funktioniert Spaced Repetition?', - back: 'Karten, die du gut kennst, werden seltener gezeigt. Schwierige Karten erscheinen häufiger, bis du sie beherrschst.', - difficulty: 2, - reviewCount: 0, - order: 1, - }, - { - id: 'card-3', - deckId: ONBOARDING_DECK_ID, - front: 'Wie erstelle ich ein neues Deck?', - back: 'Klicke auf den + Button auf der Decks-Seite, um ein neues Deck mit eigenen Karteikarten zu erstellen.', - difficulty: 1, - reviewCount: 0, - order: 2, - }, -]; diff --git a/apps/cards/apps/web-archived/src/lib/data/local-store.ts b/apps/cards/apps/web-archived/src/lib/data/local-store.ts deleted file mode 100644 index 3a689409a..000000000 --- a/apps/cards/apps/web-archived/src/lib/data/local-store.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Cards — Local-First Data Layer - * - * Defines the IndexedDB database, collections, and guest seed data. - * This is the single source of truth for all Cards data. - */ - -import { createLocalStore, type BaseRecord } from '@manacore/local-store'; -import { guestDecks, guestCards } from './guest-seed'; - -// ─── Types ────────────────────────────────────────────────── - -export interface LocalDeck extends BaseRecord { - name: string; - description?: string | null; - color: string; - cardCount: number; - lastStudied?: string | null; - isPublic: boolean; -} - -export interface LocalCard extends BaseRecord { - deckId: string; - front: string; - back: string; - difficulty: number; // 1-5 - nextReview?: string | null; - reviewCount: number; - order: number; -} - -// ─── Store ────────────────────────────────────────────────── - -const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050'; - -export const cardsStore = createLocalStore({ - appId: 'cards', - collections: [ - { - name: 'decks', - indexes: ['isPublic'], - guestSeed: guestDecks, - }, - { - name: 'cards', - indexes: ['deckId', 'difficulty', 'nextReview', 'order', '[deckId+order]'], - guestSeed: guestCards, - }, - ], - sync: { - serverUrl: SYNC_SERVER_URL, - }, -}); - -// Typed collection accessors -export const deckCollection = cardsStore.collection('decks'); -export const cardCollection = cardsStore.collection('cards'); diff --git a/apps/cards/apps/web-archived/src/lib/data/queries.ts b/apps/cards/apps/web-archived/src/lib/data/queries.ts deleted file mode 100644 index 98e036ec9..000000000 --- a/apps/cards/apps/web-archived/src/lib/data/queries.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Reactive Queries & Pure Helpers for Cards - * - * Uses Dexie liveQuery to automatically re-render when IndexedDB changes - * (local writes, sync updates, other tabs). Components call these hooks - * at init time; no manual fetch/refresh needed. - */ - -import { useLiveQueryWithDefault } from '@manacore/local-store/svelte'; -import { deckCollection, cardCollection, type LocalDeck, type LocalCard } from './local-store'; -import type { Deck } from '$lib/types/deck'; -import type { Card } from '$lib/types/card'; - -// ─── Type Converters ─────────────────────────────────────── - -export function toDeck(local: LocalDeck): Deck { - return { - id: local.id, - user_id: 'guest', - title: local.name, - description: local.description ?? undefined, - is_public: local.isPublic, - settings: {}, - tags: [], - metadata: {}, - created_at: local.createdAt ?? new Date().toISOString(), - updated_at: local.updatedAt ?? new Date().toISOString(), - card_count: local.cardCount, - }; -} - -export function toCard(local: LocalCard): Card { - return { - id: local.id, - deck_id: local.deckId, - position: local.order, - title: local.front, - content: { - front: local.front, - back: local.back, - }, - card_type: 'flashcard', - version: 1, - is_favorite: false, - created_at: local.createdAt ?? new Date().toISOString(), - updated_at: local.updatedAt ?? new Date().toISOString(), - }; -} - -// ─── Live Query Hooks (call during component init) ───────── - -/** All decks, auto-updates on any change. */ -export function useAllDecks() { - return useLiveQueryWithDefault(async () => { - const locals = await deckCollection.getAll(); - return locals.map(toDeck); - }, [] as Deck[]); -} - -/** All cards for a specific deck, sorted by order. Auto-updates on any change. */ -export function useCardsByDeck(deckId: string) { - return useLiveQueryWithDefault(async () => { - const locals = await cardCollection.getAll( - { deckId }, - { sortBy: 'order', sortDirection: 'asc' } - ); - return locals.map(toCard); - }, [] as Card[]); -} - -/** Single deck by ID. Auto-updates on any change. */ -export function useDeck(deckId: string) { - return useLiveQueryWithDefault( - async () => { - const local = await deckCollection.get(deckId); - return local ? toDeck(local) : null; - }, - null as Deck | null - ); -} - -// ─── Pure Helper Functions ───────────────────────────────── - -export function getDeckById(decks: Deck[], id: string): Deck | undefined { - return decks.find((d) => d.id === id); -} - -export function getPublicDecks(decks: Deck[]): Deck[] { - return decks.filter((d) => d.is_public); -} diff --git a/apps/cards/apps/web-archived/src/lib/i18n/index.ts b/apps/cards/apps/web-archived/src/lib/i18n/index.ts deleted file mode 100644 index 69ad3c858..000000000 --- a/apps/cards/apps/web-archived/src/lib/i18n/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { browser } from '$app/environment'; -import { init, register, locale, waitLocale } from 'svelte-i18n'; - -// Register all available locales -register('de', () => import('./locales/de.json')); -register('en', () => import('./locales/en.json')); -register('it', () => import('./locales/it.json')); -register('fr', () => import('./locales/fr.json')); -register('es', () => import('./locales/es.json')); - -// List of supported locales -export const supportedLocales = ['de', 'en', 'it', 'fr', 'es'] as const; -export type SupportedLocale = (typeof supportedLocales)[number]; - -// Default locale -const defaultLocale = 'de'; - -// Get initial locale from browser or localStorage -function getInitialLocale(): SupportedLocale { - if (browser) { - // Check localStorage first - const stored = localStorage.getItem('locale'); - if (stored && supportedLocales.includes(stored as SupportedLocale)) { - return stored as SupportedLocale; - } - - // Fall back to browser language - const browserLang = navigator.language.split('-')[0]; - if (supportedLocales.includes(browserLang as SupportedLocale)) { - return browserLang as SupportedLocale; - } - } - - return defaultLocale; -} - -// Initialize i18n at module scope (required for SSR) -init({ - fallbackLocale: defaultLocale, - initialLocale: getInitialLocale(), -}); - -// Also export initI18n for backwards compatibility -export function initI18n() { - init({ - fallbackLocale: defaultLocale, - initialLocale: getInitialLocale(), - }); -} - -// Set locale and persist to localStorage -export function setLocale(newLocale: SupportedLocale) { - locale.set(newLocale); - if (browser) { - localStorage.setItem('locale', newLocale); - } -} - -// Wait for locale to be loaded (useful for SSR) -export { waitLocale }; diff --git a/apps/cards/apps/web-archived/src/lib/i18n/locales/de.json b/apps/cards/apps/web-archived/src/lib/i18n/locales/de.json deleted file mode 100644 index d62ee3174..000000000 --- a/apps/cards/apps/web-archived/src/lib/i18n/locales/de.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "common": { - "save": "Speichern", - "cancel": "Abbrechen", - "delete": "Löschen", - "back": "Zurück", - "loading": "Lädt..." - }, - "app_slider": { - "title": "Teil des Mana Ökosystems", - "memoro_desc": "KI-gestützte Sprachnotizen", - "memoro_long_desc": "Erfasse deine Gedanken durch Sprache und lasse sie von KI in strukturierte Notizen verwandeln.", - "maerchenzauber_desc": "Magische Kindergeschichten", - "maerchenzauber_long_desc": "Erstelle personalisierte Kindergeschichten mit KI-generierten Illustrationen.", - "cards_desc": "KI Lernkarten", - "cards_long_desc": "Erstelle und lerne mit smarten Lernkarten und KI-gestützter Wiederholung.", - "moodlit_desc": "Stimmungslicht-Steuerung", - "moodlit_long_desc": "Steuere deine smarten Lichter basierend auf deiner Stimmung und Aktivität.", - "manacore_desc": "Zentrale Verwaltung", - "manacore_long_desc": "Verwalte alle deine Mana-Apps und Einstellungen an einem Ort.", - "status_published": "Verfügbar", - "status_beta": "Beta", - "status_development": "In Entwicklung", - "status_planning": "Geplant", - "coming_soon": "Bald verfügbar", - "download": "App öffnen" - } -} diff --git a/apps/cards/apps/web-archived/src/lib/i18n/locales/en.json b/apps/cards/apps/web-archived/src/lib/i18n/locales/en.json deleted file mode 100644 index 8beb1ac63..000000000 --- a/apps/cards/apps/web-archived/src/lib/i18n/locales/en.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "common": { - "save": "Save", - "cancel": "Cancel", - "delete": "Delete", - "back": "Back", - "loading": "Loading..." - }, - "app_slider": { - "title": "Part of the Mana Ecosystem", - "memoro_desc": "AI-powered voice notes", - "memoro_long_desc": "Capture your thoughts through voice and let AI transform them into structured notes.", - "maerchenzauber_desc": "Magical children's stories", - "maerchenzauber_long_desc": "Create personalized children's stories with AI-generated illustrations.", - "cards_desc": "AI Flashcards", - "cards_long_desc": "Create and study with smart flashcards and AI-powered spaced repetition.", - "moodlit_desc": "Mood light control", - "moodlit_long_desc": "Control your smart lights based on your mood and activity.", - "manacore_desc": "Central management", - "manacore_long_desc": "Manage all your Mana apps and settings in one place.", - "status_published": "Available", - "status_beta": "Beta", - "status_development": "In Development", - "status_planning": "Planned", - "coming_soon": "Coming Soon", - "download": "Open App" - } -} diff --git a/apps/cards/apps/web-archived/src/lib/i18n/locales/es.json b/apps/cards/apps/web-archived/src/lib/i18n/locales/es.json deleted file mode 100644 index 1bc965ef1..000000000 --- a/apps/cards/apps/web-archived/src/lib/i18n/locales/es.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "common": { - "save": "Guardar", - "cancel": "Cancelar", - "delete": "Eliminar", - "back": "Atrás", - "loading": "Cargando..." - }, - "app_slider": { - "title": "Parte del ecosistema Mana", - "memoro_desc": "Notas de voz con IA", - "memoro_long_desc": "Captura tus pensamientos con voz y deja que la IA los transforme en notas estructuradas.", - "maerchenzauber_desc": "Historias mágicas para niños", - "maerchenzauber_long_desc": "Crea historias personalizadas para niños con ilustraciones generadas por IA.", - "cards_desc": "Flashcards IA", - "cards_long_desc": "Crea y estudia con flashcards inteligentes y repetición espaciada con IA.", - "moodlit_desc": "Control de luces ambientales", - "moodlit_long_desc": "Controla tus luces inteligentes según tu estado de ánimo y actividades.", - "manacore_desc": "Gestión central", - "manacore_long_desc": "Gestiona todas tus apps Mana y configuraciones en un solo lugar.", - "status_published": "Disponible", - "status_beta": "Beta", - "status_development": "En Desarrollo", - "status_planning": "Planificado", - "coming_soon": "Próximamente", - "download": "Abrir App" - } -} diff --git a/apps/cards/apps/web-archived/src/lib/i18n/locales/fr.json b/apps/cards/apps/web-archived/src/lib/i18n/locales/fr.json deleted file mode 100644 index ff07f2cfe..000000000 --- a/apps/cards/apps/web-archived/src/lib/i18n/locales/fr.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "common": { - "save": "Enregistrer", - "cancel": "Annuler", - "delete": "Supprimer", - "back": "Retour", - "loading": "Chargement..." - }, - "app_slider": { - "title": "Partie de l'écosystème Mana", - "memoro_desc": "Notes vocales IA", - "memoro_long_desc": "Capturez vos pensées par la voix et laissez l'IA les transformer en notes structurées.", - "maerchenzauber_desc": "Histoires magiques pour enfants", - "maerchenzauber_long_desc": "Créez des histoires personnalisées pour enfants avec des illustrations générées par l'IA.", - "cards_desc": "Flashcards IA", - "cards_long_desc": "Créez et étudiez avec des flashcards intelligentes et la répétition espacée assistée par IA.", - "moodlit_desc": "Contrôle d'éclairage ambiant", - "moodlit_long_desc": "Contrôlez vos lumières intelligentes en fonction de votre humeur et de vos activités.", - "manacore_desc": "Gestion centrale", - "manacore_long_desc": "Gérez toutes vos applications Mana et paramètres en un seul endroit.", - "status_published": "Disponible", - "status_beta": "Bêta", - "status_development": "En Développement", - "status_planning": "Planifié", - "coming_soon": "Bientôt", - "download": "Ouvrir l'App" - } -} diff --git a/apps/cards/apps/web-archived/src/lib/i18n/locales/it.json b/apps/cards/apps/web-archived/src/lib/i18n/locales/it.json deleted file mode 100644 index 8dfb2b7e3..000000000 --- a/apps/cards/apps/web-archived/src/lib/i18n/locales/it.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "common": { - "save": "Salva", - "cancel": "Annulla", - "delete": "Elimina", - "back": "Indietro", - "loading": "Caricamento..." - }, - "app_slider": { - "title": "Parte dell'ecosistema Mana", - "memoro_desc": "Note vocali AI", - "memoro_long_desc": "Cattura i tuoi pensieri con la voce e lascia che l'AI li trasformi in note strutturate.", - "maerchenzauber_desc": "Storie magiche per bambini", - "maerchenzauber_long_desc": "Crea storie personalizzate per bambini con illustrazioni generate dall'AI.", - "cards_desc": "Flashcard AI", - "cards_long_desc": "Crea e studia con flashcard intelligenti e ripetizione spaziata basata su AI.", - "moodlit_desc": "Controllo luci ambientali", - "moodlit_long_desc": "Controlla le tue luci smart in base al tuo umore e alle tue attività.", - "manacore_desc": "Gestione centrale", - "manacore_long_desc": "Gestisci tutte le tue app Mana e impostazioni in un unico posto.", - "status_published": "Disponibile", - "status_beta": "Beta", - "status_development": "In Sviluppo", - "status_planning": "Pianificato", - "coming_soon": "Prossimamente", - "download": "Apri App" - } -} diff --git a/apps/cards/apps/web-archived/src/lib/index.ts b/apps/cards/apps/web-archived/src/lib/index.ts deleted file mode 100644 index 856f2b6c3..000000000 --- a/apps/cards/apps/web-archived/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/apps/cards/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts b/apps/cards/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts deleted file mode 100644 index 1c84db461..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/app-onboarding.svelte.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { createAppOnboardingStore, type AppOnboardingStep } from '@manacore/shared-app-onboarding'; -import { userSettings } from './user-settings.svelte'; - -/** - * Cards-specific onboarding steps - */ -const cardsOnboardingSteps: AppOnboardingStep[] = [ - { - id: 'features', - type: 'info', - question: 'Willkommen bei Cards!', - description: 'Das kann Cards für dich tun:', - emoji: '🃏', - gradient: { from: 'blue-500', to: 'blue-700' }, - bullets: [ - 'Kartendecks erstellen & lernen', - 'Spaced Repetition für optimales Lernen', - 'Fortschritt tracken', - 'Verschiedene Kartentypen', - ], - }, - { - id: 'startAction', - type: 'select', - question: 'Wie möchtest du starten?', - description: 'Du kannst alles jederzeit nachholen.', - emoji: '🃏', - gradient: { from: 'blue-500', to: 'blue-700' }, - options: [ - { - id: 'create', - label: 'Neues Deck erstellen', - description: 'Starte mit eigenen Lernkarten', - emoji: '✏️', - }, - { - id: 'explore', - label: 'Decks entdecken', - description: 'Finde geteilte Lernsets', - emoji: '🔍', - }, - { - id: 'later', - label: 'Erstmal umschauen', - description: 'Die App in Ruhe erkunden', - emoji: '👀', - }, - ], - defaultValue: 'later', - }, - { - id: 'welcome', - type: 'info', - question: 'Cards ist bereit!', - description: 'Hier sind einige Tipps:', - emoji: '🎉', - gradient: { from: 'primary', to: 'primary/70' }, - bullets: [ - 'Erstelle Decks mit Lernkarten für Spaced Repetition', - 'Nutze die tägliche Lernrunde für optimales Lernen', - 'Entdecke öffentliche Decks anderer Nutzer', - "Drücke 'F' für den Fokus-Modus beim Lernen", - ], - }, -]; - -/** - * Cards app onboarding store - */ -export const cardsOnboarding = createAppOnboardingStore({ - appId: 'cards', - steps: cardsOnboardingSteps, - userSettings, - onComplete: async () => {}, - onSkip: async () => {}, -}); diff --git a/apps/cards/apps/web-archived/src/lib/stores/auth.svelte.ts b/apps/cards/apps/web-archived/src/lib/stores/auth.svelte.ts deleted file mode 100644 index 5df932791..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/auth.svelte.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Auth Store — uses centralized Mana auth factory. - */ - -import { createManaAuthStore } from '@manacore/shared-auth-stores'; - -export const authStore = createManaAuthStore(); diff --git a/apps/cards/apps/web-archived/src/lib/stores/cardStore.svelte.ts b/apps/cards/apps/web-archived/src/lib/stores/cardStore.svelte.ts deleted file mode 100644 index 0da4a6c97..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/cardStore.svelte.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Card Store — Mutation-Only Service - * - * All reads are handled by useLiveQuery() hooks in queries.ts. - * This store only provides write operations (create, update, delete, reorder). - * IndexedDB writes automatically trigger UI updates via Dexie liveQuery. - */ - -import type { Card, CreateCardInput, UpdateCardInput } from '$lib/types/card'; -import { cardCollection, deckCollection, type LocalCard } from '$lib/data/local-store'; -import { toCard } from '$lib/data/queries'; -import { CardsEvents } from '@manacore/shared-utils/analytics'; - -let error = $state(null); - -export const cardStore = { - get error() { - return error; - }, - - /** - * Create new card -- writes to IndexedDB instantly. - */ - async createCard(input: CreateCardInput, currentCardCount: number = 0): Promise { - error = null; - try { - const content = input.content as { front?: string; back?: string; text?: string }; - const newLocal: LocalCard = { - id: crypto.randomUUID(), - deckId: input.deck_id, - front: content.front || content.text || input.title || '', - back: content.back || '', - difficulty: 1, - reviewCount: 0, - order: input.position ?? currentCardCount, - }; - - const inserted = await cardCollection.insert(newLocal); - - // Update deck card count - const deck = await deckCollection.get(input.deck_id); - if (deck) { - await deckCollection.update(input.deck_id, { - cardCount: (deck.cardCount || 0) + 1, - }); - } - - CardsEvents.cardCreated(); - return toCard(inserted); - } catch (err: any) { - error = err.message || 'Failed to create card'; - console.error('Create card error:', err); - return null; - } - }, - - /** - * Update card -- writes to IndexedDB instantly. - */ - async updateCard(id: string, updates: UpdateCardInput) { - error = null; - try { - const localUpdates: Partial = {}; - if (updates.content) { - const content = updates.content as { front?: string; back?: string; text?: string }; - if (content.front !== undefined) localUpdates.front = content.front; - if (content.back !== undefined) localUpdates.back = content.back; - } - if (updates.title !== undefined) localUpdates.front = updates.title; - if (updates.position !== undefined) localUpdates.order = updates.position; - - await cardCollection.update(id, localUpdates); - } catch (err: any) { - error = err.message || 'Failed to update card'; - console.error('Update card error:', err); - } - }, - - /** - * Delete card -- writes to IndexedDB instantly. - */ - async deleteCard(id: string, deckId?: string) { - error = null; - try { - await cardCollection.delete(id); - - // Update deck card count - if (deckId) { - const deck = await deckCollection.get(deckId); - if (deck) { - await deckCollection.update(deckId, { - cardCount: Math.max(0, (deck.cardCount || 0) - 1), - }); - } - } - - CardsEvents.cardDeleted(); - } catch (err: any) { - error = err.message || 'Failed to delete card'; - console.error('Delete card error:', err); - } - }, - - /** - * Reorder cards -- writes to IndexedDB instantly. - */ - async reorderCards(deckId: string, cardIds: string[]) { - error = null; - try { - for (let i = 0; i < cardIds.length; i++) { - await cardCollection.update(cardIds[i], { order: i } as Partial); - } - } catch (err: any) { - error = err.message || 'Failed to reorder cards'; - console.error('Reorder cards error:', err); - } - }, - - /** - * Clear error - */ - clearError() { - error = null; - }, -}; diff --git a/apps/cards/apps/web-archived/src/lib/stores/deckStore.svelte.ts b/apps/cards/apps/web-archived/src/lib/stores/deckStore.svelte.ts deleted file mode 100644 index 8f4bff244..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/deckStore.svelte.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Deck Store — Mutation-Only Service - * - * All reads are handled by useLiveQuery() hooks in queries.ts. - * This store only provides write operations (create, update, delete). - * IndexedDB writes automatically trigger UI updates via Dexie liveQuery. - */ - -import type { CreateDeckInput, UpdateDeckInput } from '$lib/types/deck'; -import { deckCollection, cardCollection, type LocalDeck } from '$lib/data/local-store'; -import { toDeck } from '$lib/data/queries'; -import { CardsEvents } from '@manacore/shared-utils/analytics'; -import type { Deck } from '$lib/types/deck'; - -let error = $state(null); - -export const deckStore = { - get error() { - return error; - }, - - /** - * Create new deck -- writes to IndexedDB instantly. - */ - async createDeck(input: CreateDeckInput): Promise { - error = null; - try { - const newLocal: LocalDeck = { - id: crypto.randomUUID(), - name: input.title, - description: input.description || null, - color: '#6366f1', - cardCount: 0, - isPublic: input.is_public ?? false, - }; - - const inserted = await deckCollection.insert(newLocal); - CardsEvents.deckCreated(); - return toDeck(inserted); - } catch (err: any) { - error = err.message || 'Failed to create deck'; - console.error('Create deck error:', err); - return null; - } - }, - - /** - * Update deck -- writes to IndexedDB instantly. - */ - async updateDeck(id: string, updates: UpdateDeckInput) { - error = null; - try { - const localUpdates: Partial = {}; - if (updates.title !== undefined) localUpdates.name = updates.title; - if (updates.description !== undefined) localUpdates.description = updates.description; - if (updates.is_public !== undefined) localUpdates.isPublic = updates.is_public; - - await deckCollection.update(id, localUpdates); - } catch (err: any) { - error = err.message || 'Failed to update deck'; - console.error('Update deck error:', err); - } - }, - - /** - * Delete deck -- writes to IndexedDB instantly. - */ - async deleteDeck(id: string) { - error = null; - try { - // Delete all cards belonging to this deck - const cards = await cardCollection.getAll({ deckId: id }); - for (const card of cards) { - await cardCollection.delete(card.id); - } - - await deckCollection.delete(id); - CardsEvents.deckDeleted(); - } catch (err: any) { - error = err.message || 'Failed to delete deck'; - console.error('Delete deck error:', err); - } - }, - - /** - * Clear error - */ - clearError() { - error = null; - }, -}; diff --git a/apps/cards/apps/web-archived/src/lib/stores/navigation.ts b/apps/cards/apps/web-archived/src/lib/stores/navigation.ts deleted file mode 100644 index e922e7ce5..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/navigation.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createSimpleNavigationStores } from '@manacore/shared-stores'; - -export const { isNavCollapsed } = createSimpleNavigationStores(); diff --git a/apps/cards/apps/web-archived/src/lib/stores/progressStore.svelte.ts b/apps/cards/apps/web-archived/src/lib/stores/progressStore.svelte.ts deleted file mode 100644 index 4c4574c57..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/progressStore.svelte.ts +++ /dev/null @@ -1,307 +0,0 @@ -import type { StudySession, CardProgress, DailyProgress } from '$lib/types/study'; -import { PUBLIC_API_URL } from '$env/static/public'; -import { authService } from '$lib/auth'; - -// Svelte 5 runes-based progress store -let studySessions = $state([]); -let cardProgress = $state([]); -let statistics = $state(null); -let streakInfo = $state(null); -let loading = $state(false); -let error = $state(null); - -interface Statistics { - totalCardsStudied: number; - totalStudyTimeMinutes: number; - averageAccuracy: number; - totalSessions: number; -} - -interface StreakInfo { - currentStreak: number; - longestStreak: number; - lastStudyDate: string; - totalStudyDays: number; -} - -/** - * Helper to make authenticated API requests - */ -async function apiRequest(endpoint: string, options: RequestInit = {}): Promise { - const appToken = await authService.getAppToken(); - if (!appToken) { - throw new Error('Not authenticated'); - } - - const response = await fetch(`${PUBLIC_API_URL}${endpoint}`, { - ...options, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${appToken}`, - ...options.headers, - }, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `API error: ${response.status}`); - } - - return response.json(); -} - -/** - * Map backend session to frontend format - */ -function mapSessionFromApi(apiSession: any): StudySession { - return { - id: apiSession.id, - deck_id: apiSession.deckId, - user_id: apiSession.userId, - mode: 'all', - total_cards: apiSession.totalCards || 0, - completed_cards: apiSession.completedCards || 0, - correct_cards: apiSession.correctCards || 0, - started_at: apiSession.startedAt, - completed_at: apiSession.endedAt, - time_spent_seconds: apiSession.timeSpentSeconds || 0, - }; -} - -/** - * Map backend card progress to frontend format - */ -function mapProgressFromApi(apiProgress: any): CardProgress { - return { - id: apiProgress.id, - user_id: apiProgress.userId, - card_id: apiProgress.cardId, - ease_factor: apiProgress.easeFactor, - interval: apiProgress.interval, - repetitions: apiProgress.repetitions, - last_reviewed: apiProgress.lastReviewed, - next_review: apiProgress.nextReview, - status: apiProgress.status || 'new', - created_at: apiProgress.createdAt, - updated_at: apiProgress.updatedAt, - }; -} - -/** - * Calculate streak from sessions - */ -function calculateStreak(sessions: StudySession[]): StreakInfo { - if (!sessions || sessions.length === 0) { - return { - currentStreak: 0, - longestStreak: 0, - lastStudyDate: '', - totalStudyDays: 0, - }; - } - - // Get unique study dates - const studyDates = new Set( - sessions.map((s) => new Date(s.started_at).toISOString().split('T')[0]) - ); - const sortedDates = Array.from(studyDates).sort(); - - // Calculate streaks - let currentStreak = 0; - let longestStreak = 0; - let tempStreak = 1; - - const today = new Date().toISOString().split('T')[0]; - const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; - - const lastStudyDate = sortedDates[sortedDates.length - 1]; - if (lastStudyDate === today || lastStudyDate === yesterday) { - currentStreak = 1; - - for (let i = sortedDates.length - 2; i >= 0; i--) { - const prevDate = new Date(sortedDates[i]); - const currDate = new Date(sortedDates[i + 1]); - const diffDays = Math.floor((currDate.getTime() - prevDate.getTime()) / 86400000); - - if (diffDays === 1) { - currentStreak++; - } else { - break; - } - } - } - - for (let i = 1; i < sortedDates.length; i++) { - const prevDate = new Date(sortedDates[i - 1]); - const currDate = new Date(sortedDates[i]); - const diffDays = Math.floor((currDate.getTime() - prevDate.getTime()) / 86400000); - - if (diffDays === 1) { - tempStreak++; - } else { - longestStreak = Math.max(longestStreak, tempStreak); - tempStreak = 1; - } - } - longestStreak = Math.max(longestStreak, tempStreak, currentStreak); - - return { - currentStreak, - longestStreak, - lastStudyDate, - totalStudyDays: studyDates.size, - }; -} - -export const progressStore = { - get studySessions() { - return studySessions; - }, - get cardProgress() { - return cardProgress; - }, - get statistics() { - return statistics; - }, - get streakInfo() { - return streakInfo; - }, - get loading() { - return loading; - }, - get error() { - return error; - }, - - /** - * Fetch all study sessions - */ - async fetchStudySessions() { - loading = true; - error = null; - - try { - const response = await apiRequest<{ sessions: any[]; count: number }>( - '/v1/api/study-sessions' - ); - studySessions = (response.sessions || []).map(mapSessionFromApi); - - // Calculate streak - streakInfo = calculateStreak(studySessions); - } catch (err: any) { - error = err.message || 'Failed to fetch study sessions'; - console.error('Fetch study sessions error:', err); - } finally { - loading = false; - } - }, - - /** - * Fetch study session statistics - */ - async fetchStatistics() { - loading = true; - error = null; - - try { - const [sessionsResponse, progressStatsResponse] = await Promise.all([ - apiRequest<{ stats: any }>('/v1/api/study-sessions/stats'), - apiRequest<{ stats: any }>('/v1/api/progress/stats'), - ]); - - const sessionStats = sessionsResponse.stats || {}; - const progressStats = progressStatsResponse.stats || {}; - - statistics = { - totalCardsStudied: sessionStats.totalCardsStudied || 0, - totalStudyTimeMinutes: Math.round((sessionStats.totalTimeSeconds || 0) / 60), - averageAccuracy: - sessionStats.totalCardsStudied > 0 - ? Math.round( - ((sessionStats.totalCorrectCards || 0) / sessionStats.totalCardsStudied) * 100 - ) - : 0, - totalSessions: sessionStats.totalSessions || 0, - }; - } catch (err: any) { - error = err.message || 'Failed to fetch statistics'; - console.error('Fetch statistics error:', err); - } finally { - loading = false; - } - }, - - /** - * Fetch card progress for a deck - */ - async fetchDeckProgress(deckId: string) { - loading = true; - error = null; - - try { - const response = await apiRequest<{ progress: any[]; count: number }>( - `/v1/api/progress/deck/${deckId}` - ); - cardProgress = (response.progress || []).map(mapProgressFromApi); - } catch (err: any) { - error = err.message || 'Failed to fetch deck progress'; - console.error('Fetch deck progress error:', err); - } finally { - loading = false; - } - }, - - /** - * Fetch due cards - */ - async fetchDueCards(deckId?: string) { - loading = true; - error = null; - - try { - const endpoint = deckId ? `/v1/api/progress/deck/${deckId}/due` : '/v1/api/progress/due'; - const response = await apiRequest<{ progress: any[]; count: number }>(endpoint); - return (response.progress || []).map(mapProgressFromApi); - } catch (err: any) { - error = err.message || 'Failed to fetch due cards'; - console.error('Fetch due cards error:', err); - return []; - } finally { - loading = false; - } - }, - - /** - * Get progress summary for a deck - */ - getDeckProgressSummary(deckId: string) { - const deckProgress = cardProgress.filter((p) => { - // This requires cards info - for now return all progress - return true; - }); - - const total = deckProgress.length; - const mastered = deckProgress.filter((p) => p.ease_factor >= 2.5 && p.interval >= 21).length; - const learning = deckProgress.filter((p) => p.status === 'learning').length; - const newCards = deckProgress.filter((p) => p.status === 'new').length; - const dueNow = deckProgress.filter((p) => { - if (!p.next_review) return false; - return new Date(p.next_review) <= new Date(); - }).length; - - return { - total, - mastered, - learning, - newCards, - dueNow, - }; - }, - - /** - * Clear error - */ - clearError() { - error = null; - }, -}; diff --git a/apps/cards/apps/web-archived/src/lib/stores/tags.svelte.ts b/apps/cards/apps/web-archived/src/lib/stores/tags.svelte.ts deleted file mode 100644 index 074782375..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/tags.svelte.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Tag Store — Local-First via Shared Tag Store - * Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps. - * Use context ('tags') for reads, tagMutations for writes. - */ -export { - tagMutations, - useAllTags, - getTagById, - getTagsByIds, - getTagColor, - getTagsByGroup, -} from '@manacore/shared-stores'; diff --git a/apps/cards/apps/web-archived/src/lib/stores/theme.ts b/apps/cards/apps/web-archived/src/lib/stores/theme.ts deleted file mode 100644 index 1415854be..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/theme.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Cards Theme Store - * - * Uses the shared theme system with Cards's indigo primary color. - */ -import { createThemeStore } from '@manacore/shared-theme'; - -// Re-export types for convenience -export type { ThemeMode, ThemeVariant, EffectiveMode } from '@manacore/shared-theme'; - -/** - * Cards theme store instance - * - * - Default variant: ocean (blue) - * - Custom primary: Indigo (#6366f1) - * - All 4 theme variants available - */ -export const theme = createThemeStore({ - appId: 'cards', - defaultVariant: 'ocean', - primaryColor: { - light: '239 84% 67%', // Indigo #6366f1 - dark: '239 84% 67%', - }, -}); diff --git a/apps/cards/apps/web-archived/src/lib/stores/user-settings.svelte.ts b/apps/cards/apps/web-archived/src/lib/stores/user-settings.svelte.ts deleted file mode 100644 index 17099845c..000000000 --- a/apps/cards/apps/web-archived/src/lib/stores/user-settings.svelte.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * User Settings Store for Cards - * - * This store syncs settings with mana-core-auth and provides: - * - Global settings that apply to all apps - * - Per-app overrides for customization - * - localStorage caching for offline support - */ - -import { browser } from '$app/environment'; -import { createUserSettingsStore } from '@manacore/shared-theme'; -import { authStore } from './auth.svelte'; - -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - if (injectedUrl) return injectedUrl; - } - return import.meta.env.DEV ? 'http://localhost:3001' : ''; -} - -export const userSettings = createUserSettingsStore({ - appId: 'cards', - authUrl: getAuthUrl, - getAccessToken: () => authStore.getAccessToken(), -}); diff --git a/apps/cards/apps/web-archived/src/lib/types/auth.ts b/apps/cards/apps/web-archived/src/lib/types/auth.ts deleted file mode 100644 index 4253556e9..000000000 --- a/apps/cards/apps/web-archived/src/lib/types/auth.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Mana Core Authentication Types - -export interface ManaUser { - id: string; - email: string; - role: string; - name?: string; - username?: string; - display_name?: string; - avatar_url?: string; - organizationId?: string; - metadata?: Record; -} - -export interface AppSettings { - b2b?: B2BSettings; - [key: string]: any; -} - -export interface B2BSettings { - disableRevenueCat?: boolean; - organizationId?: string; - plan?: string; - role?: string; -} - -export interface JwtPayload { - // Standard JWT claims - sub: string; // User ID - iat?: number; - exp?: number; - nbf?: number; - iss?: string; - aud?: string | string[]; - - // Custom claims - email?: string; - user_id?: string; - session_id?: string; - role: string; - app_id?: string; - app_settings?: AppSettings; -} - -export interface DecodedToken extends JwtPayload {} - -export interface AuthTokens { - appToken: string; - refreshToken: string; -} - -export interface SignInResponse extends AuthTokens { - user?: ManaUser; -} - -export interface SignUpResponse extends AuthTokens { - user?: ManaUser; - message?: string; -} - -export interface AuthError { - message: string; - statusCode?: number; -} - -export interface CreditBalance { - credits: number; - userId: string; -} - -export interface DeviceInfo { - deviceId: string; - deviceName: string; - deviceType: 'ios' | 'android' | 'web' | 'desktop'; - userAgent?: string; -} diff --git a/apps/cards/apps/web-archived/src/lib/types/card.ts b/apps/cards/apps/web-archived/src/lib/types/card.ts deleted file mode 100644 index 1eaad39fd..000000000 --- a/apps/cards/apps/web-archived/src/lib/types/card.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Content types for different card types -export interface TextContent { - text: string; - formatting?: { - bold?: boolean; - italic?: boolean; - underline?: boolean; - }; -} - -export interface FlashcardContent { - front: string; - back: string; - hint?: string; -} - -export interface QuizContent { - question: string; - options: string[]; - correct_answer: number; - explanation?: string; -} - -export interface MixedContent { - blocks: ContentBlock[]; -} - -export type ContentBlock = - | { type: 'text'; data: { text: string } } - | { type: 'image'; data: { url: string; caption?: string } } - | { type: 'quiz'; data: QuizContent } - | { type: 'flashcard'; data: FlashcardContent }; - -export type CardContent = TextContent | FlashcardContent | QuizContent | MixedContent; - -export interface Card { - id: string; - deck_id: string; - position: number; - title?: string; - content: CardContent; - card_type: 'text' | 'flashcard' | 'quiz' | 'mixed'; - ai_model?: string; - ai_prompt?: string; - version: number; - is_favorite: boolean; - created_at: string; - updated_at: string; -} - -export interface CreateCardInput { - deck_id: string; - title?: string; - content: CardContent; - card_type: 'text' | 'flashcard' | 'quiz' | 'mixed'; - position?: number; -} - -export interface UpdateCardInput { - title?: string; - content?: CardContent; - card_type?: 'text' | 'flashcard' | 'quiz' | 'mixed'; - position?: number; - is_favorite?: boolean; -} diff --git a/apps/cards/apps/web-archived/src/lib/types/credits.ts b/apps/cards/apps/web-archived/src/lib/types/credits.ts deleted file mode 100644 index e3d950798..000000000 --- a/apps/cards/apps/web-archived/src/lib/types/credits.ts +++ /dev/null @@ -1,40 +0,0 @@ -export interface CreditBalance { - userId: string; - balance: number; - currency: string; - timestamp: string; -} - -export interface InsufficientCreditsError { - error: 'insufficient_credits'; - message: string; - requiredCredits: number; - availableCredits: number; - operation?: string; -} - -export interface CreditResponse { - success?: boolean; - creditsUsed?: number; - error?: string; - message?: string; - requiredCredits?: number; - availableCredits?: number; - operation?: string; -} - -export function isInsufficientCreditsError(error: any): error is InsufficientCreditsError { - return error && error.error === 'insufficient_credits'; -} - -export function extractCreditError(error: any): InsufficientCreditsError | null { - if (isInsufficientCreditsError(error)) { - return error; - } - - if (error?.response?.data && isInsufficientCreditsError(error.response.data)) { - return error.response.data; - } - - return null; -} diff --git a/apps/cards/apps/web-archived/src/lib/types/deck.ts b/apps/cards/apps/web-archived/src/lib/types/deck.ts deleted file mode 100644 index 27607420d..000000000 --- a/apps/cards/apps/web-archived/src/lib/types/deck.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface Deck { - id: string; - user_id: string; - title: string; - description?: string; - cover_image_url?: string; - is_public: boolean; - settings: Record; - tags: string[]; - metadata: Record; - created_at: string; - updated_at: string; - card_count?: number; -} - -export interface CreateDeckInput { - title: string; - description?: string; - is_public?: boolean; - tags?: string[]; - settings?: Record; -} - -export interface UpdateDeckInput { - title?: string; - description?: string; - is_public?: boolean; - tags?: string[]; - settings?: Record; -} diff --git a/apps/cards/apps/web-archived/src/lib/types/study.ts b/apps/cards/apps/web-archived/src/lib/types/study.ts deleted file mode 100644 index 8e99d1e71..000000000 --- a/apps/cards/apps/web-archived/src/lib/types/study.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type StudyMode = 'all' | 'new' | 'review' | 'favorites' | 'random'; - -export interface StudySession { - id: string; - deck_id: string; - user_id: string; - mode: StudyMode; - total_cards: number; - completed_cards: number; - correct_cards: number; - started_at: string; - completed_at?: string; - time_spent_seconds: number; -} - -export interface CardProgress { - id: string; - user_id: string; - card_id: string; - ease_factor: number; - interval: number; - repetitions: number; - last_reviewed?: string; - next_review?: string; - status: 'new' | 'learning' | 'review' | 'relearning'; - created_at: string; - updated_at: string; -} - -export interface StudyCardResult { - cardId: string; - quality: 0 | 1 | 2 | 3 | 4 | 5; // SM-2 quality rating - timeSpent: number; // seconds -} - -export interface DailyProgress { - user_id: string; - date: string; - cards_studied: number; - time_spent_minutes: number; - accuracy_percentage: number; - decks_studied: string[]; -} diff --git a/apps/cards/apps/web-archived/src/lib/version.ts b/apps/cards/apps/web-archived/src/lib/version.ts deleted file mode 100644 index d63b4cfef..000000000 --- a/apps/cards/apps/web-archived/src/lib/version.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const APP_VERSION = '0.2.0'; -export const BUILD_TIME: string = - typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : new Date().toISOString(); -export const BUILD_HASH: string = typeof __BUILD_HASH__ !== 'undefined' ? __BUILD_HASH__ : 'dev'; diff --git a/apps/cards/apps/web-archived/src/routes/(app)/+layout.svelte b/apps/cards/apps/web-archived/src/routes/(app)/+layout.svelte deleted file mode 100644 index fbff19b2d..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/+layout.svelte +++ /dev/null @@ -1,353 +0,0 @@ - - - - - -
- - - - - {#if isTagStripVisible} - ({ - id: t.id, - name: t.name, - color: t.color || '#3b82f6', - }))} - selectedIds={[]} - onToggle={() => {}} - onClear={() => {}} - managementHref="/tags" - /> - {/if} - - - - - -
-
- {@render children()} -
-
-
- - - {#if cardsOnboarding.shouldShow} - - {/if} - - - (showGuestWelcome = false)} - onLogin={() => goto('/login')} - onRegister={() => goto('/register')} - locale={($locale || 'de') === 'de' ? 'de' : 'en'} - /> - - {#if authStore.isAuthenticated} - - {/if} - -
diff --git a/apps/cards/apps/web-archived/src/routes/(app)/apps/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/apps/+page.svelte deleted file mode 100644 index dddf23a14..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/apps/+page.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -
- -
- - diff --git a/apps/cards/apps/web-archived/src/routes/(app)/decks/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/decks/+page.svelte deleted file mode 100644 index a6a5c5ab4..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/decks/+page.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - - - My Decks - Cards - - -
- -
-
-

My Decks

-

Organize your learning materials into decks

-
- -
- - - {#if deckStore.error} -
-

Error loading decks

-

{deckStore.error}

-
- {:else if allDecks.value.length === 0} - -
-
📚
-

No decks yet

-

- Create your first deck to start organizing your learning materials -

- -
- {:else} - -
- {#each allDecks.value as deck (deck.id)} - -
handleContextMenu(e, deck)}> - handleDeckClick(deck.id)} /> -
- {/each} -
- {/if} -
- - - - -{#if contextMenu.target} - (contextMenu = { visible: false, x: 0, y: 0, target: null })} - /> -{/if} diff --git a/apps/cards/apps/web-archived/src/routes/(app)/decks/[id]/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/decks/[id]/+page.svelte deleted file mode 100644 index 925241941..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/decks/[id]/+page.svelte +++ /dev/null @@ -1,169 +0,0 @@ - - - - {currentDeck.value?.title || 'Deck'} - Cards - - -{#if currentDeck.value} - {@const deck = currentDeck.value} -
- - - - -
-
-

{deck.title}

- {#if deck.description} -

{deck.description}

- {/if} - - {#if deck.tags && deck.tags.length > 0} -
- {#each deck.tags as tag} - {tag} - {/each} -
- {/if} -
- -
- {#if deck.is_public} - Public - {/if} - -
-
- - -
- -
-
{deckCards.value.length || deck.card_count || 0}
-
Total Cards
-
-
- -
-
{masteredCount}
-
Mastered
-
-
- -
-
{dueCount}
-
Due for Review
-
-
-
- - -
- - -
- - - -

Recent Cards

-
- {#if (deck.card_count || 0) === 0} -

No cards yet. Add your first card to get started!

- - {:else} -

Card list coming soon...

- {/if} -
-
- - - {#if showDeleteConfirm} -
(showDeleteConfirm = false)} - > -
e.stopPropagation()} - > -

Delete Deck?

-

- Are you sure you want to delete "{deck.title}"? This action cannot be undone and will - also delete all cards in this deck. -

-
- - -
-
-
- {/if} -
-{:else} -
-

Deck not found

- -
-{/if} diff --git a/apps/cards/apps/web-archived/src/routes/(app)/explore/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/explore/+page.svelte deleted file mode 100644 index d068d7630..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/explore/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - Explore - Cards - - -
-
-

Explore

-

Discover public decks from the community

-
- - -
-
🔍
-

Explore Feature

-

Browse and discover public decks - Coming soon!

-
-
-
diff --git a/apps/cards/apps/web-archived/src/routes/(app)/feedback/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/feedback/+page.svelte deleted file mode 100644 index 9024d5b88..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/feedback/+page.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/cards/apps/web-archived/src/routes/(app)/help/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/help/+page.svelte deleted file mode 100644 index 59e9443c7..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/help/+page.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - {translations.title} | Cards - - - goto('/')} - showGettingStarted={false} - showChangelog={false} - defaultSection="faq" -/> diff --git a/apps/cards/apps/web-archived/src/routes/(app)/mana/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/mana/+page.svelte deleted file mode 100644 index 56b083aa0..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/mana/+page.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/apps/cards/apps/web-archived/src/routes/(app)/profile/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/profile/+page.svelte deleted file mode 100644 index 29274922e..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/profile/+page.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - diff --git a/apps/cards/apps/web-archived/src/routes/(app)/progress/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/progress/+page.svelte deleted file mode 100644 index fdfafb4d6..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/progress/+page.svelte +++ /dev/null @@ -1,137 +0,0 @@ - - - - Progress - Cards - - -
-
-

Progress

-

Track your learning progress and statistics

-
- - {#if progressStore.loading} -
-
-
- {:else} - -
- -
-
- {progressStore.statistics?.totalCardsStudied || 0} -
-
Cards Studied
-
-
- -
-
- 🔥 {progressStore.streakInfo?.currentStreak || 0} -
-
Current Streak
-
-
- -
-
- {progressStore.statistics?.averageAccuracy || 0}% -
-
Average Accuracy
-
-
- -
-
- {progressStore.statistics?.totalStudyTimeMinutes || 0} -
-
Minutes Studied
-
-
-
- - - -

📊 Study Streak

-
-
-
- {progressStore.streakInfo?.currentStreak || 0} -
-
Current Streak
-
-
-
- {progressStore.streakInfo?.longestStreak || 0} -
-
Longest Streak
-
-
-
- {progressStore.streakInfo?.totalStudyDays || 0} -
-
Total Study Days
-
-
-
- {progressStore.statistics?.totalSessions || 0} -
-
Total Sessions
-
-
-
- - - -

📚 Recent Study Sessions

- {#if progressStore.studySessions.length === 0} -
-
🎯
-

No study sessions yet.

-

- Start studying a deck to see your progress here! -

-
- {:else} -
- {#each progressStore.studySessions.slice(0, 10) as session} -
-
-
- {new Date(session.started_at).toLocaleDateString('de-DE', { - weekday: 'short', - day: 'numeric', - month: 'short', - })} -
-
- {session.completed_cards} cards • {Math.round(session.time_spent_seconds / 60)} min -
-
-
-
- {session.completed_cards > 0 - ? Math.round((session.correct_cards / session.completed_cards) * 100) - : 0}% -
-
accuracy
-
-
- {/each} -
- {/if} -
- {/if} -
diff --git a/apps/cards/apps/web-archived/src/routes/(app)/settings/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/settings/+page.svelte deleted file mode 100644 index 00e8cac54..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/settings/+page.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - - Einstellungen - Cards - - - - - - - - - {#snippet icon()} - - {/snippet} - - - - 1.0.0 - - - - -

v{APP_VERSION}

-
diff --git a/apps/cards/apps/web-archived/src/routes/(app)/tags/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/tags/+page.svelte deleted file mode 100644 index 49c105125..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/tags/+page.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - Tags | Cards - - -
-

Tags verwalten

-

- Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps. -

- - {#if tagsCtx.value.length === 0} -

Keine Tags vorhanden.

- {:else} -
- {#each tagsCtx.value as tag} -
- - {tag.name} -
- {/each} -
- {/if} -
- - diff --git a/apps/cards/apps/web-archived/src/routes/(app)/themes/+page.svelte b/apps/cards/apps/web-archived/src/routes/(app)/themes/+page.svelte deleted file mode 100644 index 922a91b31..000000000 --- a/apps/cards/apps/web-archived/src/routes/(app)/themes/+page.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - Themes | Cards - - - theme.setVariant(v)} - showModeSelector={true} - currentMode={theme.mode} - onModeChange={(m) => theme.setMode(m)} - showBackButton={true} - onBack={() => goto('/decks')} -/> diff --git a/apps/cards/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte b/apps/cards/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte deleted file mode 100644 index 27d613061..000000000 --- a/apps/cards/apps/web-archived/src/routes/(auth)/forgot-password/+page.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/cards/apps/web-archived/src/routes/(auth)/login/+page.svelte b/apps/cards/apps/web-archived/src/routes/(auth)/login/+page.svelte deleted file mode 100644 index 17fd2bab6..000000000 --- a/apps/cards/apps/web-archived/src/routes/(auth)/login/+page.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - - authStore.signInWithPasskey()} - onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)} - onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)} - onSendMagicLink={(email) => authStore.sendMagicLink(email)} - {goto} - successRedirect="/decks" - registerPath="/register" - forgotPasswordPath="/forgot-password" - lightBackground="#faf5ff" - darkBackground="#1a1625" - {translations} - {verified} - {initialEmail} - version={APP_VERSION} - buildTime={BUILD_TIME} -> - {#snippet headerControls()} - - {/snippet} - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/cards/apps/web-archived/src/routes/(auth)/register/+page.svelte b/apps/cards/apps/web-archived/src/routes/(auth)/register/+page.svelte deleted file mode 100644 index bf3e3cc6a..000000000 --- a/apps/cards/apps/web-archived/src/routes/(auth)/register/+page.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - - {translations.title} | Cards - - - - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/cards/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte b/apps/cards/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte deleted file mode 100644 index 9dbd33c91..000000000 --- a/apps/cards/apps/web-archived/src/routes/(auth)/reset-password/+page.svelte +++ /dev/null @@ -1,174 +0,0 @@ - - - - Reset Password - Cards - - -
-
- - - Cards - -
- -
-
-
-

Reset Password

-

- {#if success}Password reset successfully - {:else if hasToken}Enter your new password - {:else}Invalid or missing token{/if} -

-
- - {#if success} -
-
-
-

- Your password has been reset successfully. You will be redirected to the login page - shortly. -

- - Go to login - -
-
- {:else if hasToken} -
-
- {#if error} -
- {error} -
- {/if} -
-
- - -

At least 8 characters

-
-
- - -
- -
-
-
- {:else} -
-
-
⚠️
-

- This password reset link is invalid or has expired. -

- - Request a new link - -
-
- {/if} -
-
-
diff --git a/apps/cards/apps/web-archived/src/routes/+layout.svelte b/apps/cards/apps/web-archived/src/routes/+layout.svelte deleted file mode 100644 index 5067c4bbf..000000000 --- a/apps/cards/apps/web-archived/src/routes/+layout.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - -{@render children()} diff --git a/apps/cards/apps/web-archived/src/routes/+layout.ts b/apps/cards/apps/web-archived/src/routes/+layout.ts deleted file mode 100644 index 8c482a26a..000000000 --- a/apps/cards/apps/web-archived/src/routes/+layout.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { waitLocale } from '$lib/i18n'; -import '$lib/i18n'; // This triggers the init() call at module scope - -export const load = async () => { - await waitLocale(); - return {}; -}; diff --git a/apps/cards/apps/web-archived/src/routes/+page.svelte b/apps/cards/apps/web-archived/src/routes/+page.svelte deleted file mode 100644 index c4d6b11b4..000000000 --- a/apps/cards/apps/web-archived/src/routes/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
-
-
-

Loading...

-
-
diff --git a/apps/cards/apps/web-archived/src/routes/health/+server.ts b/apps/cards/apps/web-archived/src/routes/health/+server.ts deleted file mode 100644 index 949c64729..000000000 --- a/apps/cards/apps/web-archived/src/routes/health/+server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; - -export const GET: RequestHandler = async () => { - return json({ - status: 'ok', - service: 'cards-web', - timestamp: new Date().toISOString(), - }); -}; diff --git a/apps/cards/apps/web-archived/src/routes/offline/+page.svelte b/apps/cards/apps/web-archived/src/routes/offline/+page.svelte deleted file mode 100644 index 00416310c..000000000 --- a/apps/cards/apps/web-archived/src/routes/offline/+page.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/cards/apps/web-archived/src/routes/offline/+page.ts b/apps/cards/apps/web-archived/src/routes/offline/+page.ts deleted file mode 100644 index 189f71e2e..000000000 --- a/apps/cards/apps/web-archived/src/routes/offline/+page.ts +++ /dev/null @@ -1 +0,0 @@ -export const prerender = true; diff --git a/apps/cards/apps/web-archived/static/images/app-icons/cards-logo-gradient.png b/apps/cards/apps/web-archived/static/images/app-icons/cards-logo-gradient.png deleted file mode 100644 index 7bb2798b3..000000000 Binary files a/apps/cards/apps/web-archived/static/images/app-icons/cards-logo-gradient.png and /dev/null differ diff --git a/apps/cards/apps/web-archived/static/images/app-icons/maerchenzauber-logo-gradient.png b/apps/cards/apps/web-archived/static/images/app-icons/maerchenzauber-logo-gradient.png deleted file mode 100644 index e47ad9138..000000000 Binary files a/apps/cards/apps/web-archived/static/images/app-icons/maerchenzauber-logo-gradient.png and /dev/null differ diff --git a/apps/cards/apps/web-archived/static/images/app-icons/manacore-logo-gradient.png b/apps/cards/apps/web-archived/static/images/app-icons/manacore-logo-gradient.png deleted file mode 100644 index 7bb2798b3..000000000 Binary files a/apps/cards/apps/web-archived/static/images/app-icons/manacore-logo-gradient.png and /dev/null differ diff --git a/apps/cards/apps/web-archived/static/images/app-icons/memoro-logo-gradient.png b/apps/cards/apps/web-archived/static/images/app-icons/memoro-logo-gradient.png deleted file mode 100644 index f7bbee22d..000000000 Binary files a/apps/cards/apps/web-archived/static/images/app-icons/memoro-logo-gradient.png and /dev/null differ diff --git a/apps/cards/apps/web-archived/static/images/app-icons/moodlit-logo-gradient.png b/apps/cards/apps/web-archived/static/images/app-icons/moodlit-logo-gradient.png deleted file mode 100644 index 69fcd68a1..000000000 Binary files a/apps/cards/apps/web-archived/static/images/app-icons/moodlit-logo-gradient.png and /dev/null differ diff --git a/apps/cards/apps/web-archived/static/robots.txt b/apps/cards/apps/web-archived/static/robots.txt deleted file mode 100644 index b6dd6670c..000000000 --- a/apps/cards/apps/web-archived/static/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# allow crawling everything by default -User-agent: * -Disallow: diff --git a/apps/cards/apps/web-archived/svelte.config.js b/apps/cards/apps/web-archived/svelte.config.js deleted file mode 100644 index 7664b57cd..000000000 --- a/apps/cards/apps/web-archived/svelte.config.js +++ /dev/null @@ -1,18 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://svelte.dev/docs/kit/integrations - // for more information about preprocessors - preprocess: vitePreprocess(), - - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter(), - }, -}; - -export default config; diff --git a/apps/cards/apps/web-archived/tailwind.config.js.bak b/apps/cards/apps/web-archived/tailwind.config.js.bak deleted file mode 100644 index a5e29e3ef..000000000 --- a/apps/cards/apps/web-archived/tailwind.config.js.bak +++ /dev/null @@ -1,12 +0,0 @@ -import preset from '@manacore/shared-tailwind/preset'; - -/** @type {import('tailwindcss').Config} */ -export default { - presets: [preset], - content: [ - './src/**/*.{html,js,svelte,ts}', - '../../../packages/shared-ui/src/**/*.{html,js,svelte,ts}', - '../../../packages/shared-auth-ui/src/**/*.{html,js,svelte,ts}', - ], - plugins: [require('@tailwindcss/typography')], -}; diff --git a/apps/cards/apps/web-archived/tsconfig.json b/apps/cards/apps/web-archived/tsconfig.json deleted file mode 100644 index 5a3b413ed..000000000 --- a/apps/cards/apps/web-archived/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowImportingTsExtensions": true, - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // To make changes to top-level options such as include and exclude, we recommend extending - // the generated config; see https://svelte.dev/docs/kit/configuration#typescript -} diff --git a/apps/cards/apps/web-archived/vite.config.ts b/apps/cards/apps/web-archived/vite.config.ts deleted file mode 100644 index 5d471a3cd..000000000 --- a/apps/cards/apps/web-archived/vite.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vite'; -import { SvelteKitPWA } from '@vite-pwa/sveltekit'; -import { createPWAConfig } from '@manacore/shared-pwa'; -import { MANACORE_SHARED_PACKAGES, getBuildDefines } from '@manacore/shared-vite-config'; - -export default defineConfig({ - plugins: [ - tailwindcss(), - sveltekit(), - SvelteKitPWA( - createPWAConfig({ - name: 'Cards - Kartendecks', - shortName: 'Cards', - description: 'Kartendecks erstellen und lernen', - themeColor: '#f59e0b', - }) - ), - ], - server: { - port: 5176, - strictPort: true, - }, - ssr: { - noExternal: [...MANACORE_SHARED_PACKAGES], - }, - optimizeDeps: { - exclude: [...MANACORE_SHARED_PACKAGES], - }, - define: { - ...getBuildDefines(), - }, -}); diff --git a/apps/chat/apps/web-archived/.env.example b/apps/chat/apps/web-archived/.env.example deleted file mode 100644 index d683ba963..000000000 --- a/apps/chat/apps/web-archived/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Mana Core Auth Configuration -PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 - -# Chat Backend API -PUBLIC_BACKEND_URL=http://localhost:3002 diff --git a/apps/chat/apps/web-archived/Dockerfile b/apps/chat/apps/web-archived/Dockerfile deleted file mode 100644 index 99131c30b..000000000 --- a/apps/chat/apps/web-archived/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM sveltekit-base:local AS builder - -ARG PUBLIC_BACKEND_URL=http://chat-server -ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001 -ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL -ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL - -COPY apps/chat/packages/chat-types ./apps/chat/packages/chat-types -COPY apps/chat/apps/web ./apps/chat/apps/web - -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ - pnpm install --no-frozen-lockfile --ignore-scripts - -WORKDIR /app/apps/chat/apps/web -RUN pnpm exec svelte-kit sync -RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build - -FROM node:20-alpine AS production -WORKDIR /app/apps/chat/apps/web -COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm -COPY --from=builder /app/apps/chat/apps/web/node_modules ./node_modules -COPY --from=builder /app/apps/chat/apps/web/build ./build -COPY --from=builder /app/apps/chat/apps/web/package.json ./ - -EXPOSE 5010 -ENV NODE_ENV=production PORT=5010 HOST=0.0.0.0 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:5010/health || exit 1 - -CMD ["node", "build"] diff --git a/apps/chat/apps/web-archived/eslint.config.js b/apps/chat/apps/web-archived/eslint.config.js deleted file mode 100644 index f0e51b62e..000000000 --- a/apps/chat/apps/web-archived/eslint.config.js +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-check -import { - baseConfig, - typescriptConfig, - svelteConfig, - prettierConfig, -} from '@manacore/eslint-config'; - -export default [ - { - ignores: ['dist/**', '.svelte-kit/**', 'node_modules/**'], - }, - ...baseConfig, - ...typescriptConfig, - ...svelteConfig, - ...prettierConfig, -]; diff --git a/apps/chat/apps/web-archived/package.json b/apps/chat/apps/web-archived/package.json deleted file mode 100644 index 3e11b9886..000000000 --- a/apps/chat/apps/web-archived/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "@chat/web", - "private": true, - "version": "0.3.0", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "type-check": "svelte-kit sync && svelte-check --threshold error", - "lint": "eslint ." - }, - "devDependencies": { - "@manacore/shared-pwa": "workspace:*", - "@manacore/shared-vite-config": "workspace:*", - "@sveltejs/adapter-auto": "^6.0.0", - "@sveltejs/adapter-node": "^5.4.0", - "@sveltejs/kit": "^2.47.1", - "@sveltejs/vite-plugin-svelte": "^6.2.0", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.7", - "@vite-pwa/sveltekit": "^1.1.0", - "autoprefixer": "^10.4.21", - "postcss": "^8.5.6", - "svelte": "^5.41.0", - "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "vite": "^7.1.7" - }, - "dependencies": { - "@chat/types": "workspace:*", - "@manacore/shared-api-client": "workspace:*", - "@manacore/shared-app-onboarding": "workspace:*", - "@manacore/shared-auth": "workspace:*", - "@manacore/shared-auth-stores": "workspace:*", - "@manacore/shared-auth-ui": "workspace:*", - "@manacore/local-store": "workspace:*", - "@manacore/shared-branding": "workspace:*", - "@manacore/shared-error-tracking": "workspace:*", - "@manacore/feedback": "workspace:*", - "@manacore/shared-i18n": "workspace:*", - "@manacore/help": "workspace:*", - "@manacore/shared-icons": "workspace:*", - "@manacore/shared-profile-ui": "workspace:*", - "@manacore/shared-stores": "workspace:*", - "@manacore/shared-tags": "workspace:*", - "@manacore/subscriptions": "workspace:*", - "@manacore/shared-tailwind": "workspace:*", - "@manacore/shared-theme": "workspace:*", - "@manacore/shared-theme-ui": "workspace:*", - "@manacore/shared-ui": "workspace:*", - "@manacore/shared-utils": "workspace:*", - "marked": "^17.0.0", - "svelte-i18n": "^4.0.1" - } -} diff --git a/apps/chat/apps/web-archived/src/app.css b/apps/chat/apps/web-archived/src/app.css deleted file mode 100644 index c29749613..000000000 --- a/apps/chat/apps/web-archived/src/app.css +++ /dev/null @@ -1,10 +0,0 @@ -@import "tailwindcss"; -@import "@manacore/shared-tailwind/themes.css"; - -/* Scan shared packages for Tailwind classes */ -@source "../../../../packages/shared-ui/src"; -@source "../../../../packages/shared-auth-ui/src"; -@source "../../../../packages/shared-branding/src"; -@source "../../../../packages/shared-theme-ui/src"; -@source "../../../../packages/shared-theme-ui/src/components"; -@source "../../../../packages/shared-theme-ui/src/pages"; diff --git a/apps/chat/apps/web-archived/src/app.d.ts b/apps/chat/apps/web-archived/src/app.d.ts deleted file mode 100644 index 2ae80f0cd..000000000 --- a/apps/chat/apps/web-archived/src/app.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -declare const __BUILD_HASH__: string; -declare const __BUILD_TIME__: string; - -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces - -declare global { - namespace App { - // interface Error {} - interface Locals { - // Auth is now handled client-side via Mana Core Auth - } - interface PageData { - pathname?: string; - } - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/apps/chat/apps/web-archived/src/app.html b/apps/chat/apps/web-archived/src/app.html deleted file mode 100644 index 77a5ff52c..000000000 --- a/apps/chat/apps/web-archived/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/chat/apps/web-archived/src/hooks.client.ts b/apps/chat/apps/web-archived/src/hooks.client.ts deleted file mode 100644 index 15549bb31..000000000 --- a/apps/chat/apps/web-archived/src/hooks.client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { initErrorTracking, handleSvelteError } from '@manacore/shared-error-tracking/browser'; -import type { HandleClientError } from '@sveltejs/kit'; - -initErrorTracking({ - serviceName: 'chat-web', - dsn: (window as any).__PUBLIC_GLITCHTIP_DSN__, - environment: import.meta.env.MODE, -}); - -export const handleError: HandleClientError = ({ error }) => { - handleSvelteError(error); -}; diff --git a/apps/chat/apps/web-archived/src/hooks.server.ts b/apps/chat/apps/web-archived/src/hooks.server.ts deleted file mode 100644 index b85e13116..000000000 --- a/apps/chat/apps/web-archived/src/hooks.server.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Server Hooks for SvelteKit - * - Injects runtime environment variables for client-side use - * - Auth is handled client-side via Mana Core Auth - */ - -import type { Handle } from '@sveltejs/kit'; -import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; - -// Get client-side URLs from environment (Docker runtime) -const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = - process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; -const PUBLIC_BACKEND_URL_CLIENT = - process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; -const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; - -export const handle: Handle = async ({ event, resolve }) => { - const response = await resolve(event, { - transformPageChunk: ({ html }) => { - // Inject runtime environment variables into the HTML - // These will be available on window.__PUBLIC_*__ for client-side code - // Use JSON.stringify to prevent HTML/script injection - const envScript = ``; - return html.replace('', `${envScript}`); - }, - }); - - setSecurityHeaders(response, { - connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], - }); - - return response; -}; diff --git a/apps/chat/apps/web-archived/src/lib/components/AppSlider.svelte b/apps/chat/apps/web-archived/src/lib/components/AppSlider.svelte deleted file mode 100644 index b36ac8d18..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/AppSlider.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/apps/chat/apps/web-archived/src/lib/components/LanguageSelector.svelte b/apps/chat/apps/web-archived/src/lib/components/LanguageSelector.svelte deleted file mode 100644 index 5630e7582..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/LanguageSelector.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/apps/chat/apps/web-archived/src/lib/components/chat/ChatInput.svelte b/apps/chat/apps/web-archived/src/lib/components/chat/ChatInput.svelte deleted file mode 100644 index 7410a7c3c..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/chat/ChatInput.svelte +++ /dev/null @@ -1,149 +0,0 @@ - - -
-
- -
-
- -
- -
- - - {#if models.length > 0 || onDocumentModeToggle} -
- - {#if models.length > 0 && onModelSelect} -
- -
- -
-
- {/if} - - -
- - - {#if onDocumentModeToggle} - - {/if} -
- {/if} -
-

- Enter zum Senden, Shift+Enter für neue Zeile -

-
diff --git a/apps/chat/apps/web-archived/src/lib/components/chat/ChatLayout.svelte b/apps/chat/apps/web-archived/src/lib/components/chat/ChatLayout.svelte deleted file mode 100644 index 7c93cd42d..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/chat/ChatLayout.svelte +++ /dev/null @@ -1,552 +0,0 @@ - - -
- -
- -
-
- - - {#if searchQuery} - - {/if} -
-
- - -
-
- - - - Neuer Chat - - - {#if filteredConversations.length === 0} -
- {#if searchQuery} -
🔍
-

Keine Ergebnisse

-

- Keine Konversationen für "{searchQuery}" -

- - {:else} -
💬
-

Keine Konversationen

-

Starte einen neuen Chat

- {/if} -
- {:else} - - {#if pinnedConversations.length > 0} - - {/if} - - - {#each sectionOrder as section} - {@const convs = groupedConversations()[section]} - {#if convs.length > 0} - - {/if} - {/each} - {/if} -
-
-
- - - - - -
- {@render children()} -
-
- - - - - diff --git a/apps/chat/apps/web-archived/src/lib/components/chat/ConversationList.svelte b/apps/chat/apps/web-archived/src/lib/components/chat/ConversationList.svelte deleted file mode 100644 index b054a24cf..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/chat/ConversationList.svelte +++ /dev/null @@ -1,224 +0,0 @@ - - -
- - - - -
- {#if isLoading} -
-
-
- {:else if conversations.length === 0} -
-

Keine Konversationen

-

Starte einen neuen Chat

-
- {:else} -
- {#each conversations as conv (conv.id)} - {@const isActive = $page.params.id === conv.id} - {#if editingId === conv.id} - -
- handleKeydown(e, conv.id)} - class="flex-1 px-2 py-1 text-sm bg-background border border-border rounded-md - focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" - autofocus - /> - - -
- {:else} - - -
handleContextMenu(e, conv)}> - -
- - {truncateTitle(conv.title || 'Neue Konversation')} - - - {formatDate(conv.updatedAt || conv.createdAt)} - -
-
- - -
- {/if} - {/each} -
- {/if} -
- - {#if contextMenuConv} - { - contextMenuVisible = false; - contextMenuConv = null; - }} - /> - {/if} -
diff --git a/apps/chat/apps/web-archived/src/lib/components/chat/MessageBubble.svelte b/apps/chat/apps/web-archived/src/lib/components/chat/MessageBubble.svelte deleted file mode 100644 index fcfd2a27b..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/chat/MessageBubble.svelte +++ /dev/null @@ -1,232 +0,0 @@ - - -
- -
- {#if isUser} - - {:else} - - {/if} -
- - -
- -
- {#if isUser} -

{message.messageText}

- {:else} -
- {@html htmlContent} -
- {/if} -
- - -
- - {formattedTime} - -
-
-
- - diff --git a/apps/chat/apps/web-archived/src/lib/components/chat/MessageList.svelte b/apps/chat/apps/web-archived/src/lib/components/chat/MessageList.svelte deleted file mode 100644 index 93b676125..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/chat/MessageList.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - -
- {#if messages.length === 0} -
- -

Keine Nachrichten

-

Starte eine Konversation!

-
- {:else} - {#each messages as message (message.id)} - - {/each} - {#if isTyping} - - {/if} - {/if} -
diff --git a/apps/chat/apps/web-archived/src/lib/components/chat/ModelSelector.svelte b/apps/chat/apps/web-archived/src/lib/components/chat/ModelSelector.svelte deleted file mode 100644 index 590165a4d..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/chat/ModelSelector.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - -
- -
- -
-
diff --git a/apps/chat/apps/web-archived/src/lib/components/chat/TypingIndicator.svelte b/apps/chat/apps/web-archived/src/lib/components/chat/TypingIndicator.svelte deleted file mode 100644 index 27b3d1167..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/chat/TypingIndicator.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - -
- -
- -
- - -
-
-
-
-
-
-
-
-
-
- - diff --git a/apps/chat/apps/web-archived/src/lib/components/compare/CompareInput.svelte b/apps/chat/apps/web-archived/src/lib/components/compare/CompareInput.svelte deleted file mode 100644 index 79ffb05bd..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/compare/CompareInput.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - -
- - - - -
- -
- - onTemperatureChange(parseFloat(e.currentTarget.value))} - disabled={isRunning || disabled} - class="w-24 h-2 bg-muted rounded-full appearance-none cursor-pointer - [&::-webkit-slider-thumb]:appearance-none - [&::-webkit-slider-thumb]:w-4 - [&::-webkit-slider-thumb]:h-4 - [&::-webkit-slider-thumb]:rounded-full - [&::-webkit-slider-thumb]:bg-primary - [&::-webkit-slider-thumb]:cursor-pointer - disabled:opacity-50" - /> -
- - -
- - -
- - -
- - - -
-
diff --git a/apps/chat/apps/web-archived/src/lib/components/compare/CompareProgress.svelte b/apps/chat/apps/web-archived/src/lib/components/compare/CompareProgress.svelte deleted file mode 100644 index 820c8faeb..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/compare/CompareProgress.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - -
- -
-

- Verarbeite Modell {currentIndex + 1} von - {totalModels}: - {currentModelName} -

- -
- - -
-
-
- - -

- {Math.round(progress)}% -

-
diff --git a/apps/chat/apps/web-archived/src/lib/components/compare/ModelResponseCard.svelte b/apps/chat/apps/web-archived/src/lib/components/compare/ModelResponseCard.svelte deleted file mode 100644 index 06d10dcfb..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/compare/ModelResponseCard.svelte +++ /dev/null @@ -1,185 +0,0 @@ - - -
- -
-

{result.modelName}

- - {#if result.status === 'loading'} - - - {statusConfig().label} - - {:else if result.status === 'complete' && formattedDuration()} - {formattedDuration()} - {:else} - {statusConfig().label} - {/if} - -
- - -
- {#if result.status === 'pending'} -

Wartet auf Verarbeitung...

- {:else if result.status === 'loading'} -
- - - -
- {:else if result.status === 'error'} -

{result.error || 'Ein Fehler ist aufgetreten'}

- {:else if result.response} -
- {@html htmlContent} -
- {/if} -
- - - {#if result.status === 'complete' && result.usage} -
- {result.usage.total_tokens} tokens - {#if tokensPerSecond()} - | - {tokensPerSecond()} t/s - {/if} -
- {/if} -
- - diff --git a/apps/chat/apps/web-archived/src/lib/components/compare/ModelResponseGrid.svelte b/apps/chat/apps/web-archived/src/lib/components/compare/ModelResponseGrid.svelte deleted file mode 100644 index cd35c7d85..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/compare/ModelResponseGrid.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - -{#if results.length === 0} -
-

Keine Ergebnisse vorhanden.

-

Gib einen Prompt ein und starte den Vergleich.

-
-{:else} -
- {#each results as result, index (result.modelId)} - - {/each} -
-{/if} diff --git a/apps/chat/apps/web-archived/src/lib/components/spaces/SpaceCard.svelte b/apps/chat/apps/web-archived/src/lib/components/spaces/SpaceCard.svelte deleted file mode 100644 index 2987e1a10..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/spaces/SpaceCard.svelte +++ /dev/null @@ -1,127 +0,0 @@ - - - (showMenu = false)} /> - -
onSelect(space.id)} - onkeydown={(e) => e.key === 'Enter' && onSelect(space.id)} - role="button" - tabindex="0" -> -
-
-
-
- -

- {space.name} -

- {#if isOwner} - - Besitzer - - {/if} -
- - {#if space.description} -

- {space.description} -

- {/if} - -

- Erstellt: {formatDate(space.createdAt)} -

-
- - -
- - - {#if showMenu} -
e.stopPropagation()} - onkeydown={() => {}} - role="menu" - tabindex="-1" - > - {#if isOwner} - - - {:else} - - {/if} -
- {/if} -
-
-
-
diff --git a/apps/chat/apps/web-archived/src/lib/components/spaces/SpaceForm.svelte b/apps/chat/apps/web-archived/src/lib/components/spaces/SpaceForm.svelte deleted file mode 100644 index 34fbd9763..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/spaces/SpaceForm.svelte +++ /dev/null @@ -1,106 +0,0 @@ - - -
-

- {isEditMode ? 'Space bearbeiten' : 'Neuen Space erstellen'} -

- -
{ - e.preventDefault(); - handleSubmit(); - }} - class="space-y-5" - > - -
- - - {#if errors.name} -

{errors.name}

- {/if} -
- - -
- - -
- - -
- - -
-
-
diff --git a/apps/chat/apps/web-archived/src/lib/components/templates/TemplateCard.svelte b/apps/chat/apps/web-archived/src/lib/components/templates/TemplateCard.svelte deleted file mode 100644 index 88a9b4f82..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/templates/TemplateCard.svelte +++ /dev/null @@ -1,96 +0,0 @@ - - -
- -
- - -
-
-
-
-

- {template.name} -

- {#if template.isDefault} - - Standard - - {/if} -
- - {#if template.description} -

- {template.description} -

- {/if} - -

- {truncatePrompt(template.systemPrompt)} -

-
- - -
- {#if !template.isDefault} - - {/if} - - -
-
- - - -
-
diff --git a/apps/chat/apps/web-archived/src/lib/components/templates/TemplateForm.svelte b/apps/chat/apps/web-archived/src/lib/components/templates/TemplateForm.svelte deleted file mode 100644 index 726a54112..000000000 --- a/apps/chat/apps/web-archived/src/lib/components/templates/TemplateForm.svelte +++ /dev/null @@ -1,262 +0,0 @@ - - -
-

- {isEditMode ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'} -

- -
{ - e.preventDefault(); - handleSubmit(); - }} - class="space-y-5" - > - -
- - - {#if errors.name} -

{errors.name}

- {/if} -
- - -
- - -
- - -
- - - {#if errors.systemPrompt} -

{errors.systemPrompt}

- {:else} -

- Der System-Prompt definiert die Rolle und das Verhalten der KI. -

- {/if} -
- - -
- - -

- Diese Frage wird als Vorschlag angezeigt, wenn die Vorlage ausgewählt wird. -

-
- - -
- -
- {#each TEMPLATE_COLORS as color} - - {/each} -
-
- - -
- - -

- Falls ausgewählt, wird dieses Modell automatisch mit der Vorlage verwendet. -

-
- - -
- -
- - -
- - -
-
-
diff --git a/apps/chat/apps/web-archived/src/lib/content/help/index.test.ts b/apps/chat/apps/web-archived/src/lib/content/help/index.test.ts deleted file mode 100644 index 23b833183..000000000 --- a/apps/chat/apps/web-archived/src/lib/content/help/index.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { getChatHelpContent } from './index'; - -describe('Chat Help Content', () => { - it('returns valid German content', () => { - const content = getChatHelpContent('de'); - - expect(content.faq.length).toBeGreaterThan(0); - content.faq.forEach((faq) => { - expect(faq.id).toBeTruthy(); - expect(faq.question).toBeTruthy(); - expect(faq.answer).toBeTruthy(); - }); - - expect(content.features).toBeDefined(); - expect(content.contact).toBeDefined(); - expect(content.contact.supportEmail).toBe('support@mana.how'); - }); - - it('returns valid English content', () => { - const content = getChatHelpContent('en'); - - expect(content.faq.length).toBeGreaterThan(0); - content.faq.forEach((faq) => { - expect(faq.id).toBeTruthy(); - expect(faq.question).toBeTruthy(); - expect(faq.answer).toBeTruthy(); - }); - - expect(content.features).toBeDefined(); - expect(content.contact).toBeDefined(); - }); - - it('returns same number of FAQ items for both languages', () => { - const de = getChatHelpContent('de'); - const en = getChatHelpContent('en'); - - expect(de.faq.length).toBe(en.faq.length); - expect(de.features.length).toBe(en.features.length); - }); - - it('has unique FAQ IDs', () => { - const content = getChatHelpContent('de'); - const ids = content.faq.map((f) => f.id); - expect(new Set(ids).size).toBe(ids.length); - }); -}); diff --git a/apps/chat/apps/web-archived/src/lib/content/help/index.ts b/apps/chat/apps/web-archived/src/lib/content/help/index.ts deleted file mode 100644 index d28a26a4e..000000000 --- a/apps/chat/apps/web-archived/src/lib/content/help/index.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Help content for Chat app - */ - -import type { HelpContent } from '@manacore/help'; -import { getPrivacyFAQs } from '@manacore/help'; - -export function getChatHelpContent(locale: string): HelpContent { - const isDE = locale === 'de'; - - return { - faq: [ - { - id: 'faq-models', - question: isDE - ? 'Welche KI-Modelle stehen zur Verfügung?' - : 'Which AI models are available?', - answer: isDE - ? '

Es gibt zwei Kategorien von Modellen:

  • Lokale Modelle (kostenlos): Laufen auf unserem Server — z.B. Gemma 3, Qwen2.5 Coder, LLaVA Vision
  • Cloud-Modelle (Credits): Leistungsstärkere Modelle — z.B. Claude, GPT-4o, DeepSeek, Llama

Lokale Modelle sind ideal für alltägliche Aufgaben. Cloud-Modelle bieten höhere Qualität für komplexe Anfragen.

' - : '

There are two categories of models:

  • Local models (free): Run on our server — e.g. Gemma 3, Qwen2.5 Coder, LLaVA Vision
  • Cloud models (credits): More powerful models — e.g. Claude, GPT-4o, DeepSeek, Llama

Local models are ideal for everyday tasks. Cloud models offer higher quality for complex requests.

', - category: 'features', - order: 1, - language: isDE ? 'de' : 'en', - featured: true, - tags: isDE ? ['modelle', 'ki', 'lokal', 'cloud'] : ['models', 'ai', 'local', 'cloud'], - }, - { - id: 'faq-spaces', - question: isDE ? 'Was sind Spaces?' : 'What are Spaces?', - answer: isDE - ? '

Spaces sind Bereiche, um deine Chats thematisch zu organisieren:

  • Erstelle Spaces für verschiedene Projekte oder Themen
  • Jeder Space enthält eigene Unterhaltungen
  • Wechsle schnell zwischen Kontexten
' - : '

Spaces are areas to organize your chats by topic:

  • Create spaces for different projects or topics
  • Each space contains its own conversations
  • Switch quickly between contexts
', - category: 'features', - order: 2, - language: isDE ? 'de' : 'en', - tags: isDE ? ['spaces', 'organisation', 'bereiche'] : ['spaces', 'organize', 'areas'], - }, - { - id: 'faq-templates', - question: isDE ? 'Wie nutze ich Templates?' : 'How do I use templates?', - answer: isDE - ? '

Templates sind vordefinierte Prompt-Vorlagen:

  • Wähle ein Template aus der Vorlagenbibliothek
  • Das Template füllt automatisch den Chat mit einem optimierten Prompt
  • Ideal für wiederkehrende Aufgaben wie Texterstellung, Übersetzung oder Code-Analyse
' - : '

Templates are predefined prompt presets:

  • Select a template from the template library
  • The template automatically fills the chat with an optimized prompt
  • Ideal for recurring tasks like writing, translation, or code analysis
', - category: 'features', - order: 3, - language: isDE ? 'de' : 'en', - tags: isDE ? ['templates', 'vorlagen', 'prompts'] : ['templates', 'presets', 'prompts'], - }, - { - id: 'faq-compare', - question: isDE ? 'Was ist der Modellvergleich?' : 'What is model comparison?', - answer: isDE - ? '

Mit dem Modellvergleich kannst du denselben Prompt an mehrere Modelle gleichzeitig senden und die Antworten nebeneinander vergleichen. So findest du das beste Modell für deine Aufgabe.

' - : '

Model comparison lets you send the same prompt to multiple models simultaneously and compare responses side by side. This helps you find the best model for your task.

', - category: 'features', - order: 4, - language: isDE ? 'de' : 'en', - tags: isDE ? ['vergleich', 'modelle', 'test'] : ['compare', 'models', 'test'], - }, - ...getPrivacyFAQs(locale, { - dataTypeDE: 'Chats', - dataTypeEN: 'chats', - extraBulletsDE: [ - 'Lokale Modelle: Bei lokalen Modellen verlassen deine Daten nie unseren Server', - ], - extraBulletsEN: [ - 'Local models: With local models, your data never leaves our server', - ], - }), - ], - features: [ - { - id: 'feature-models', - title: isDE ? 'Mehrere KI-Modelle' : 'Multiple AI Models', - description: isDE - ? 'Lokale und Cloud-Modelle für jede Aufgabe' - : 'Local and cloud models for every task', - icon: '🤖', - category: 'core', - highlights: isDE - ? ['7+ lokale Modelle (kostenlos)', 'Cloud-Modelle (Claude, GPT)', 'Vision-Modelle'] - : ['7+ local models (free)', 'Cloud models (Claude, GPT)', 'Vision models'], - content: '', - order: 1, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-spaces', - title: 'Spaces', - description: isDE - ? 'Organisiere Chats nach Themen und Projekten' - : 'Organize chats by topics and projects', - icon: '📂', - category: 'core', - highlights: isDE - ? ['Thematische Gruppierung', 'Schneller Kontextwechsel', 'Eigene Unterhaltungen'] - : ['Topic grouping', 'Quick context switch', 'Separate conversations'], - content: '', - order: 2, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-compare', - title: isDE ? 'Modellvergleich' : 'Model Comparison', - description: isDE - ? 'Vergleiche Antworten mehrerer Modelle nebeneinander' - : 'Compare responses from multiple models side by side', - icon: '⚖️', - category: 'advanced', - highlights: isDE - ? ['Parallele Anfragen', 'Qualitätsvergleich', 'Beste Modellwahl'] - : ['Parallel requests', 'Quality comparison', 'Best model selection'], - content: '', - order: 3, - language: isDE ? 'de' : 'en', - }, - { - id: 'feature-templates', - title: 'Templates', - description: isDE - ? 'Optimierte Prompts für wiederkehrende Aufgaben' - : 'Optimized prompts for recurring tasks', - icon: '📋', - category: 'core', - highlights: isDE - ? ['Prompt-Vorlagen', 'Ein-Klick-Start', 'Verschiedene Kategorien'] - : ['Prompt presets', 'One-click start', 'Various categories'], - content: '', - order: 4, - language: isDE ? 'de' : 'en', - }, - ], - shortcuts: [ - { - id: 'shortcuts-nav', - category: 'navigation', - title: 'Navigation', - language: isDE ? 'de' : 'en', - order: 1, - shortcuts: [ - { shortcut: 'Cmd/Ctrl + 1', action: 'Chat' }, - { shortcut: 'Cmd/Ctrl + 2', action: isDE ? 'Vergleich' : 'Compare' }, - { shortcut: 'Cmd/Ctrl + 3', action: 'Templates' }, - { shortcut: 'Cmd/Ctrl + 4', action: 'Spaces' }, - { shortcut: 'Cmd/Ctrl + 5', action: isDE ? 'Dokumente' : 'Documents' }, - { shortcut: 'Cmd/Ctrl + 6', action: isDE ? 'Archiv' : 'Archive' }, - ], - }, - ], - gettingStarted: [], - changelog: [], - contact: { - id: 'contact-support', - title: isDE ? 'Support kontaktieren' : 'Contact Support', - content: isDE - ? '

Unser Support-Team hilft dir bei allen Fragen rund um Chat.

' - : '

Our support team is here to help you with any chat-related questions.

', - language: isDE ? 'de' : 'en', - order: 1, - supportEmail: 'support@mana.how', - documentationUrl: 'https://mana.how/docs', - responseTime: isDE ? 'Normalerweise innerhalb von 24 Stunden' : 'Usually within 24 hours', - }, - }; -} diff --git a/apps/chat/apps/web-archived/src/lib/data/guest-seed.ts b/apps/chat/apps/web-archived/src/lib/data/guest-seed.ts deleted file mode 100644 index 65b4ff321..000000000 --- a/apps/chat/apps/web-archived/src/lib/data/guest-seed.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Guest seed data for the Chat app. - * - * Provides a demo conversation to showcase the chat experience. - */ - -import type { LocalConversation, LocalMessage } from './local-store'; - -const DEMO_CONVERSATION_ID = 'demo-welcome'; - -export const guestConversations: LocalConversation[] = [ - { - id: DEMO_CONVERSATION_ID, - title: 'Willkommen bei Chat!', - conversationMode: 'free', - documentMode: false, - isArchived: false, - isPinned: true, - }, -]; - -export const guestMessages: LocalMessage[] = [ - { - id: 'msg-1', - conversationId: DEMO_CONVERSATION_ID, - sender: 'assistant', - messageText: - 'Hallo! Ich bin dein KI-Assistent. Du kannst mir Fragen stellen, Texte schreiben lassen oder einfach ein Gespräch führen. Melde dich an, um deine Unterhaltungen zu speichern und zu synchronisieren.', - }, -]; diff --git a/apps/chat/apps/web-archived/src/lib/data/local-store.ts b/apps/chat/apps/web-archived/src/lib/data/local-store.ts deleted file mode 100644 index fb9030ecd..000000000 --- a/apps/chat/apps/web-archived/src/lib/data/local-store.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Chat — Local-First Data Layer - * - * Conversations, messages, and templates stored locally. - * LLM streaming and model management remain server-side. - */ - -import { createLocalStore, type BaseRecord } from '@manacore/local-store'; -import { guestConversations, guestMessages } from './guest-seed'; - -// ─── Types ────────────────────────────────────────────────── - -export interface LocalConversation extends BaseRecord { - title?: string | null; - modelId?: string | null; - templateId?: string | null; - spaceId?: string | null; - conversationMode: 'free' | 'guided' | 'template'; - documentMode: boolean; - isArchived: boolean; - isPinned: boolean; -} - -export interface LocalMessage extends BaseRecord { - conversationId: string; - sender: 'user' | 'assistant' | 'system'; - messageText: string; -} - -export interface LocalTemplate extends BaseRecord { - name: string; - description: string; - systemPrompt: string; - initialQuestion?: string | null; - modelId?: string | null; - color: string; - isDefault: boolean; - documentMode: boolean; -} - -// ─── Store ────────────────────────────────────────────────── - -const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050'; - -export const chatStore = createLocalStore({ - appId: 'chat', - collections: [ - { - name: 'conversations', - indexes: ['isArchived', 'isPinned', 'spaceId', 'templateId'], - guestSeed: guestConversations, - }, - { - name: 'messages', - indexes: ['conversationId', 'sender', '[conversationId+sender]'], - guestSeed: guestMessages, - }, - { - name: 'templates', - indexes: ['isDefault'], - }, - ], - sync: { - serverUrl: SYNC_SERVER_URL, - }, -}); - -// Typed collection accessors -export const conversationCollection = chatStore.collection('conversations'); -export const messageCollection = chatStore.collection('messages'); -export const templateCollection = chatStore.collection('templates'); diff --git a/apps/chat/apps/web-archived/src/lib/data/queries.ts b/apps/chat/apps/web-archived/src/lib/data/queries.ts deleted file mode 100644 index 6fcdb0179..000000000 --- a/apps/chat/apps/web-archived/src/lib/data/queries.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Reactive Queries & Pure Helpers for Chat - * - * Uses Dexie liveQuery to automatically re-render when IndexedDB changes - * (local writes, sync updates, other tabs). Components call these hooks - * at init time; no manual fetch/refresh needed. - */ - -import { useLiveQueryWithDefault } from '@manacore/local-store/svelte'; -import { - conversationCollection, - templateCollection, - messageCollection, - type LocalConversation, - type LocalTemplate, - type LocalMessage, -} from './local-store'; -import type { Conversation, Template, Message } from '@chat/types'; - -// ─── Type Converters ─────────────────────────────────────── - -export function toConversation(local: LocalConversation): Conversation { - return { - id: local.id, - userId: local.userId ?? 'guest', - modelId: local.modelId ?? '', - templateId: local.templateId ?? undefined, - spaceId: local.spaceId ?? undefined, - conversationMode: local.conversationMode, - documentMode: local.documentMode, - title: local.title ?? undefined, - isArchived: local.isArchived, - isPinned: local.isPinned, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: local.updatedAt ?? new Date().toISOString(), - }; -} - -export function toTemplate(local: LocalTemplate): Template { - return { - id: local.id, - userId: local.userId ?? 'guest', - name: local.name, - description: local.description || null, - systemPrompt: local.systemPrompt, - initialQuestion: local.initialQuestion ?? null, - modelId: local.modelId ?? null, - color: local.color, - isDefault: local.isDefault, - documentMode: local.documentMode, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: local.updatedAt ?? new Date().toISOString(), - }; -} - -export function toMessage(local: LocalMessage): Message { - return { - id: local.id, - conversationId: local.conversationId, - sender: local.sender, - messageText: local.messageText, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: local.updatedAt ?? undefined, - }; -} - -// ─── Live Query Hooks (call during component init) ───────── - -/** All non-archived conversations, sorted by pinned first then updatedAt desc. */ -export function useAllConversations() { - return useLiveQueryWithDefault(async () => { - const locals = await conversationCollection.getAll({ isArchived: false }); - return sortConversations(locals.map(toConversation)); - }, [] as Conversation[]); -} - -/** All archived conversations, sorted by updatedAt desc. */ -export function useArchivedConversations() { - return useLiveQueryWithDefault(async () => { - const locals = await conversationCollection.getAll({ isArchived: true }); - return locals - .map(toConversation) - .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); - }, [] as Conversation[]); -} - -/** All templates, sorted by name. */ -export function useAllTemplates() { - return useLiveQueryWithDefault(async () => { - const locals = await templateCollection.getAll(); - return locals.map(toTemplate).sort((a, b) => a.name.localeCompare(b.name)); - }, [] as Template[]); -} - -/** Messages for a specific conversation, sorted by createdAt asc. */ -export function useConversationMessages(conversationId: string) { - return useLiveQueryWithDefault(async () => { - const locals = await messageCollection.getAll({ conversationId }); - return locals - .map(toMessage) - .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - }, [] as Message[]); -} - -// ─── Pure Sort / Filter Functions (for $derived) ─────────── - -/** Sort conversations: pinned first, then by updatedAt descending. */ -export function sortConversations(list: Conversation[]): Conversation[] { - return [...list].sort((a, b) => { - if (a.isPinned && !b.isPinned) return -1; - if (!a.isPinned && b.isPinned) return 1; - return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); - }); -} - -/** Filter conversations by space. */ -export function filterBySpace(conversations: Conversation[], spaceId: string): Conversation[] { - return conversations.filter((c) => c.spaceId === spaceId); -} - -/** Filter conversations by search query on title. */ -export function filterBySearch(conversations: Conversation[], query: string): Conversation[] { - if (!query.trim()) return conversations; - const lower = query.toLowerCase(); - return conversations.filter((c) => c.title?.toLowerCase().includes(lower)); -} - -/** Split conversations into pinned and unpinned. */ -export function splitPinned(conversations: Conversation[]) { - return { - pinned: conversations.filter((c) => c.isPinned), - unpinned: conversations.filter((c) => !c.isPinned), - }; -} diff --git a/apps/chat/apps/web-archived/src/lib/i18n/index.ts b/apps/chat/apps/web-archived/src/lib/i18n/index.ts deleted file mode 100644 index 7634ae4ad..000000000 --- a/apps/chat/apps/web-archived/src/lib/i18n/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { browser } from '$app/environment'; -import { init, register, locale, waitLocale } from 'svelte-i18n'; - -// List of supported locales -export const supportedLocales = ['de', 'en', 'it', 'fr', 'es'] as const; -export type SupportedLocale = (typeof supportedLocales)[number]; - -// Default locale -const defaultLocale = 'de'; - -// Register all available locales -register('de', () => import('./locales/de.json')); -register('en', () => import('./locales/en.json')); -register('it', () => import('./locales/it.json')); -register('fr', () => import('./locales/fr.json')); -register('es', () => import('./locales/es.json')); - -// Get initial locale from browser or localStorage -function getInitialLocale(): SupportedLocale { - if (browser) { - // Check localStorage first - const stored = localStorage.getItem('chat_locale'); - if (stored && supportedLocales.includes(stored as SupportedLocale)) { - return stored as SupportedLocale; - } - - // Fall back to browser language - const browserLang = navigator.language.split('-')[0]; - if (supportedLocales.includes(browserLang as SupportedLocale)) { - return browserLang as SupportedLocale; - } - } - - return defaultLocale; -} - -// Initialize i18n at module scope (required for SSR) -init({ - fallbackLocale: defaultLocale, - initialLocale: getInitialLocale(), -}); - -// Set locale and persist to localStorage -export function setLocale(newLocale: SupportedLocale) { - locale.set(newLocale); - if (browser) { - localStorage.setItem('chat_locale', newLocale); - } -} - -// Wait for locale to be loaded (useful for SSR) -export { waitLocale }; diff --git a/apps/chat/apps/web-archived/src/lib/i18n/locales/de.json b/apps/chat/apps/web-archived/src/lib/i18n/locales/de.json deleted file mode 100644 index 30b2317f2..000000000 --- a/apps/chat/apps/web-archived/src/lib/i18n/locales/de.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "app_slider": { - "title": "Weitere Manacore Apps", - "memoro_desc": "KI-gestützte Sprachmemos", - "maerchenzauber_desc": "Magische Gute-Nacht-Geschichten", - "cards_desc": "KI Lernkarten", - "picture_desc": "KI Bildgenerierung", - "moodlit_desc": "Dein Stimmungsbegleiter", - "manacore_desc": "KI-Produktivitätssuite", - "coming_soon": "Demnächst", - "download": "Download" - } -} diff --git a/apps/chat/apps/web-archived/src/lib/i18n/locales/en.json b/apps/chat/apps/web-archived/src/lib/i18n/locales/en.json deleted file mode 100644 index 1cf6356fd..000000000 --- a/apps/chat/apps/web-archived/src/lib/i18n/locales/en.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "app_slider": { - "title": "More Manacore Apps", - "memoro_desc": "AI-powered voice memos", - "maerchenzauber_desc": "Magical bedtime stories", - "cards_desc": "AI flashcards", - "picture_desc": "AI image generation", - "moodlit_desc": "Your mood companion", - "manacore_desc": "AI productivity suite", - "coming_soon": "Coming soon", - "download": "Download" - } -} diff --git a/apps/chat/apps/web-archived/src/lib/i18n/locales/es.json b/apps/chat/apps/web-archived/src/lib/i18n/locales/es.json deleted file mode 100644 index 5f56d6cd0..000000000 --- a/apps/chat/apps/web-archived/src/lib/i18n/locales/es.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "app_slider": { - "title": "Más Apps de Manacore", - "memoro_desc": "Notas de voz con IA", - "maerchenzauber_desc": "Cuentos mágicos para dormir", - "cards_desc": "Flashcards con IA", - "picture_desc": "Generación de imágenes con IA", - "moodlit_desc": "Tu compañero de ánimo", - "manacore_desc": "Suite de productividad IA", - "coming_soon": "Próximamente", - "download": "Descargar" - } -} diff --git a/apps/chat/apps/web-archived/src/lib/i18n/locales/fr.json b/apps/chat/apps/web-archived/src/lib/i18n/locales/fr.json deleted file mode 100644 index 2e2308534..000000000 --- a/apps/chat/apps/web-archived/src/lib/i18n/locales/fr.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "app_slider": { - "title": "Autres Apps Manacore", - "memoro_desc": "Mémos vocaux avec IA", - "maerchenzauber_desc": "Histoires magiques du soir", - "cards_desc": "Flashcards avec IA", - "picture_desc": "Génération d'images avec IA", - "moodlit_desc": "Ton compagnon d'humeur", - "manacore_desc": "Suite de productivité IA", - "coming_soon": "Bientôt disponible", - "download": "Télécharger" - } -} diff --git a/apps/chat/apps/web-archived/src/lib/i18n/locales/it.json b/apps/chat/apps/web-archived/src/lib/i18n/locales/it.json deleted file mode 100644 index c7558130b..000000000 --- a/apps/chat/apps/web-archived/src/lib/i18n/locales/it.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "app_slider": { - "title": "Altre App Manacore", - "memoro_desc": "Memo vocali con IA", - "maerchenzauber_desc": "Storie magiche della buonanotte", - "cards_desc": "Flashcard con IA", - "picture_desc": "Generazione immagini con IA", - "moodlit_desc": "Il tuo compagno d'umore", - "manacore_desc": "Suite di produttività IA", - "coming_soon": "Prossimamente", - "download": "Scarica" - } -} diff --git a/apps/chat/apps/web-archived/src/lib/services/api.ts b/apps/chat/apps/web-archived/src/lib/services/api.ts deleted file mode 100644 index 94f10b5be..000000000 --- a/apps/chat/apps/web-archived/src/lib/services/api.ts +++ /dev/null @@ -1,662 +0,0 @@ -/** - * API Client for Chat Backend - * - * This replaces direct Supabase calls with backend API calls. - * All database operations now go through the NestJS backend. - * - * Token handling: Uses authStore.getValidToken() which automatically - * refreshes expired tokens before making requests. - */ - -import { env } from '$env/dynamic/public'; -import { authStore } from '$lib/stores/auth.svelte'; -import type { - Conversation, - Message, - Template, - Space, - SpaceMember, - Document, - AIModel, - ChatMessage, - ChatCompletionResponse, -} from '@chat/types'; - -// Re-export types for convenience -export type { - Conversation, - Message, - Template, - Space, - SpaceMember, - Document, - AIModel, - ChatMessage, - ChatCompletionResponse, -}; - -const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3002'; - -type FetchOptions = { - method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; - body?: unknown; - token?: string; -}; - -async function fetchApi( - endpoint: string, - options: FetchOptions = {} -): Promise<{ data: T | null; error: Error | null }> { - const { method = 'GET', body, token } = options; - - // Get a valid token (auto-refreshes if expired) - const authToken = token || (await authStore.getValidToken()); - - if (!authToken) { - return { data: null, error: new Error('No authentication token') }; - } - - try { - const response = await fetch(`${API_BASE}/api/v1${endpoint}`, { - method, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${authToken}`, - }, - body: body ? JSON.stringify(body) : undefined, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - return { - data: null, - error: new Error(errorData.message || `API error: ${response.status}`), - }; - } - - const data = await response.json(); - return { data, error: null }; - } catch (error) { - return { - data: null, - error: error instanceof Error ? error : new Error('Unknown error'), - }; - } -} - -// ============ Conversation API ============ - -export const conversationApi = { - async getConversations(spaceId?: string): Promise { - const query = spaceId ? `?spaceId=${spaceId}` : ''; - const { data, error } = await fetchApi(`/conversations${query}`); - if (error) { - console.error('Error loading conversations:', error); - return []; - } - return data || []; - }, - - async getArchivedConversations(): Promise { - const { data, error } = await fetchApi('/conversations/archived'); - if (error) { - console.error('Error loading archived conversations:', error); - return []; - } - return data || []; - }, - - async getConversation(id: string): Promise { - const { data, error } = await fetchApi(`/conversations/${id}`); - if (error) { - console.error('Error loading conversation:', error); - return null; - } - return data; - }, - - async getMessages(conversationId: string): Promise { - const { data, error } = await fetchApi(`/conversations/${conversationId}/messages`); - if (error) { - console.error('Error loading messages:', error); - return []; - } - return data || []; - }, - - async createConversation(options: { - modelId: string; - title?: string; - templateId?: string; - conversationMode?: 'free' | 'guided' | 'template'; - documentMode?: boolean; - spaceId?: string; - }): Promise { - const { data, error } = await fetchApi('/conversations', { - method: 'POST', - body: options, - }); - if (error) { - console.error('Error creating conversation:', error); - return null; - } - return data; - }, - - async addMessage( - conversationId: string, - sender: 'user' | 'assistant' | 'system', - messageText: string - ): Promise { - const { data, error } = await fetchApi(`/conversations/${conversationId}/messages`, { - method: 'POST', - body: { sender, messageText }, - }); - if (error) { - console.error('Error adding message:', error); - return null; - } - return data; - }, - - async updateTitle(conversationId: string, title: string): Promise { - const { error } = await fetchApi(`/conversations/${conversationId}/title`, { - method: 'PATCH', - body: { title }, - }); - if (error) { - console.error('Error updating title:', error); - return false; - } - return true; - }, - - async archiveConversation(conversationId: string): Promise { - const { error } = await fetchApi(`/conversations/${conversationId}/archive`, { - method: 'PATCH', - }); - if (error) { - console.error('Error archiving conversation:', error); - return false; - } - return true; - }, - - async unarchiveConversation(conversationId: string): Promise { - const { error } = await fetchApi(`/conversations/${conversationId}/unarchive`, { - method: 'PATCH', - }); - if (error) { - console.error('Error unarchiving conversation:', error); - return false; - } - return true; - }, - - async deleteConversation(conversationId: string): Promise { - const { error } = await fetchApi<{ success: boolean }>(`/conversations/${conversationId}`, { - method: 'DELETE', - }); - if (error) { - console.error('Error deleting conversation:', error); - return false; - } - return true; - }, - - async pinConversation(conversationId: string): Promise { - const { error } = await fetchApi(`/conversations/${conversationId}/pin`, { - method: 'PATCH', - }); - if (error) { - console.error('Error pinning conversation:', error); - return false; - } - return true; - }, - - async unpinConversation(conversationId: string): Promise { - const { error } = await fetchApi(`/conversations/${conversationId}/unpin`, { - method: 'PATCH', - }); - if (error) { - console.error('Error unpinning conversation:', error); - return false; - } - return true; - }, -}; - -// ============ Template API ============ - -export const templateApi = { - async getTemplates(): Promise { - const { data, error } = await fetchApi('/templates'); - if (error) { - console.error('Error loading templates:', error); - return []; - } - return data || []; - }, - - async getTemplate(id: string): Promise