From 2859ee29687fa97826015912d1cd15f4bb9a0eef Mon Sep 17 00:00:00 2001 From: hawkinslabdev <59891413+hawkinslabdev@users.noreply.github.com> Date: Wed, 20 May 2026 10:09:49 +0200 Subject: [PATCH 1/5] feat: prepare .env for demo_mode --- motomate/.env.example | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/motomate/.env.example b/motomate/.env.example index 2fcb40a..26ff210 100644 --- a/motomate/.env.example +++ b/motomate/.env.example @@ -35,8 +35,11 @@ PUBLIC_APP_URL=http://localhost:5173 PUBLIC_APP_ORIGINS=http://localhost:5173 PUBLIC_APP_NAME=MotoMate -# Scheduler -CRON_INTERVAL_HOURS=1 # workflow check interval in hours (default: 1) +# Demo mode (which will seed a read-only demo account on startup; don't use a pre-existing db!) +PUBLIC_DEMO_ENABLED=false + +# Scheduler for workflow/reminders +CRON_INTERVAL_HOURS=1 # Push notifications (VAPID) VAPID_PUBLIC_KEY= From dfe7ba1d87dd355cd6ac531b96cf0ce7a764cf07 Mon Sep 17 00:00:00 2001 From: hawkinslabdev <59891413+hawkinslabdev@users.noreply.github.com> Date: Wed, 20 May 2026 10:10:30 +0200 Subject: [PATCH 2/5] feat: implement demo data seeding functionality --- motomate/demo-seed.js | 218 ++++++++++++++++ motomate/server.js | 8 +- motomate/src/hooks.server.ts | 27 ++ motomate/src/lib/db/demo-seed.ts | 421 +++++++++++++++++++++++++++++++ 4 files changed, 672 insertions(+), 2 deletions(-) create mode 100644 motomate/demo-seed.js create mode 100644 motomate/src/lib/db/demo-seed.ts diff --git a/motomate/demo-seed.js b/motomate/demo-seed.js new file mode 100644 index 0000000..de79986 --- /dev/null +++ b/motomate/demo-seed.js @@ -0,0 +1,218 @@ +import Database from 'better-sqlite3'; +import { hash } from '@node-rs/argon2'; +import { randomBytes } from 'node:crypto'; + +const ARGON2_OPTS = { memoryCost: 19456, timeCost: 2, outputLen: 32, parallelism: 1 }; + +function id() { + return randomBytes(10).toString('hex'); +} + +export async function seedDemo() { + const url = process.env.DATABASE_URL ?? './data/motomate.db'; + const sqlite = new Database(url); + + try { + const existing = sqlite + .prepare('SELECT id FROM users WHERE email = ?') + .get('demo@motomate.local'); + + if (existing) { + console.log('[motomate] Demo seed: user already exists, skipping'); + return; + } + + const passwordHash = await hash('password123', ARGON2_OPTS); + const userId = id(); + const vehicleId = id(); + const oilTmplId = id(); + const chainTmplId = id(); + const tireTmplId = id(); + const oilTrackerId = id(); + const chainTrackerId = id(); + const tireTrackerId = id(); + + const settings = JSON.stringify({ + theme: 'system', + currency: 'EUR', + odometer_unit: 'km', + locale: 'en', + avatar_seed: id() + }); + + sqlite.transaction(() => { + sqlite + .prepare( + `INSERT INTO users (id, email, password_hash, onboarding_done, settings) + VALUES (?, ?, ?, 1, ?)` + ) + .run(userId, 'demo@motomate.local', passwordHash, settings); + + sqlite + .prepare( + `INSERT INTO vehicles (id, user_id, type, name, make, model, year, + current_odometer, current_measurement, current_measurement_unit, odometer_unit, meta) + VALUES (?, ?, 'motorcycle', 'Honda CB500F', 'Honda', 'CB500F', 2021, + 18400, 18400, 'km', 'km', '{"avatar_emoji":"๐Ÿ๏ธ"}')` + ) + .run(vehicleId, userId); + + // Task templates + sqlite + .prepare( + `INSERT INTO task_templates + (id, user_id, vehicle_id, name, category, description, interval_km, interval_measurement, interval_unit, interval_months, is_preset) + VALUES (?, ?, ?, 'Oil & Filter Change', 'oil', 'Engine oil and oil filter replacement', 10000, 10000, 'km', 12, 1)` + ) + .run(oilTmplId, userId, vehicleId); + + sqlite + .prepare( + `INSERT INTO task_templates + (id, user_id, vehicle_id, name, category, description, interval_km, interval_measurement, interval_unit, interval_months, is_preset) + VALUES (?, ?, ?, 'Chain Clean & Lube', 'chain', 'Clean and lubricate the chain', 500, 500, 'km', NULL, 1)` + ) + .run(chainTmplId, userId, vehicleId); + + sqlite + .prepare( + `INSERT INTO task_templates + (id, user_id, vehicle_id, name, category, description, interval_km, interval_measurement, interval_unit, interval_months, is_preset) + VALUES (?, ?, ?, 'Tire Pressure & Wear Check', 'tire', 'Check tyre pressure and inspect tread depth', NULL, NULL, NULL, 1, 1)` + ) + .run(tireTmplId, userId, vehicleId); + + // Active trackers + sqlite + .prepare( + `INSERT INTO active_trackers + (id, vehicle_id, template_id, last_done_at, last_done_odometer, last_done_measurement, + next_due_odometer, next_due_measurement, next_due_at, measurement_unit, status) + VALUES (?, ?, ?, '2025-04-10', 8000, 8000, 18000, 18000, '2026-04-10', 'km', 'overdue')` + ) + .run(oilTrackerId, vehicleId, oilTmplId); + + sqlite + .prepare( + `INSERT INTO active_trackers + (id, vehicle_id, template_id, last_done_at, last_done_odometer, last_done_measurement, + next_due_odometer, next_due_measurement, measurement_unit, status) + VALUES (?, ?, ?, '2026-04-15', 17950, 17950, 18450, 18450, 'km', 'due')` + ) + .run(chainTrackerId, vehicleId, chainTmplId); + + sqlite + .prepare( + `INSERT INTO active_trackers + (id, vehicle_id, template_id, last_done_at, next_due_at, status) + VALUES (?, ?, ?, '2026-05-05', '2026-06-05', 'ok')` + ) + .run(tireTrackerId, vehicleId, tireTmplId); + + // Service logs + const sl = sqlite.prepare( + `INSERT INTO service_logs + (id, vehicle_id, tracker_id, performed_at, odometer_at_service, measurement_at_service, measurement_unit, cost_cents, currency, notes) + VALUES (?, ?, ?, ?, ?, ?, 'km', ?, 'EUR', ?)` + ); + sl.run(id(), vehicleId, null, '2024-10-15', 3000, 3000, 4200, 'Oil & Filter Change'); + sl.run(id(), vehicleId, oilTrackerId, '2025-04-10', 8000, 8000, 4500, 'Oil & Filter Change'); + sl.run( + id(), + vehicleId, + chainTrackerId, + '2025-06-20', + 10500, + 10500, + 800, + 'Chain Clean & Lube' + ); + sl.run( + id(), + vehicleId, + chainTrackerId, + '2025-09-15', + 13200, + 13200, + 800, + 'Chain Clean & Lube' + ); + sl.run( + id(), + vehicleId, + chainTrackerId, + '2026-04-15', + 17950, + 17950, + 800, + 'Chain Clean & Lube' + ); + + // Workflow rules + const wr = sqlite.prepare( + `INSERT INTO workflow_rules (id, user_id, vehicle_id, name, description, trigger, actions, enabled) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ); + wr.run( + id(), + userId, + vehicleId, + 'Oil change overdue', + 'Notify when oil change is past due by 500 km', + JSON.stringify({ type: 'odometer_overdue', km_past: 500 }), + JSON.stringify({ + title: 'Oil change overdue', + body: 'Your Honda CB500F is due for an oil change.' + }), + 1 + ); + wr.run( + id(), + userId, + vehicleId, + 'Upcoming service reminder', + 'Notify 7 days before a tracker is due by date', + JSON.stringify({ type: 'date_upcoming', days_before: 7 }), + JSON.stringify({ + title: 'Service reminder', + body: 'A scheduled service is coming up in 7 days.' + }), + 1 + ); + wr.run( + id(), + userId, + null, + 'No odometer update', + 'Notify if no odometer reading logged in 30 days', + JSON.stringify({ type: 'no_odometer_update', days: 30 }), + JSON.stringify({ + title: 'No recent activity', + body: 'No odometer update logged in the last 30 days.' + }), + 0 + ); + + // Finance transactions + const ft = sqlite.prepare( + `INSERT INTO finance_transactions + (id, vehicle_id, user_id, category, amount_cents, currency, notes, performed_at) + VALUES (?, ?, ?, ?, ?, 'EUR', ?, ?)` + ); + ft.run(id(), vehicleId, userId, 'fuel', 6000, 'Fuel fill-up', '2026-02-10'); + ft.run( + id(), + vehicleId, + userId, + 'accessories', + 15000, + 'Handlebar grips & bar end weights', + '2025-11-05' + ); + })(); + + console.log('[motomate] Demo data seeded (demo@motomate.local / password123)'); + } finally { + sqlite.close(); + } +} diff --git a/motomate/server.js b/motomate/server.js index 63847f4..0ca7803 100644 --- a/motomate/server.js +++ b/motomate/server.js @@ -11,12 +11,16 @@ try { process.exit(1); } +if (process.env.PUBLIC_DEMO_ENABLED === 'true') { + const { seedDemo } = await import('./demo-seed.js'); + await seedDemo(); +} + const { handler } = await import('./build/handler.js'); const app = express(); -// Helmet CSP conflicts with SvelteKit's inline scripts, so we disable it. -// SvelteKit handles its own CSP via tags in app.html. +// Helmet CSP conflicts with SvelteKit's inline scripts, so we disable it. SvelteKit handles its own CSP via tags in app.html. app.use( helmet({ contentSecurityPolicy: false diff --git a/motomate/src/hooks.server.ts b/motomate/src/hooks.server.ts index 1b85953..6678af5 100644 --- a/motomate/src/hooks.server.ts +++ b/motomate/src/hooks.server.ts @@ -1,10 +1,13 @@ import { lucia } from '$lib/auth/index.js'; import type { Handle } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; +import { env as pubEnv } from '$env/dynamic/public'; import { initScheduler } from '$lib/server/scheduler.js'; initScheduler(); +let _demoSeeded = false; + function isOriginTrusted(origin: string | null, referer: string | null, url: string): boolean { const configuredOrigins = process.env.PUBLIC_APP_ORIGINS ? process.env.PUBLIC_APP_ORIGINS.split(',') @@ -102,6 +105,12 @@ function buildCorsHeaders(requestOrigin: string | null): Record } export const handle: Handle = async ({ event, resolve }) => { + if (!_demoSeeded && pubEnv.PUBLIC_DEMO_ENABLED === 'true') { + _demoSeeded = true; + const { seedDemo } = await import('$lib/db/demo-seed.js'); + await seedDemo(); + } + if (event.request.method === 'OPTIONS') { return new Response(null, { status: 204, @@ -109,6 +118,24 @@ export const handle: Handle = async ({ event, resolve }) => { }); } + if ( + pubEnv.PUBLIC_DEMO_ENABLED === 'true' && + event.request.method !== 'GET' && + event.request.method !== 'HEAD' && + !event.url.pathname.startsWith('/login') && + !event.url.pathname.startsWith('/register') && + !event.url.pathname.startsWith('/magic-link') && + event.url.pathname !== '/auth/logout' + ) { + if (event.request.headers.get('x-sveltekit-action') === 'true') { + return new Response(JSON.stringify({ type: 'success', status: 200 }), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + } + return new Response(null, { status: 303, headers: { Location: event.url.pathname } }); + } + if (event.request.method !== 'GET' && event.request.method !== 'HEAD') { const origin = event.request.headers.get('origin'); const referer = event.request.headers.get('referer'); diff --git a/motomate/src/lib/db/demo-seed.ts b/motomate/src/lib/db/demo-seed.ts new file mode 100644 index 0000000..dc0a076 --- /dev/null +++ b/motomate/src/lib/db/demo-seed.ts @@ -0,0 +1,421 @@ +import { writeFileSync, mkdirSync, existsSync, copyFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join, dirname } from 'node:path'; +import { hash } from '@node-rs/argon2'; +import { eq, count } from 'drizzle-orm'; +import { db } from './index.js'; +import { getUserByEmail } from './repositories/users.js'; +import { + users, + vehicles, + task_templates, + active_trackers, + service_logs, + finance_transactions, + workflow_rules, + documents, + travels, + odometer_logs +} from './schema.js'; +import { generateId } from '../utils/id.js'; + +const ARGON2_OPTS = { memoryCost: 19456, timeCost: 2, outputLen: 32, parallelism: 1 }; + +const ASSETS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'demo-assets'); + +function copyAssetToStorage(assetName: string, storageKey: string): boolean { + const storagePath = process.env.STORAGE_LOCAL_PATH ?? './uploads'; + const destPath = join(storagePath, storageKey); + if (existsSync(destPath)) return true; + try { + const destDir = dirname(destPath); + if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true }); + copyFileSync(join(ASSETS_DIR, assetName), destPath); + return true; + } catch { + return false; + } +} + +async function patchDemoContent(userId: string, vehicleId: string): Promise { + // Finance: dealer 1000 km service + const [{ value: ftCount }] = await db + .select({ value: count() }) + .from(finance_transactions) + .where(eq(finance_transactions.vehicle_id, vehicleId)); + if (ftCount < 3) { + await db.insert(finance_transactions).values({ + id: generateId(), + vehicle_id: vehicleId, + user_id: userId, + category: 'maintenance', + amount_cents: 28500, + currency: 'EUR', + notes: '1000 km dealer service. Oil, filters, chain & brake inspection', + performed_at: '2022-05-20', + odometer_at_transaction: 1000, + measurement_at_transaction: 1000, + measurement_unit: 'km' + }); + } + + // Odometer logs + const [{ value: odoCount }] = await db + .select({ value: count() }) + .from(odometer_logs) + .where(eq(odometer_logs.vehicle_id, vehicleId)); + if (odoCount < 3) { + await db.insert(odometer_logs).values([ + { + id: generateId(), + vehicle_id: vehicleId, + user_id: userId, + odometer: 14200, + measurement: 14200, + measurement_unit: 'km', + kind: 'odometer', + recorded_at: '2025-01-15' + }, + { + id: generateId(), + vehicle_id: vehicleId, + user_id: userId, + odometer: 16800, + measurement: 16800, + measurement_unit: 'km', + kind: 'odometer', + recorded_at: '2025-07-10' + }, + { + id: generateId(), + vehicle_id: vehicleId, + user_id: userId, + odometer: 17900, + measurement: 17900, + measurement_unit: 'km', + remark: 'Spring prep', + kind: 'odometer', + recorded_at: '2026-03-01' + } + ]); + } + + // Always ensure asset files are present in uploads (idempotent & skips if file exists) + const pdfKey = 'demo/dealer-service-1000km.pdf'; + const gpxKey = 'demo/eifel-route.gpx'; + copyAssetToStorage('dealer-service-1000km.pdf', pdfKey); + copyAssetToStorage('eifel-route.gpx', gpxKey); + + // PDF document DB record + const [{ value: docCount }] = await db + .select({ value: count() }) + .from(documents) + .where(eq(documents.vehicle_id, vehicleId)); + if (docCount === 0) { + await db.insert(documents).values({ + id: generateId(), + vehicle_id: vehicleId, + user_id: userId, + name: 'dealer-service-invoice-1000km.pdf', + title: 'Dealer service invoice; 1000 km', + doc_type: 'service', + storage_key: pdfKey, + mime_type: 'application/pdf', + size_bytes: 66156 + }); + } + + // GPX travel DB records + const [{ value: travelCount }] = await db + .select({ value: count() }) + .from(travels) + .where(eq(travels.vehicle_id, vehicleId)); + if (travelCount === 0) { + const gpxDocId = generateId(); + await db.insert(documents).values({ + id: gpxDocId, + vehicle_id: vehicleId, + user_id: userId, + name: 'eifel-route.gpx', + title: 'Eifel Route, Germany', + doc_type: 'route', + storage_key: gpxKey, + mime_type: 'application/gpx+xml', + size_bytes: 384624 + }); + await db.insert(travels).values({ + id: generateId(), + vehicle_id: vehicleId, + user_id: userId, + title: 'Germany, Eifel', + start_date: '2025-06-14', + duration_days: 2, + remark: 'Weekend tour through the Eifel region', + total_expenses_cents: 18000, + currency: 'EUR', + gpx_document_ids: [gpxDocId, null] + }); + } +} + +async function patchWorkflowRules(userId: string, vehicleId: string): Promise { + const existing = await db.query.workflow_rules.findFirst({ + where: eq(workflow_rules.user_id, userId) + }); + if (existing) return; + + await db.insert(workflow_rules).values([ + { + id: generateId(), + user_id: userId, + vehicle_id: vehicleId, + name: 'Oil change overdue', + description: 'Notify when oil change is past due by 500 km', + trigger: { type: 'odometer_overdue', km_past: 500 }, + actions: { + title: 'Oil change overdue', + body: 'Your Honda CB500F is due for an oil change.' + }, + enabled: true + }, + { + id: generateId(), + user_id: userId, + vehicle_id: vehicleId, + name: 'Upcoming service reminder', + description: 'Notify 7 days before a tracker is due by date', + trigger: { type: 'date_upcoming', days_before: 7 }, + actions: { title: 'Service reminder', body: 'A scheduled service is coming up in 7 days.' }, + enabled: true + }, + { + id: generateId(), + user_id: userId, + vehicle_id: null, + name: 'No odometer update', + description: 'Notify if no odometer reading logged in 30 days', + trigger: { type: 'no_odometer_update', days: 30 }, + actions: { + title: 'No recent activity', + body: 'No odometer update logged in the last 30 days.' + }, + enabled: false + } + ]); +} + +export async function seedDemo(): Promise { + const existing = await getUserByEmail('demo@motomate.local'); + if (existing) { + const vehicle = await db.query.vehicles.findFirst({ + where: eq(vehicles.user_id, existing.id) + }); + if (vehicle) { + await patchWorkflowRules(existing.id, vehicle.id); + await patchDemoContent(existing.id, vehicle.id); + } + return; + } + + const passwordHash = await hash('password123', ARGON2_OPTS); + const userId = generateId(); + const vehicleId = generateId(); + const oilTmplId = generateId(); + const chainTmplId = generateId(); + const tireTmplId = generateId(); + const oilTrackerId = generateId(); + const chainTrackerId = generateId(); + const tireTrackerId = generateId(); + + await db.insert(users).values({ + id: userId, + email: 'demo@motomate.local', + password_hash: passwordHash, + onboarding_done: true, + settings: { + theme: 'system', + currency: 'EUR', + odometer_unit: 'km', + locale: 'en', + avatar_seed: generateId() + } + }); + + await db.insert(vehicles).values({ + id: vehicleId, + user_id: userId, + type: 'motorcycle', + name: 'Honda CB500F', + make: 'Honda', + model: 'CB500F', + year: 2021, + current_odometer: 18400, + current_measurement: 18400, + current_measurement_unit: 'km', + odometer_unit: 'km', + meta: { avatar_emoji: '๐Ÿ๏ธ' } + }); + + await db.insert(task_templates).values([ + { + id: oilTmplId, + user_id: userId, + vehicle_id: vehicleId, + name: 'Oil & Filter Change', + category: 'oil', + description: 'Engine oil and oil filter replacement', + interval_km: 10000, + interval_measurement: 10000, + interval_unit: 'km', + interval_months: 12, + is_preset: true + }, + { + id: chainTmplId, + user_id: userId, + vehicle_id: vehicleId, + name: 'Chain Clean & Lube', + category: 'chain', + description: 'Clean and lubricate the chain', + interval_km: 500, + interval_measurement: 500, + interval_unit: 'km', + is_preset: true + }, + { + id: tireTmplId, + user_id: userId, + vehicle_id: vehicleId, + name: 'Tire Pressure & Wear Check', + category: 'tire', + description: 'Check tyre pressure and inspect tread depth', + interval_months: 1, + is_preset: true + } + ]); + + await db.insert(active_trackers).values([ + { + id: oilTrackerId, + vehicle_id: vehicleId, + template_id: oilTmplId, + last_done_at: '2025-04-10', + last_done_odometer: 8000, + last_done_measurement: 8000, + next_due_odometer: 18000, + next_due_measurement: 18000, + next_due_at: '2026-04-10', + measurement_unit: 'km', + status: 'overdue' + }, + { + id: chainTrackerId, + vehicle_id: vehicleId, + template_id: chainTmplId, + last_done_at: '2026-04-15', + last_done_odometer: 17950, + last_done_measurement: 17950, + next_due_odometer: 18450, + next_due_measurement: 18450, + measurement_unit: 'km', + status: 'due' + }, + { + id: tireTrackerId, + vehicle_id: vehicleId, + template_id: tireTmplId, + last_done_at: '2026-05-05', + next_due_at: '2026-06-05', + status: 'ok' + } + ]); + + await db.insert(service_logs).values([ + { + id: generateId(), + vehicle_id: vehicleId, + performed_at: '2024-10-15', + odometer_at_service: 3000, + measurement_at_service: 3000, + measurement_unit: 'km', + cost_cents: 4200, + currency: 'EUR', + notes: 'Oil & Filter Change' + }, + { + id: generateId(), + vehicle_id: vehicleId, + tracker_id: oilTrackerId, + performed_at: '2025-04-10', + odometer_at_service: 8000, + measurement_at_service: 8000, + measurement_unit: 'km', + cost_cents: 4500, + currency: 'EUR', + notes: 'Oil & Filter Change' + }, + { + id: generateId(), + vehicle_id: vehicleId, + tracker_id: chainTrackerId, + performed_at: '2025-06-20', + odometer_at_service: 10500, + measurement_at_service: 10500, + measurement_unit: 'km', + cost_cents: 800, + currency: 'EUR', + notes: 'Chain Clean & Lube' + }, + { + id: generateId(), + vehicle_id: vehicleId, + tracker_id: chainTrackerId, + performed_at: '2025-09-15', + odometer_at_service: 13200, + measurement_at_service: 13200, + measurement_unit: 'km', + cost_cents: 800, + currency: 'EUR', + notes: 'Chain Clean & Lube' + }, + { + id: generateId(), + vehicle_id: vehicleId, + tracker_id: chainTrackerId, + performed_at: '2026-04-15', + odometer_at_service: 17950, + measurement_at_service: 17950, + measurement_unit: 'km', + cost_cents: 800, + currency: 'EUR', + notes: 'Chain Clean & Lube' + } + ]); + + await db.insert(finance_transactions).values([ + { + id: generateId(), + vehicle_id: vehicleId, + user_id: userId, + category: 'fuel', + amount_cents: 6000, + currency: 'EUR', + notes: 'Fuel fill-up', + performed_at: '2026-02-10' + }, + { + id: generateId(), + vehicle_id: vehicleId, + user_id: userId, + category: 'accessories', + amount_cents: 15000, + currency: 'EUR', + notes: 'Handlebar grips & bar end weights', + performed_at: '2025-11-05' + } + ]); + + await patchWorkflowRules(userId, vehicleId); + await patchDemoContent(userId, vehicleId); + console.log('[motomate] Demo data seeded (demo@motomate.local / password123)'); +} From 63791cab9dd1932018cbe05bfa091a63e3940502 Mon Sep 17 00:00:00 2001 From: hawkinslabdev <59891413+hawkinslabdev@users.noreply.github.com> Date: Wed, 20 May 2026 10:16:42 +0200 Subject: [PATCH 3/5] prepare: auth for demo_mode --- motomate/src/routes/(auth)/+layout.server.ts | 3 +- motomate/src/routes/(auth)/+layout.svelte | 180 +++++++++++++++--- motomate/src/routes/(auth)/login/+page.svelte | 42 ++-- 3 files changed, 181 insertions(+), 44 deletions(-) diff --git a/motomate/src/routes/(auth)/+layout.server.ts b/motomate/src/routes/(auth)/+layout.server.ts index a005cc2..ea61333 100644 --- a/motomate/src/routes/(auth)/+layout.server.ts +++ b/motomate/src/routes/(auth)/+layout.server.ts @@ -1,5 +1,6 @@ +import { env } from '$env/dynamic/public'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async () => { - return {}; + return { demoMode: env.PUBLIC_DEMO_ENABLED === 'true' }; }; diff --git a/motomate/src/routes/(auth)/+layout.svelte b/motomate/src/routes/(auth)/+layout.svelte index 6b74ca7..cb8098e 100644 --- a/motomate/src/routes/(auth)/+layout.svelte +++ b/motomate/src/routes/(auth)/+layout.svelte @@ -6,10 +6,34 @@ import Moon from '$lib/components/icons/Moon.svelte'; import Monitor from '$lib/components/icons/Monitor.svelte'; - let { children } = $props<{ + let { children, data } = $props<{ children: any; + data: { demoMode?: boolean }; }>(); + let copied = $state<'email' | 'password' | null>(null); + + async function copyToClipboard(text: string, field: 'email' | 'password') { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + } else { + const el = document.createElement('textarea'); + el.value = text; + el.style.position = 'fixed'; + el.style.opacity = '0'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + } + copied = field; + setTimeout(() => (copied = null), 1800); + } catch { + // perhaps clipboard unavailable + } + } + const themes = [ { id: 'light', label: 'Light', icon: Sun }, { id: 'dark', label: 'Dark', icon: Moon }, @@ -108,6 +132,22 @@ }} />
+ {#if data.demoMode} +
+ Demo instance +
+ + / + +
+
+ {/if}
-
diff --git a/motomate/src/routes/(auth)/login/+page.svelte b/motomate/src/routes/(auth)/login/+page.svelte index 2e590df..c9f906a 100644 --- a/motomate/src/routes/(auth)/login/+page.svelte +++ b/motomate/src/routes/(auth)/login/+page.svelte @@ -3,7 +3,7 @@ import { _ } from '$lib/i18n'; let { data, form } = $props<{ - data: { registrationEnabled: boolean }; + data: { registrationEnabled: boolean; demoMode?: boolean }; form: { error?: string; email?: string; @@ -39,20 +39,22 @@
{form.error}
{/if} -
- - -
+ {#if !data.demoMode} +
+ + +
+ {/if} {#if mode === 'password'}
{$_('auth.login.rememberMe')} - + {#if !data.demoMode} + + {/if}