355 lines
15 KiB
JavaScript
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
|
|
}
|
|
})
|