import { defineStore } from 'pinia' import { ref, computed, watch } from 'vue' import { db } from 'src/db' import { startAutoSync, getToken } from 'src/services/syncService' // Color interpolation function lerpColor(a, b, t) { const ar = parseInt(a.slice(1, 3), 16) const ag = parseInt(a.slice(3, 5), 16) const ab = parseInt(a.slice(5, 7), 16) const br = parseInt(b.slice(1, 3), 16) const bg = parseInt(b.slice(3, 5), 16) const bb = parseInt(b.slice(5, 7), 16) const r = Math.round(ar + (br - ar) * t) const g = Math.round(ag + (bg - ag) * t) const blue = Math.round(ab + (bb - ab) * t) return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}` } // Gradient presets: [negative, neutral, positive] const GRADIENT_PRESETS = [ { name: 'Standard', colors: ['#E91E63', '#FFD700', '#4CAF50'] }, { name: 'Sunset', colors: ['#FD1D1D', '#FCB045', '#833AB4'] }, { name: 'Earth', colors: ['#ED8153', '#ED8153', '#217B9E'] }, { name: 'Ocean', colors: ['#00D4FF', '#164173', '#440559'] }, { name: 'Spring', colors: ['#FDBB2D', '#96BE74', '#22C1C3'] }, { name: 'Neon', colors: ['#FC466B', '#9A52B6', '#3F5EFB'] }, { name: 'Pastel', colors: ['#EEAECA', '#C2B4D9', '#94BBE9'] }, { name: 'Aurora', colors: ['#FF6B6B', '#C084FC', '#67E8F9'] }, { name: 'Forest', colors: ['#DC2626', '#A3A830', '#059669'] }, { name: 'Berry', colors: ['#F472B6', '#FB923C', '#A78BFA'] } ] // Glow color logic: emotion value → color, with optional gradient preset function emotionToColor(emotion, gradientIdx = null) { const preset = gradientIdx !== null ? GRADIENT_PRESETS[gradientIdx] : null if (preset) { const [neg, mid, pos] = preset.colors if (emotion >= 0) { return lerpColor(mid, pos, emotion) } else { return lerpColor(mid, neg, Math.abs(emotion)) } } if (emotion >= 0) { if (emotion < 0.5) { return lerpColor('#FF6B35', '#FFD700', emotion / 0.5) } return lerpColor('#FFD700', '#4CAF50', (emotion - 0.5) / 0.5) } else { const abs = Math.abs(emotion) if (abs < 0.5) { return lerpColor('#2196F3', '#9C27B0', abs / 0.5) } return lerpColor('#9C27B0', '#E91E63', (abs - 0.5) / 0.5) } } // Demo seed data const demoEvents = [ { id: crypto.randomUUID(), title: 'Erster Schultag', date: '1995-09-01', emotion: 0.6, customColor: null, gradientPreset: null, image: null, note: '', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }, { id: crypto.randomUUID(), title: 'Abiball', date: '2004-06-25', emotion: 0.85, customColor: null, gradientPreset: 1, image: 'demo/photo-1530103862676-de8c9debad1d.jpeg', note: 'Was für eine Party!', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }, { id: crypto.randomUUID(), title: 'Trennung', date: '2010-03-15', emotion: -0.7, customColor: null, gradientPreset: null, image: null, note: '', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }, { id: crypto.randomUUID(), title: 'Bergwanderung', date: '2014-08-12', emotion: 0.75, customColor: null, gradientPreset: 4, image: 'demo/photo-1534067783941-51c9c23ecefd.jpeg', note: 'Unvergesslicher Ausblick', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }, { id: crypto.randomUUID(), title: 'Jobverlust', date: '2016-11-03', emotion: -0.6, customColor: null, gradientPreset: null, image: null, note: '', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }, { id: crypto.randomUUID(), title: 'Hochzeit', date: '2018-07-20', emotion: 0.95, customColor: null, gradientPreset: 5, image: 'demo/photo-1506905925346-21bda4d32df4.jpeg', note: 'Der schönste Tag', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }, { id: crypto.randomUUID(), title: 'Umzug', date: '2021-04-01', emotion: -0.3, customColor: null, gradientPreset: null, image: null, note: '', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }, { id: crypto.randomUUID(), title: 'Neuer Job', date: '2023-01-10', emotion: 0.5, customColor: null, gradientPreset: null, image: 'demo/photo-1530103862676-de8c9debad1d.jpeg', note: 'Neues Kapitel', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() } ] // Generate realistic demo events for testing at scale function generateManyEvents(count = 500) { // Realistic life event categories with emotion ranges const categories = [ // Positive events { titles: ['Geburtstag', 'Geburtstagsfeier', 'Überraschungsparty'], emotionRange: [0.3, 0.8], noteChance: 0.4, notes: ['Tolles Fest!', 'Viele Geschenke', 'Schöner Tag mit Freunden', 'Alles Gute!'] }, { titles: ['Urlaub', 'Strandurlaub', 'Städtereise', 'Roadtrip', 'Backpacking'], emotionRange: [0.4, 0.95], noteChance: 0.6, notes: ['Unvergesslich', 'Wunderschöne Landschaft', 'Endlich Erholung', 'Muss ich wiederholen'] }, { titles: ['Hochzeit', 'Verlobung', 'Jahrestag'], emotionRange: [0.7, 1.0], noteChance: 0.8, notes: ['Der schönste Tag', 'Für immer', 'Tränen der Freude', 'Unbeschreiblich'] }, { titles: ['Beförderung', 'Neuer Job', 'Gehaltserhöhung', 'Jobangebot'], emotionRange: [0.5, 0.9], noteChance: 0.5, notes: ['Endlich!', 'Harte Arbeit zahlt sich aus', 'Neues Kapitel', 'Verdient'] }, { titles: ['Konzert', 'Festival', 'Theaterbesuch', 'Oper'], emotionRange: [0.3, 0.85], noteChance: 0.5, notes: ['Gänsehaut', 'Beste Band ever', 'Geniale Atmosphäre', 'Nächstes Jahr wieder'] }, { titles: ['Geburt', 'Baby da!', 'Nachwuchs'], emotionRange: [0.85, 1.0], noteChance: 0.9, notes: ['Das größte Wunder', 'Willkommen auf der Welt', 'Unbeschreibliches Glück'] }, { titles: ['Abschluss', 'Prüfung bestanden', 'Diplom', 'Master geschafft'], emotionRange: [0.6, 0.95], noteChance: 0.6, notes: ['Geschafft!', 'Jahre harter Arbeit', 'Stolz', 'Endlich vorbei'] }, { titles: ['Bergwanderung', 'Gipfel erreicht', 'Marathon geschafft', 'Triathlon'], emotionRange: [0.5, 0.9], noteChance: 0.5, notes: ['Was für ein Ausblick!', 'Körperliche Grenzen überwunden', 'Nie aufgeben'] }, { titles: ['Hauskauf', 'Wohnungseinweihung', 'Renovierung fertig'], emotionRange: [0.4, 0.8], noteChance: 0.5, notes: ['Endlich eigene vier Wände', 'Traum wird wahr', 'Viel Arbeit, aber es lohnt sich'] }, { titles: ['Erstes Date', 'Zusammengekommen', 'Liebeserklärung'], emotionRange: [0.5, 0.95], noteChance: 0.6, notes: ['Schmetterlinge', 'Liebe auf den ersten Blick', 'Endlich getraut'] }, // Neutral events { titles: ['Umzug', 'Neue Stadt', 'Wohnungswechsel'], emotionRange: [-0.2, 0.3], noteChance: 0.4, notes: ['Neuanfang', 'Alles anders', 'Spannend und stressig zugleich'] }, { titles: ['Arztbesuch', 'Vorsorge', 'Check-up'], emotionRange: [-0.1, 0.1], noteChance: 0.2, notes: ['Alles okay', 'Routine'] }, { titles: ['Meeting', 'Präsentation', 'Workshop'], emotionRange: [-0.1, 0.4], noteChance: 0.3, notes: ['Gut gelaufen', 'Viel gelernt', 'Anstrengend'] }, { titles: ['Friseur', 'Shopping', 'Einkauf'], emotionRange: [0.0, 0.3], noteChance: 0.1, notes: ['Neuer Look', 'Guter Fund'] }, // Negative events { titles: ['Trennung', 'Beziehungsende', 'Scheidung'], emotionRange: [-1.0, -0.5], noteChance: 0.5, notes: ['Schmerzhaft', 'Warum?', 'Es ist besser so', 'Brauche Zeit'] }, { titles: ['Jobverlust', 'Kündigung', 'Firma pleite'], emotionRange: [-0.9, -0.4], noteChance: 0.5, notes: ['Schock', 'Wie geht es weiter?', 'Unverdient'] }, { titles: ['Krankheit', 'OP', 'Krankenhaus'], emotionRange: [-0.8, -0.3], noteChance: 0.6, notes: ['Wird schon', 'Hauptsache gesund werden', 'Lange Genesung'] }, { titles: ['Abschied', 'Verlust', 'Trauer'], emotionRange: [-1.0, -0.6], noteChance: 0.7, notes: ['Ruhe in Frieden', 'Fehlt mir', 'Unvergessen', 'Schwerer Tag'] }, { titles: ['Streit', 'Konflikt', 'Auseinandersetzung'], emotionRange: [-0.7, -0.2], noteChance: 0.3, notes: ['Muss nicht sein', 'Hoffe auf Klärung'] }, { titles: ['Unfall', 'Panne', 'Autopanne'], emotionRange: [-0.6, -0.2], noteChance: 0.4, notes: ['Zum Glück nichts Schlimmes', 'Ärgerlich', 'Hätte schlimmer sein können'] }, { titles: ['Prüfung nicht bestanden', 'Absage', 'Ablehnung'], emotionRange: [-0.7, -0.3], noteChance: 0.4, notes: ['Nächstes Mal', 'Nicht aufgeben', 'Enttäuschend'] }, ] const demoImages = [ 'demo/photo-1530103862676-de8c9debad1d.jpeg', 'demo/photo-1534067783941-51c9c23ecefd.jpeg', 'demo/photo-1506905925346-21bda4d32df4.jpeg' ] // Seeded random for reproducibility let seed = 42 function rand() { seed = (seed * 16807 + 0) % 2147483647 return (seed - 1) / 2147483646 } function randInt(min, max) { return Math.floor(rand() * (max - min + 1)) + min } function pick(arr) { return arr[Math.floor(rand() * arr.length)] } function randFloat(min, max) { return Math.round((min + rand() * (max - min)) * 100) / 100 } const evts = [] const startYear = 1985 const endYear = 2026 // Generate events with realistic distribution (more events in recent years) for (let i = 0; i < count; i++) { // Weight towards recent years: cube root distribution const t = rand() const yearFloat = startYear + (endYear - startYear) * (t * t * 0.4 + t * 0.6) const year = Math.floor(yearFloat) const month = randInt(1, 12) const day = randInt(1, 28) // safe for all months const date = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}` const cat = pick(categories) const title = pick(cat.titles) const emotion = randFloat(cat.emotionRange[0], cat.emotionRange[1]) const hasNote = rand() < cat.noteChance const note = hasNote ? pick(cat.notes) : '' const hasImage = rand() < 0.15 // 15% chance const image = hasImage ? pick(demoImages) : null const hasPreset = rand() < 0.25 // 25% chance const gradientPreset = hasPreset ? randInt(0, 9) : null evts.push({ id: crypto.randomUUID(), title, date, emotion, customColor: null, gradientPreset, image, note, syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }) } // Sort by date evts.sort((a, b) => a.date.localeCompare(b.date)) return evts } export { emotionToColor, GRADIENT_PRESETS, demoEvents, generateManyEvents } export const useEventsStore = defineStore('events', () => { const events = ref([]) const isLoaded = ref(false) const selectedEventId = ref(null) const panelOpen = ref(false) const editingEventId = ref(null) // Load events from IndexedDB; seed demo data on first launch async function init() { try { let stored = await db.events.orderBy('date').toArray() if (stored.length === 0) { const seed = generateManyEvents(500) await db.events.bulkPut(seed) stored = seed } events.value = stored } catch (e) { console.warn('Dexie load failed, using demo data:', e) events.value = [...demoEvents] } isLoaded.value = true // Start auto-sync if authenticated getToken().then((token) => { if (token) startAutoSync() }) } // Fire-and-forget DB write (UI already updated via ref) function dbPut(event) { db.events.put(event).catch(e => console.warn('Dexie put failed:', e)) } function dbDelete(id) { db.events.delete(id).catch(e => console.warn('Dexie delete failed:', e)) } function dbQueueSync(eventId, action, payload) { db.syncQueue.add({ eventId, action, payload, createdAt: Date.now() }) .catch(e => console.warn('Dexie sync queue failed:', e)) } // Ghost event for live preview while creating/editing const ghostEmotion = ref(0) const ghostCustomColor = ref(null) const ghostGradientPreset = ref(null) const ghostTitle = ref('') const ghostDate = ref(new Date().toISOString().slice(0, 10)) const ghostNote = ref('') const ghostImage = ref(null) const ghostEvent = computed(() => ({ id: '__ghost__', title: ghostTitle.value || 'New Event', date: ghostDate.value, emotion: ghostEmotion.value, customColor: ghostCustomColor.value, gradientPreset: ghostGradientPreset.value, image: ghostImage.value, note: ghostNote.value })) const sortedEvents = computed(() => { return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date)) }) function selectEvent(id) { selectedEventId.value = id } function openPanel(eventId = null) { if (eventId) { editingEventId.value = eventId const event = events.value.find((e) => e.id === eventId) if (event) { ghostTitle.value = event.title ghostDate.value = event.date ghostEmotion.value = event.emotion ghostCustomColor.value = event.customColor ghostGradientPreset.value = event.gradientPreset ?? null ghostImage.value = event.image || null ghostNote.value = event.note } } else { editingEventId.value = null ghostTitle.value = '' ghostDate.value = new Date().toISOString().slice(0, 10) ghostEmotion.value = 0 ghostCustomColor.value = null ghostGradientPreset.value = null ghostImage.value = null ghostNote.value = '' } panelOpen.value = true } // Auto-save: persist ghost → event in edit mode function persistToEvent() { if (!editingEventId.value) return const idx = events.value.findIndex((e) => e.id === editingEventId.value) if (idx === -1) return const updated = { ...events.value[idx], title: ghostTitle.value, date: ghostDate.value, emotion: ghostEmotion.value, customColor: ghostCustomColor.value, gradientPreset: ghostGradientPreset.value, image: ghostImage.value, note: ghostNote.value, syncStatus: 'modified', updatedAt: Date.now() } events.value[idx] = updated dbPut(updated) } watch( [ghostTitle, ghostDate, ghostEmotion, ghostCustomColor, ghostGradientPreset, ghostImage, ghostNote], () => { persistToEvent() } ) function closePanel() { if (!editingEventId.value && ghostTitle.value.trim()) { const newEvent = { id: crypto.randomUUID(), title: ghostTitle.value, date: ghostDate.value, emotion: ghostEmotion.value, customColor: ghostCustomColor.value, gradientPreset: ghostGradientPreset.value, image: ghostImage.value, note: ghostNote.value, syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() } events.value.push(newEvent) dbPut(newEvent) dbQueueSync(newEvent.id, 'create', { ...newEvent }) } panelOpen.value = false editingEventId.value = null selectedEventId.value = null } function deleteEvent(id) { events.value = events.value.filter((e) => e.id !== id) dbDelete(id) dbQueueSync(id, 'delete', null) closePanel() } function getGlowColor(event) { if (event.customColor) return event.customColor return emotionToColor(event.emotion, event.gradientPreset ?? null) } // Auto-init on store creation init() return { events, isLoaded, selectedEventId, panelOpen, editingEventId, ghostEmotion, ghostCustomColor, ghostGradientPreset, ghostTitle, ghostDate, ghostNote, ghostImage, ghostEvent, sortedEvents, selectEvent, openPanel, closePanel, deleteEvent, getGlowColor } })