thats-me/frontend/_src/stores/events.js
2026-04-22 12:57:10 +02:00

355 lines
15 KiB
JavaScript

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
}
})