diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..104066d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,109 @@ +# Copilot Instructions for "That's Me" Project + +## Project Architecture + +This is a dual-stack application with separate **Laravel backend** and **Quasar frontend**: + +- **Backend** (`/backend/`): Laravel 12 with Livewire/Volt for server-side rendered admin panels +- **Frontend** (`/frontend/`): Quasar/Vue 3 SPA with anime.js for "LifeWave" visualizations +- **Core Feature**: Interactive wave visualization showing life events as animated points + +## Key Technology Patterns + +### Backend (Laravel/Livewire/Volt) +- **Livewire Volt**: Single-file components in `resources/views/livewire/` with PHP class logic at top +- **Flux UI**: Use `flux:` prefixed components (`flux:input`, `flux:button`, `flux:modal`) instead of raw HTML +- **Layout Pattern**: Nested layouts via `x-layouts.app` → `x-layouts.app.sidebar` for main app UI +- **Auth**: Uses Laravel Breeze with Volt components (`auth.login`, `auth.register`, etc.) +- **Routing**: Volt routes defined in `routes/web.php` as `Volt::route('path', 'component.name')` + +### Frontend (Quasar/Vue) +- **Animation Core**: Uses anime.js for wave animations (see `/test/` prototypes) +- **State Management**: Pinia for Vue state +- **Build Tool**: Vite with Quasar CLI +- **Components**: Lives in `frontend/src/pages/` and `frontend/src/components/` + +### Development Setup +- **Backend Dev**: `cd backend && php artisan serve` + `npm run dev` (Vite for assets) +- **Frontend Dev**: `cd frontend && quasar dev` +- **HTTPS Config**: Backend Vite configured for MAMP SSL certificates +- **Testing**: Pest for backend tests (`php artisan test`) + +## File Structure Conventions + +### Backend Key Files +- `routes/web.php`: Main routing with Volt component mappings +- `resources/views/livewire/`: Volt single-file components +- `resources/views/components/layouts/`: Layout components +- `app/Providers/VoltServiceProvider.php`: Volt mount configuration +- `resources/css/app.css`: Imports Tailwind + Flux UI + +### Frontend Key Files +- `src/pages/WavePage.vue`: Main life visualization component +- `src/utils/ConnectedDotsVisualization.js`: Wave animation utilities +- `quasar.config.js`: Build configuration +- `/test/anime-*.html`: Animation prototypes and examples + +## Specific Coding Patterns + +### Livewire/Volt Components +```php + + +
+ + +
+``` + +### Wave Animation Pattern +The project centers around animated life event visualizations: +- Life events have `value` (emotional weight), `x` (timeline position), `imageUrl`, `title` +- Uses anime.js for smooth wave animations with multiple sine wave layers +- See `test/anime-points-animation.html` for complete implementation patterns + +### Flux UI Usage +- Always use `flux:` components: `flux:button`, `flux:input`, `flux:modal`, `flux:navlist` +- Layout components: `flux:header`, `flux:sidebar`, `flux:main` +- Include `@fluxScripts` in layout files +- Wire navigation: `wire:navigate` for SPA-like navigation + +## Development Workflows + +- **Backend Changes**: Run `npm run dev` in `/backend/` for asset hot reload +- **Component Testing**: Use Pest with Volt test helpers: `Volt::test('component.name')` +- **Styling**: Tailwind + Flux UI (no custom CSS files needed) +- **Animation Prototyping**: Create in `/test/` directory first, then integrate + +## Project-Specific Guidelines + +1. **Animation First**: Wave visualizations are core - always consider animation performance +2. **Offline Support**: Frontend designed for PWA with offline capabilities +3. **Dual Development**: Backend and frontend are separate apps - coordinate API contracts +4. **Component Isolation**: Livewire components should be self-contained with embedded logic +5. **German Language**: UI text often in German (`__('German text')` for translations) + +## Common Commands + +```bash +# Backend +cd backend && composer install && php artisan serve +cd backend && npm run dev # Asset compilation + +# Frontend +cd frontend && npm install && quasar dev +cd frontend && quasar build # Production build + +# Testing +cd backend && php artisan test +``` \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cd4e8c4..edf944f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,7 +14,8 @@ "pinia": "^3.0.1", "quasar": "^2.16.0", "vue": "^3.4.18", - "vue-router": "^4.0.0" + "vue-router": "^4.0.0", + "vue-select": "^4.0.0-beta.6" }, "devDependencies": { "@eslint/js": "^9.14.0", @@ -7148,6 +7149,14 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/vue-select": { + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/vue-select/-/vue-select-4.0.0-beta.6.tgz", + "integrity": "sha512-K+zrNBSpwMPhAxYLTCl56gaMrWZGgayoWCLqe5rWwkB8aUbAUh7u6sXjIR7v4ckp2WKC7zEEUY27g6h1MRsIHw==", + "peerDependencies": { + "vue": "3.x" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index c8d94ac..bac294a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,8 @@ "pinia": "^3.0.1", "quasar": "^2.16.0", "vue": "^3.4.18", - "vue-router": "^4.0.0" + "vue-router": "^4.0.0", + "vue-select": "^4.0.0-beta.6" }, "devDependencies": { "@eslint/js": "^9.14.0", diff --git a/frontend/src/css/app.css b/frontend/src/css/app.css new file mode 100644 index 0000000..5cf80e8 --- /dev/null +++ b/frontend/src/css/app.css @@ -0,0 +1 @@ +.controls{display:flex;justify-content:space-between;width:100%;max-width:500px;margin-bottom:10px}.button{padding:6px 12px;background-color:#4f46e5;color:#fff;border:none;border-radius:4px;cursor:pointer}.visualization-container{position:relative;width:100%;height:calc(100vh - 86px);overflow:hidden}.gradient-bg{position:absolute;top:0;left:0;width:100%;height:100%;z-index:-1;background:linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2);background-size:200% 200%;animation:gradientAnimation 20s ease infinite}@keyframes gradientAnimation{0%{background-position:0% 0%}25%{background-position:100% 0%}50%{background-position:100% 100%}75%{background-position:0% 100%}100%{background-position:0% 0%}}.median{position:absolute;top:51%;left:0;right:0;height:2px;background-color:rgba(255,255,255,.3);z-index:10}.scroll-container{position:relative;width:100%;height:100%;overflow-x:auto;overflow-y:hidden;min-height:400px;-ms-overflow-style:none;scrollbar-width:none}.scroll-container::-webkit-scrollbar{display:none}.smooth-scroll{scroll-behavior:smooth}.active{cursor:grabbing}.spacer{height:100vh}.dot-tooltip{pointer-events:none;opacity:1}.dot-tooltip .tooltip-background{fill:rgba(0,0,0,0)}.dot-tooltip .tooltip-content{display:flex;justify-content:center;align-items:center;flex-direction:column;width:100%;height:100%;color:#fff}.dot-tooltip .image_container{margin-top:8px;box-shadow:0 0 20px 0 rgba(255,255,255,.25);transition:box-shadow .25s ease-in-out;width:80px;height:80px;overflow:hidden;border-radius:50%;border:2px solid #fff;display:flex;justify-content:center}.dot-tooltip .image_container:hover{box-shadow:0 0 30px 0 rgba(255,255,255,.8)}.dot-tooltip .tooltip-image{width:100%;height:auto;display:block;pointer-events:auto}.dot-tooltip .tooltip-title{font-size:14px;font-weight:400;margin-bottom:2px;text-align:center;text-wrap:balance;-webkit-hyphens:auto;hyphens:auto;line-height:1.1}.dot-tooltip .tooltip-description{font-size:12px;font-weight:300}.dot-tooltip .tooltip-arrow{width:1px;height:30px;background:linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent)}.dot{transition:r .2s ease,fill .2s ease;cursor:pointer}.dot:hover{fill:rgba(255,255,255,.9);filter:drop-shadow(0 0 5px rgba(255, 255, 255, 0.8))}.tooltip-img{width:100%;height:100%;-o-object-fit:cover;object-fit:cover;border-radius:4px} \ No newline at end of file diff --git a/frontend/src/css/app.scss b/frontend/src/css/app.scss index 814b596..4ddb09f 100644 --- a/frontend/src/css/app.scss +++ b/frontend/src/css/app.scss @@ -62,7 +62,7 @@ $image-size: 80px; .median { position: absolute; - top: 54.75%; + top: 51%; left: 0; right: 0; height: 2px; @@ -76,6 +76,7 @@ $image-size: 80px; height: 100%; overflow-x: auto; overflow-y: hidden; + min-height:400px; &::-webkit-scrollbar { display: none; } diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index a715f66..07cbb76 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -51,7 +51,7 @@ - + diff --git a/frontend/src/pages/EditPage.vue b/frontend/src/pages/EditPage.vue new file mode 100644 index 0000000..3c7703a --- /dev/null +++ b/frontend/src/pages/EditPage.vue @@ -0,0 +1,931 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/pages/WavePage.vue b/frontend/src/pages/WavePage.vue index 1e23d1a..8811faf 100644 --- a/frontend/src/pages/WavePage.vue +++ b/frontend/src/pages/WavePage.vue @@ -112,7 +112,7 @@ export default defineComponent({ imageUrl: "/images/0_3.png", title: "Beginn des neuen Abenteuers", description: "01.10.2024", - link: "/page1", + link: "/edit/1", }, { id: 2, @@ -121,7 +121,7 @@ export default defineComponent({ imageUrl: "/images/0_2.png", title: "Omas Annis Geburtstag", description: "02.10.2024", - link: "/page2", + link: "/edit/2", }, { id: 3, @@ -130,7 +130,7 @@ export default defineComponent({ imageUrl: "/images/disco.png", title: "Konzertbesuch mit Freunden", description: "03.10.2024", - link: "/page3", + link: "/edit/3", }, { id: 4, @@ -139,7 +139,7 @@ export default defineComponent({ imageUrl: "/images/pferd.png", title: "Wanderreiten in den Bergen", description: "04.10.2024", - link: "/page4", + link: "/edit/4", }, { id: 5, @@ -148,7 +148,7 @@ export default defineComponent({ imageUrl: "/images/gpt.png", title: "Ruhiger Tag zu Hause", description: "05.10.2024", - link: "/page5", + link: "/edit/5", }, { id: 6, @@ -157,7 +157,7 @@ export default defineComponent({ imageUrl: "/images/oma.png", title: "Oma Erna verstorben", description: "06.10.2024", - link: "/page6", + link: "/edit/6", }, { id: 7, @@ -166,7 +166,7 @@ export default defineComponent({ imageUrl: "/images/see.png", title: "Erholungsausflug zum See", description: "07.10.2024", - link: "/page7", + link: "/edit/7", }, { id: 8, @@ -175,7 +175,7 @@ export default defineComponent({ imageUrl: "/images/feier.png", title: "Kleine Wochenendsfeier", description: "08.10.2024", - link: "/page8", + link: "/edit/8", }, { id: 9, @@ -184,7 +184,7 @@ export default defineComponent({ imageUrl: "/images/hochzeit.png", title: "Hochzeit von Cousine Tatjana", description: "09.10.2024", - link: "/page9", + link: "/edit/9", }, { id: 10, @@ -193,7 +193,7 @@ export default defineComponent({ imageUrl: "/images/work.png", title: "Erster Tag im neuen Job", description: "10.10.2024", - link: "/page10", + link: "/edit/10", }, { id: 11, @@ -202,7 +202,7 @@ export default defineComponent({ imageUrl: "/images/klasse.png", title: "Klassentreffen nach vielen Jahren", description: "11.10.2024", - link: "/page11", + link: "/edit/11", }, { id: 12, @@ -211,7 +211,7 @@ export default defineComponent({ imageUrl: "/images/familie.png", title: "Familienabendessen", description: "12.10.2024", - link: "/page12", + link: "/edit/12", }, { id: 13, @@ -221,7 +221,7 @@ export default defineComponent({ "/images/kinobesuch.png", title: "Kinobesuch mit der ganzen Familie", description: "13.10.2024", - link: "/page13", + link: "/edit/13", }, { id: 14, @@ -231,7 +231,7 @@ export default defineComponent({ "/images/entspannung.png", title: "Entspannung", description: "14.10.2024", - link: "/page14", + link: "/edit/14", }, { id: 15, @@ -240,7 +240,7 @@ export default defineComponent({ imageUrl: "/images/sonntag.png", title: "Geruhsamer Sonntag", description: "15.10.2024", - link: "/page15", + link: "/edit/15", }, { id: 16, @@ -250,7 +250,7 @@ export default defineComponent({ "/images/kindergeburtstag.png", title: "Kindergeburtstag", description: "16.10.2024", - link: "/page16", + link: "/edit/16", }, { id: 17, @@ -260,7 +260,7 @@ export default defineComponent({ "/images/familie2.png", title: "Spaziergang mit der Familie", description: "17.10.2024", - link: "/page17", + link: "/edit/17", }, { id: 18, @@ -270,7 +270,7 @@ export default defineComponent({ "/images/grosseltern.png", title: "Familienfeier bei den Großeltern", description: "18.10.2024", - link: "/page18", + link: "/edit/18", }, ]; diff --git a/frontend/src/router/routes.js b/frontend/src/router/routes.js index 8abad25..374db6a 100644 --- a/frontend/src/router/routes.js +++ b/frontend/src/router/routes.js @@ -24,6 +24,11 @@ const routes = [ name: 'wave', component: () => import('pages/WavePage.vue') }, + { + path: 'edit/:id?', + name: 'edit', + component: () => import('pages/EditPage.vue') + }, ], }, diff --git a/frontend/src/utils/ConnectedDotsVisualization.ts b/frontend/src/utils/ConnectedDotsVisualization.ts index fb67049..b62a592 100644 --- a/frontend/src/utils/ConnectedDotsVisualization.ts +++ b/frontend/src/utils/ConnectedDotsVisualization.ts @@ -75,6 +75,26 @@ export class ConnectedDotsVisualization { }; // Initialize DOM elements this.scrollContainer = document.getElementById(containerId) as HTMLElement; + + // Calculate the container height dynamically + const containerHeight = + this.scrollContainer.clientHeight || + this.scrollContainer.offsetHeight || + window.innerHeight; + + // Default configuration + this.config = { + totalWidth: calculatedWidth, + height: containerHeight, // Use the calculated container height + dotRadius: 6, + xUnitSize: xUnitSize, + tension: 0.5, + showGrid: false, + tooltipWidth: 128, + tooltipHeight: 128, + ...config, + }; + // Create SVG elements this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); this.gridGroup = document.createElementNS( @@ -494,25 +514,26 @@ export class ConnectedDotsVisualization { } // Public API methods for external use public updateDots(newDots: DotConfig[]): void { - this.dots = newDots; - // Initial width calculation based on dot positions (for grid) - if (this.dots.length > 0) { - // Find the minimum and maximum x values - const minX = Math.min(...this.dots.map((dot) => dot.x)); - const maxX = Math.max(...this.dots.map((dot) => dot.x)); - // Calculate width based on the range of x values - // Add padding on both sides (3 units on each side) - this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize; - } - // Render will calculate the tooltip edges and update the SVG width - this.render(); - } + this.dots = newDots; + // Initial width calculation based on dot positions (for grid) + if (this.dots.length > 0) { + // Find the minimum and maximum x values + const minX = Math.min(...this.dots.map((dot) => dot.x)); + const maxX = Math.max(...this.dots.map((dot) => dot.x)); + // Calculate width based on the range of x values + // Add padding on both sides (3 units on each side) + this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize; + } + // Render will calculate the tooltip edges and update the SVG width + this.render(); +} public updateConfig(newConfig: Partial): void { this.config = { ...this.config, ...newConfig }; this.render(); } public resize(): void { - this.config.height = window.innerHeight; + const containerHeight = this.scrollContainer.clientHeight || this.scrollContainer.offsetHeight || window.innerHeight; + this.config.height = containerHeight; this.svg.setAttribute("height", `${this.config.height}`); this.render(); } diff --git a/frontend/src/utils/editFormOptions.js b/frontend/src/utils/editFormOptions.js new file mode 100644 index 0000000..825850f --- /dev/null +++ b/frontend/src/utils/editFormOptions.js @@ -0,0 +1,238 @@ +// Form options and data for EditPage component + +// Main categories with their subcategories +export const categoryStructure = [ + { + label: 'Career', + value: 'career', + subcategories: [ + { label: 'Promotion', value: 'career-promotion' }, + { label: 'Retirement', value: 'career-retirement' }, + { label: 'Career Changes', value: 'career-changes' } + ] + }, + { + label: 'Education', + value: 'education', + subcategories: [ + { label: 'Graduation', value: 'education-graduation' }, + { label: 'Schooling', value: 'education-schooling' } + ] + }, + { + label: 'Awards', + value: 'awards', + subcategories: [] + }, + { + label: 'Personal Celebrations', + value: 'personal-celebrations', + subcategories: [ + { label: 'Birthday', value: 'birthday' }, + { label: 'Anniversary', value: 'anniversary' } + ] + }, + { + label: 'Relationships', + value: 'relationships', + subcategories: [ + { label: 'Engagement', value: 'relationships-engagement' }, + { label: 'Marriage', value: 'relationships-marriage' }, + { label: 'Divorce', value: 'relationships-divorce' } + ] + }, + { + label: 'Parenthood', + value: 'parenthood', + subcategories: [ + { label: 'Pregnancy', value: 'parenthood-pregnancy' }, + { label: 'Birth', value: 'parenthood-birth' }, + { label: 'Adoption', value: 'parenthood-adoption' } + ] + }, + { + label: 'Loss & Passing', + value: 'passing', + subcategories: [ + { label: 'Funeral', value: 'passing-funeral' } + ] + }, + { + label: 'Festivities', + value: 'festivities', + subcategories: [ + { label: 'Christmas', value: 'festivities-christmas' }, + { label: 'Thanksgiving', value: 'festivities-thanksgiving' }, + { label: 'New Year', value: 'festivities-new-year' }, + { label: 'Easter', value: 'festivities-easter' }, + { label: 'Holidays', value: 'festivities-holidays' } + ] + }, + { + label: 'Social Events', + value: 'social-events', + subcategories: [ + { label: 'Reunions', value: 'reunions' }, + { label: 'Concerts', value: 'concerts' }, + { label: 'Sports', value: 'sports' }, + { label: 'Festivals', value: 'festivals' } + ] + }, + { + label: 'Community', + value: 'community', + subcategories: [ + { label: 'Charity', value: 'charity' }, + { label: 'Community Service', value: 'community-service' } + ] + }, + { + label: 'Health', + value: 'health', + subcategories: [ + { label: 'Surgery', value: 'health-surgery' }, + { label: 'Illness', value: 'health-illness' }, + { label: 'Recovery', value: 'health-recovery' }, + { label: 'Transplants', value: 'health-transplants' }, + { label: 'Mental Health', value: 'health-mental-health' } + ] + }, + { + label: 'Religious & Spiritual', + value: 'religious', + subcategories: [ + { label: 'Baptism', value: 'religious-baptism' }, + { label: 'Bar/Bat Mitzvah', value: 'religious-bar-bat-mitzvah' }, + { label: 'Communion', value: 'religious-communion' }, + { label: 'Confirmation', value: 'religious-confirmation' }, + { label: 'Pilgrimage', value: 'religious-pilgrimage' } + ] + }, + { + label: 'Travel & Adventure', + value: 'travel', + subcategories: [ + { label: 'Travel', value: 'travel-general' }, + { label: 'Vacation', value: 'vacation' }, + { label: 'Adventure', value: 'adventure' } + ] + }, + { + label: 'Life Changes', + value: 'life-changes', + subcategories: [ + { label: 'Moving', value: 'moving' }, + { label: 'License', value: 'license' }, + { label: 'Voting', value: 'voting' }, + { label: 'Citizenship', value: 'citizenship' } + ] + }, + { + label: 'Milestones', + value: 'milestones', + subcategories: [] + } +] + +// Flattened category options for backward compatibility and simple select usage +export const categoryOptions = categoryStructure.reduce((acc, category) => { + // Add main category if it has no subcategories + if (category.subcategories.length === 0) { + acc.push({ label: category.label, value: category.value }) + } else { + // Add subcategories with main category prefix + category.subcategories.forEach(sub => { + acc.push({ + label: `${category.label} - ${sub.label}`, + value: sub.value, + mainCategory: category.value, + subcategory: sub.value + }) + }) + } + return acc +}, []) + +// Helper functions for category management +export const getCategoryStructure = () => categoryStructure + +export const getMainCategories = () => { + return categoryStructure.map(cat => ({ + label: cat.label, + value: cat.value + })) +} + +export const getSubcategories = (mainCategoryValue) => { + const mainCategory = categoryStructure.find(cat => cat.value === mainCategoryValue) + return mainCategory ? mainCategory.subcategories : [] +} + +export const getCategoryByValue = (value) => { + return categoryOptions.find(cat => cat.value === value) +} + +export const getMainCategoryFromValue = (value) => { + const category = getCategoryByValue(value) + return category ? category.mainCategory : null +} + +export const tagOptions = [ + // Emotions + 'happy', 'sad', 'exciting', 'stressful', 'memorable', 'important', + 'fun', 'challenging', 'rewarding', 'disappointing', 'surprising', + 'life-changing', 'routine', 'special', 'difficult', 'joyful', + 'overwhelming', 'peaceful', 'anxious', 'proud', 'grateful', + 'emotional', 'touching', 'inspiring', 'motivating', 'healing', + + // Significance + 'milestone', 'achievement', 'breakthrough', 'turning-point', + 'first-time', 'last-time', 'once-in-a-lifetime', 'unexpected', + 'planned', 'spontaneous', 'tradition', 'new-experience', + + // Social + 'family', 'friends', 'colleagues', 'community', 'solo', + 'group', 'intimate', 'public', 'private', 'celebration', + + // Intensity + 'intense', 'mild', 'dramatic', 'subtle', 'overwhelming', + 'gradual', 'sudden', 'anticipated', 'shocking', 'gentle', + + // Time-related + 'brief', 'extended', 'momentary', 'lasting', 'temporary', + 'permanent', 'seasonal', 'annual', 'weekly', 'daily' +] + +export const personOptions = [ + 'Anna Mueller', + 'Max Schmidt', + 'Sarah Johnson', + 'Michael Weber', + 'Lisa Anderson', + 'Thomas Brown', + 'Julia Martinez', + 'David Wilson', + 'Emma Garcia', + 'Robert Davis' +] + +export const defaultFormData = { + keyImage: null, + keyImageUrl: '', + additionalImages: [], + additionalImageUrls: [], + level: 0, + category: '', + headline: '', + subheadline: '', + text: '', + tags: [], + location: '', + date: '', + time: '', + audioFiles: [], + audioRecordings: [], + videoFiles: [], + videoRecordings: [], + relatedPersons: [] +} \ No newline at end of file