diff --git a/apps/moodlit/CLAUDE.md b/apps/moodlit/CLAUDE.md new file mode 100644 index 000000000..a897d80a1 --- /dev/null +++ b/apps/moodlit/CLAUDE.md @@ -0,0 +1,295 @@ +# Moodlit Project Guide + +## Übersicht + +**Moodlit** ist eine Ambient-Lighting-App, die es Benutzern ermöglicht, benutzerdefinierte Lichtstimmungen mit Farbverläufen und Animationen zu erstellen. Die App unterstützt sowohl bildschirmbasierte Beleuchtung als auch Geräte-Taschenlampensteuerung. + +| App | Port | URL | +|-----|------|-----| +| Backend | 3012 | http://localhost:3012 | +| Web App | 5182 | http://localhost:5182 | +| Landing Page | 4332 | http://localhost:4332 | + +## Project Structure + +``` +apps/moodlit/ +├── apps/ +│ ├── backend/ # NestJS API server (@moodlit/backend) +│ │ └── src/ +│ │ ├── main.ts +│ │ ├── app.module.ts +│ │ ├── db/ +│ │ │ ├── database.module.ts +│ │ │ ├── connection.ts +│ │ │ └── schema/ +│ │ │ ├── moods.schema.ts +│ │ │ └── sequences.schema.ts +│ │ ├── moods/ +│ │ │ ├── moods.module.ts +│ │ │ ├── moods.controller.ts +│ │ │ ├── moods.service.ts +│ │ │ └── dto/ +│ │ ├── sequences/ +│ │ │ ├── sequences.module.ts +│ │ │ ├── sequences.controller.ts +│ │ │ ├── sequences.service.ts +│ │ │ └── dto/ +│ │ └── health/ +│ │ +│ ├── web/ # SvelteKit web app (@moodlit/web) +│ │ └── src/ +│ │ ├── app.html +│ │ ├── app.css +│ │ └── routes/ +│ │ ├── +layout.svelte +│ │ └── +page.svelte +│ │ +│ ├── mobile/ # Expo React Native app (@moodlit/mobile) +│ │ ├── app/ # Expo Router routes +│ │ ├── components/ +│ │ ├── hooks/ +│ │ ├── store/ +│ │ └── utils/ +│ │ +│ └── landing/ # Astro landing page (@moodlit/landing) +│ +├── package.json +└── CLAUDE.md +``` + +## Commands + +### Root Level (from monorepo root) + +```bash +# Alle Apps starten +pnpm moodlit:dev # Run all moodlit apps + +# Einzelne Apps starten +pnpm dev:moodlit:backend # Start backend server (port 3012) +pnpm dev:moodlit:web # Start web app (port 5182) +pnpm dev:moodlit:mobile # Start mobile app +pnpm dev:moodlit:landing # Start landing page (port 4332) +pnpm dev:moodlit:app # Start web + backend together + +# Datenbank +pnpm moodlit:db:push # Push schema to database +pnpm moodlit:db:studio # Open Drizzle Studio +pnpm moodlit:db:seed # Seed initial data + +# Deploy +pnpm deploy:landing:moodlit # Deploy landing to Cloudflare Pages +``` + +### Backend (apps/moodlit/apps/backend) + +```bash +pnpm dev # Start with hot reload +pnpm build # Build for production +pnpm start:prod # Start production server +pnpm db:push # Push schema to database +pnpm db:studio # Open Drizzle Studio +``` + +### Web App (apps/moodlit/apps/web) + +```bash +pnpm dev # Start dev server +pnpm build # Build for production +pnpm preview # Preview production build +``` + +### Mobile App (apps/moodlit/apps/mobile) + +```bash +pnpm dev # Start Expo dev server +pnpm ios # Build and run iOS simulator +pnpm android # Build and run Android emulator +pnpm build:dev # EAS development build +``` + +### Landing Page (apps/moodlit/apps/landing) + +```bash +pnpm dev # Start dev server (port 4332) +pnpm build # Build for production +pnpm preview # Preview build +``` + +## Technology Stack + +| Layer | Technology | +|-------|------------| +| **Backend** | NestJS 10, Drizzle ORM, PostgreSQL | +| **Web** | SvelteKit 2.x, Svelte 5 (runes), Tailwind CSS 4 | +| **Mobile** | Expo SDK 54, React Native 0.81, NativeWind, Zustand | +| **Landing** | Astro 5.x, Tailwind CSS | +| **Auth** | Mana Core Auth (JWT) | + +## Features + +### 1. Mood Library +- Vorkonfigurierte Lichtstimmungen (Fire, Breath, Northern Lights, Thunder, etc.) +- Verschiedene Farbverläufe und Animationstypen +- Standard-Moods für jeden Benutzer + +### 2. Custom Moods +- Erstelle eigene Lichtstimmungen +- Anpassbare Farben und Animationen +- Speichern und Wiederverwenden + +### 3. Sequences +- Mehrere Moods zu einer Sequenz verketten +- Konfigurierbare Dauer und Übergänge +- Automatische Wiedergabe + +### 4. Dual Output +- Bildschirmbasierte Beleuchtung +- Geräte-Taschenlampensteuerung +- Umschalten zwischen Modi + +## API Endpoints + +### Health +``` +GET /api/v1/health # Health check +``` + +### Moods +``` +GET /api/v1/moods # List all moods +POST /api/v1/moods # Create mood +GET /api/v1/moods/:id # Get mood +PUT /api/v1/moods/:id # Update mood +DELETE /api/v1/moods/:id # Delete mood +``` + +### Sequences +``` +GET /api/v1/sequences # List all sequences +POST /api/v1/sequences # Create sequence +GET /api/v1/sequences/:id # Get sequence +PUT /api/v1/sequences/:id # Update sequence +DELETE /api/v1/sequences/:id # Delete sequence +``` + +## Database Schema + +### moods +| Column | Type | Description | +|--------|------|-------------| +| `id` | UUID | Primary key | +| `user_id` | TEXT | Owner | +| `name` | TEXT | Mood name | +| `colors` | JSONB | Array of color hex codes | +| `animation` | TEXT | Animation type | +| `is_default` | BOOLEAN | Default mood flag | +| `created_at` | TIMESTAMP | Created date | +| `updated_at` | TIMESTAMP | Updated date | + +### sequences +| Column | Type | Description | +|--------|------|-------------| +| `id` | UUID | Primary key | +| `user_id` | TEXT | Owner | +| `name` | TEXT | Sequence name | +| `mood_ids` | JSONB | Array of mood IDs | +| `duration` | INTEGER | Duration per mood (seconds) | +| `created_at` | TIMESTAMP | Created date | +| `updated_at` | TIMESTAMP | Updated date | + +## Environment Variables + +### Backend (.env) +```env +NODE_ENV=development +PORT=3012 +DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/moods +MANA_CORE_AUTH_URL=http://localhost:3001 +CORS_ORIGINS=http://localhost:5173,http://localhost:5182,http://localhost:8081 +DEV_BYPASS_AUTH=true +DEV_USER_ID=your-test-user-id +``` + +### Web (.env) +```env +PUBLIC_BACKEND_URL=http://localhost:3012 +PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +### Mobile (.env) +```env +EXPO_PUBLIC_BACKEND_URL=http://localhost:3012 +EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +## Quick Start + +### 1. Datenbank erstellen + +```bash +# PostgreSQL Container muss laufen +docker compose -f docker-compose.dev.yml up -d postgres + +# Datenbank erstellen +PGPASSWORD=devpassword psql -h localhost -U manacore -d postgres -c "CREATE DATABASE moods;" + +# Schema pushen +pnpm moodlit:db:push +``` + +### 2. Apps starten + +```bash +# Backend + Web zusammen +pnpm dev:moodlit:app + +# Oder einzeln: +pnpm dev:moodlit:backend # Terminal 1 +pnpm dev:moodlit:web # Terminal 2 +pnpm dev:moodlit:mobile # Terminal 3 +pnpm dev:moodlit:landing # Terminal 4 (optional) +``` + +### 3. URLs öffnen + +- Web App: http://localhost:5182 +- Landing: http://localhost:4332 +- API Health: http://localhost:3012/api/v1/health + +## Testing API (mit curl) + +```bash +# Health Check +curl http://localhost:3012/api/v1/health + +# Login (get token) +TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email": "test@example.com", "password": "password"}' | jq -r '.accessToken') + +# Moods abrufen +curl http://localhost:3012/api/v1/moods \ + -H "Authorization: Bearer $TOKEN" + +# Neues Mood erstellen +curl -X POST http://localhost:3012/api/v1/moods \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "Sunset", "colors": ["#ff6b6b", "#feca57", "#ff9ff3"], "animation": "gradient"}' + +# Sequence erstellen +curl -X POST http://localhost:3012/api/v1/sequences \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "Evening Flow", "moodIds": ["mood-id-1", "mood-id-2"], "duration": 30}' +``` + +## Important Notes + +1. **Authentication**: Nutzt Mana Core Auth (JWT im Authorization Header) +2. **Database**: PostgreSQL mit Drizzle ORM (Port 5432) +3. **Port**: Backend läuft auf Port 3012, Web auf 5182, Landing auf 4332 +4. **Mobile**: Verwendet Expo Dev Client (nicht Expo Go) wegen nativer Dependencies +5. **Theme**: Purple/Violet als Primärfarbe für die Mood-Thematik diff --git a/apps/moodlit/apps/backend/drizzle.config.ts b/apps/moodlit/apps/backend/drizzle.config.ts new file mode 100644 index 000000000..64eae1e4c --- /dev/null +++ b/apps/moodlit/apps/backend/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'postgresql', + schema: './src/db/schema/index.ts', + out: './src/db/migrations', + dbCredentials: { + url: process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/moods', + }, + verbose: true, + strict: true, +}); diff --git a/apps/moodlit/apps/backend/nest-cli.json b/apps/moodlit/apps/backend/nest-cli.json new file mode 100644 index 000000000..95538fb90 --- /dev/null +++ b/apps/moodlit/apps/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/moodlit/apps/backend/package.json b/apps/moodlit/apps/backend/package.json new file mode 100644 index 000000000..bc2daa683 --- /dev/null +++ b/apps/moodlit/apps/backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "@moodlit/backend", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "nest build", + "start": "nest start", + "dev": "nest start --watch", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "type-check": "tsc --noEmit", + "migration:generate": "drizzle-kit generate", + "migration:run": "tsx src/db/migrate.ts", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio", + "db:seed": "tsx src/db/seed.ts" + }, + "dependencies": { + "@manacore/shared-nestjs-auth": "workspace:*", + "@nestjs/common": "^10.4.15", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.15", + "@nestjs/platform-express": "^10.4.15", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dotenv": "^16.4.7", + "drizzle-kit": "^0.30.2", + "drizzle-orm": "^0.38.3", + "postgres": "^3.4.5", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.9", + "@nestjs/schematics": "^10.2.3", + "@types/express": "^5.0.0", + "@types/node": "^22.10.2", + "@typescript-eslint/eslint-plugin": "^8.18.1", + "@typescript-eslint/parser": "^8.18.1", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/apps/moodlit/apps/backend/src/app.module.ts b/apps/moodlit/apps/backend/src/app.module.ts new file mode 100644 index 000000000..ec1932d98 --- /dev/null +++ b/apps/moodlit/apps/backend/src/app.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DatabaseModule } from './db/database.module'; +import { HealthModule } from './health/health.module'; +import { MoodsModule } from './moods/moods.module'; +import { SequencesModule } from './sequences/sequences.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: '.env', + }), + DatabaseModule, + HealthModule, + MoodsModule, + SequencesModule, + ], +}) +export class AppModule {} diff --git a/apps/moodlit/apps/backend/src/db/connection.ts b/apps/moodlit/apps/backend/src/db/connection.ts new file mode 100644 index 000000000..fccc63f4a --- /dev/null +++ b/apps/moodlit/apps/backend/src/db/connection.ts @@ -0,0 +1,38 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import * as schema from './schema'; + +// Use require for postgres to avoid ESM/CommonJS interop issues +// eslint-disable-next-line @typescript-eslint/no-var-requires +const postgres = require('postgres'); + +let connection: ReturnType | null = null; +let db: ReturnType | null = null; + +export function getConnection(databaseUrl: string) { + if (!connection) { + connection = postgres(databaseUrl, { + max: 10, + idle_timeout: 20, + connect_timeout: 10, + }); + } + return connection; +} + +export function getDb(databaseUrl: string) { + if (!db) { + const conn = getConnection(databaseUrl); + db = drizzle(conn, { schema }); + } + return db; +} + +export async function closeConnection() { + if (connection) { + await connection.end(); + connection = null; + db = null; + } +} + +export type Database = ReturnType; diff --git a/apps/moodlit/apps/backend/src/db/database.module.ts b/apps/moodlit/apps/backend/src/db/database.module.ts new file mode 100644 index 000000000..b4d1f2af6 --- /dev/null +++ b/apps/moodlit/apps/backend/src/db/database.module.ts @@ -0,0 +1,28 @@ +import { Module, Global, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { getDb, closeConnection, type Database } from './connection'; + +export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; + +@Global() +@Module({ + providers: [ + { + provide: DATABASE_CONNECTION, + useFactory: (configService: ConfigService): Database => { + const databaseUrl = configService.get('DATABASE_URL'); + if (!databaseUrl) { + throw new Error('DATABASE_URL environment variable is not set'); + } + return getDb(databaseUrl); + }, + inject: [ConfigService], + }, + ], + exports: [DATABASE_CONNECTION], +}) +export class DatabaseModule implements OnModuleDestroy { + async onModuleDestroy() { + await closeConnection(); + } +} diff --git a/apps/moodlit/apps/backend/src/db/schema/index.ts b/apps/moodlit/apps/backend/src/db/schema/index.ts new file mode 100644 index 000000000..8d2070194 --- /dev/null +++ b/apps/moodlit/apps/backend/src/db/schema/index.ts @@ -0,0 +1,2 @@ +export * from './moods.schema'; +export * from './sequences.schema'; diff --git a/apps/moodlit/apps/backend/src/db/schema/moods.schema.ts b/apps/moodlit/apps/backend/src/db/schema/moods.schema.ts new file mode 100644 index 000000000..1e31e0e3a --- /dev/null +++ b/apps/moodlit/apps/backend/src/db/schema/moods.schema.ts @@ -0,0 +1,15 @@ +import { pgTable, uuid, text, jsonb, boolean, timestamp } from 'drizzle-orm/pg-core'; + +export const moods = pgTable('moods', { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id').notNull(), + name: text('name').notNull(), + colors: jsonb('colors').notNull().$type(), + animation: text('animation'), + isDefault: boolean('is_default').default(false), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +export type Mood = typeof moods.$inferSelect; +export type NewMood = typeof moods.$inferInsert; diff --git a/apps/moodlit/apps/backend/src/db/schema/sequences.schema.ts b/apps/moodlit/apps/backend/src/db/schema/sequences.schema.ts new file mode 100644 index 000000000..263238f3f --- /dev/null +++ b/apps/moodlit/apps/backend/src/db/schema/sequences.schema.ts @@ -0,0 +1,14 @@ +import { pgTable, uuid, text, jsonb, integer, timestamp } from 'drizzle-orm/pg-core'; + +export const sequences = pgTable('sequences', { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id').notNull(), + name: text('name').notNull(), + moodIds: jsonb('mood_ids').notNull().$type(), + duration: integer('duration').default(30), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +export type Sequence = typeof sequences.$inferSelect; +export type NewSequence = typeof sequences.$inferInsert; diff --git a/apps/moodlit/apps/backend/src/health/health.controller.ts b/apps/moodlit/apps/backend/src/health/health.controller.ts new file mode 100644 index 000000000..47b440bdc --- /dev/null +++ b/apps/moodlit/apps/backend/src/health/health.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('health') +export class HealthController { + @Get() + check() { + return { + status: 'ok', + timestamp: new Date().toISOString(), + service: 'moods-backend', + }; + } +} diff --git a/apps/moodlit/apps/backend/src/health/health.module.ts b/apps/moodlit/apps/backend/src/health/health.module.ts new file mode 100644 index 000000000..a61d8b044 --- /dev/null +++ b/apps/moodlit/apps/backend/src/health/health.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; + +@Module({ + controllers: [HealthController], +}) +export class HealthModule {} diff --git a/apps/moodlit/apps/backend/src/main.ts b/apps/moodlit/apps/backend/src/main.ts new file mode 100644 index 000000000..4c7ef872a --- /dev/null +++ b/apps/moodlit/apps/backend/src/main.ts @@ -0,0 +1,40 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // Enable CORS for mobile and web apps + const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((origin) => origin.trim()) || [ + 'http://localhost:3000', + 'http://localhost:5173', + 'http://localhost:5182', + 'http://localhost:8081', + 'exp://localhost:8081', + 'http://localhost:3001', + ]; + + app.enableCors({ + origin: corsOrigins, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + credentials: true, + }); + + // Enable validation + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }) + ); + + // Set global prefix for API routes + app.setGlobalPrefix('api/v1'); + + const port = process.env.PORT || 3012; + await app.listen(port); + console.log(`Moods backend running on http://localhost:${port}`); +} +bootstrap(); diff --git a/apps/moodlit/apps/backend/src/moods/dto/index.ts b/apps/moodlit/apps/backend/src/moods/dto/index.ts new file mode 100644 index 000000000..62c60e9e7 --- /dev/null +++ b/apps/moodlit/apps/backend/src/moods/dto/index.ts @@ -0,0 +1,37 @@ +import { IsString, IsArray, IsBoolean, IsOptional } from 'class-validator'; + +export class CreateMoodDto { + @IsString() + name: string; + + @IsArray() + @IsString({ each: true }) + colors: string[]; + + @IsString() + @IsOptional() + animation?: string; + + @IsBoolean() + @IsOptional() + isDefault?: boolean; +} + +export class UpdateMoodDto { + @IsString() + @IsOptional() + name?: string; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + colors?: string[]; + + @IsString() + @IsOptional() + animation?: string; + + @IsBoolean() + @IsOptional() + isDefault?: boolean; +} diff --git a/apps/moodlit/apps/backend/src/moods/moods.controller.ts b/apps/moodlit/apps/backend/src/moods/moods.controller.ts new file mode 100644 index 000000000..cf9f98c08 --- /dev/null +++ b/apps/moodlit/apps/backend/src/moods/moods.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { MoodsService } from './moods.service'; +import { CreateMoodDto, UpdateMoodDto } from './dto'; + +@Controller('moods') +@UseGuards(JwtAuthGuard) +export class MoodsController { + constructor(private readonly moodsService: MoodsService) {} + + @Get() + async findAll(@CurrentUser() user: CurrentUserData) { + return this.moodsService.findAllByUser(user.userId); + } + + @Get(':id') + async findOne(@Param('id') id: string, @CurrentUser() user: CurrentUserData) { + return this.moodsService.findOne(id, user.userId); + } + + @Post() + async create(@Body() dto: CreateMoodDto, @CurrentUser() user: CurrentUserData) { + return this.moodsService.create(user.userId, dto); + } + + @Put(':id') + async update( + @Param('id') id: string, + @Body() dto: UpdateMoodDto, + @CurrentUser() user: CurrentUserData + ) { + return this.moodsService.update(id, user.userId, dto); + } + + @Delete(':id') + async delete(@Param('id') id: string, @CurrentUser() user: CurrentUserData) { + await this.moodsService.delete(id, user.userId); + return { success: true }; + } +} diff --git a/apps/moodlit/apps/backend/src/moods/moods.module.ts b/apps/moodlit/apps/backend/src/moods/moods.module.ts new file mode 100644 index 000000000..fee9f966b --- /dev/null +++ b/apps/moodlit/apps/backend/src/moods/moods.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { MoodsController } from './moods.controller'; +import { MoodsService } from './moods.service'; + +@Module({ + controllers: [MoodsController], + providers: [MoodsService], + exports: [MoodsService], +}) +export class MoodsModule {} diff --git a/apps/moodlit/apps/backend/src/moods/moods.service.ts b/apps/moodlit/apps/backend/src/moods/moods.service.ts new file mode 100644 index 000000000..05d8df37e --- /dev/null +++ b/apps/moodlit/apps/backend/src/moods/moods.service.ts @@ -0,0 +1,64 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { eq, and } from 'drizzle-orm'; +import { DATABASE_CONNECTION } from '../db/database.module'; +import { type Database } from '../db/connection'; +import { moods, type Mood, type NewMood } from '../db/schema/moods.schema'; +import { CreateMoodDto, UpdateMoodDto } from './dto'; + +@Injectable() +export class MoodsService { + constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + + async findAllByUser(userId: string): Promise { + return this.db.select().from(moods).where(eq(moods.userId, userId)); + } + + async findOne(id: string, userId: string): Promise { + const [mood] = await this.db + .select() + .from(moods) + .where(and(eq(moods.id, id), eq(moods.userId, userId))); + + if (!mood) { + throw new NotFoundException(`Mood with ID ${id} not found`); + } + + return mood; + } + + async create(userId: string, dto: CreateMoodDto): Promise { + const newMood: NewMood = { + userId, + name: dto.name, + colors: dto.colors, + animation: dto.animation, + isDefault: dto.isDefault ?? false, + }; + + const [mood] = await this.db.insert(moods).values(newMood).returning(); + return mood; + } + + async update(id: string, userId: string, dto: UpdateMoodDto): Promise { + // Verify the mood exists and belongs to the user + await this.findOne(id, userId); + + const [updated] = await this.db + .update(moods) + .set({ + ...dto, + updatedAt: new Date(), + }) + .where(and(eq(moods.id, id), eq(moods.userId, userId))) + .returning(); + + return updated; + } + + async delete(id: string, userId: string): Promise { + // Verify the mood exists and belongs to the user + await this.findOne(id, userId); + + await this.db.delete(moods).where(and(eq(moods.id, id), eq(moods.userId, userId))); + } +} diff --git a/apps/moodlit/apps/backend/src/sequences/dto/index.ts b/apps/moodlit/apps/backend/src/sequences/dto/index.ts new file mode 100644 index 000000000..f97a818a4 --- /dev/null +++ b/apps/moodlit/apps/backend/src/sequences/dto/index.ts @@ -0,0 +1,29 @@ +import { IsString, IsArray, IsNumber, IsOptional } from 'class-validator'; + +export class CreateSequenceDto { + @IsString() + name: string; + + @IsArray() + @IsString({ each: true }) + moodIds: string[]; + + @IsNumber() + @IsOptional() + duration?: number; +} + +export class UpdateSequenceDto { + @IsString() + @IsOptional() + name?: string; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + moodIds?: string[]; + + @IsNumber() + @IsOptional() + duration?: number; +} diff --git a/apps/moodlit/apps/backend/src/sequences/sequences.controller.ts b/apps/moodlit/apps/backend/src/sequences/sequences.controller.ts new file mode 100644 index 000000000..b646af262 --- /dev/null +++ b/apps/moodlit/apps/backend/src/sequences/sequences.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { SequencesService } from './sequences.service'; +import { CreateSequenceDto, UpdateSequenceDto } from './dto'; + +@Controller('sequences') +@UseGuards(JwtAuthGuard) +export class SequencesController { + constructor(private readonly sequencesService: SequencesService) {} + + @Get() + async findAll(@CurrentUser() user: CurrentUserData) { + return this.sequencesService.findAllByUser(user.userId); + } + + @Get(':id') + async findOne(@Param('id') id: string, @CurrentUser() user: CurrentUserData) { + return this.sequencesService.findOne(id, user.userId); + } + + @Post() + async create(@Body() dto: CreateSequenceDto, @CurrentUser() user: CurrentUserData) { + return this.sequencesService.create(user.userId, dto); + } + + @Put(':id') + async update( + @Param('id') id: string, + @Body() dto: UpdateSequenceDto, + @CurrentUser() user: CurrentUserData + ) { + return this.sequencesService.update(id, user.userId, dto); + } + + @Delete(':id') + async delete(@Param('id') id: string, @CurrentUser() user: CurrentUserData) { + await this.sequencesService.delete(id, user.userId); + return { success: true }; + } +} diff --git a/apps/moodlit/apps/backend/src/sequences/sequences.module.ts b/apps/moodlit/apps/backend/src/sequences/sequences.module.ts new file mode 100644 index 000000000..37e5cfcb7 --- /dev/null +++ b/apps/moodlit/apps/backend/src/sequences/sequences.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SequencesController } from './sequences.controller'; +import { SequencesService } from './sequences.service'; + +@Module({ + controllers: [SequencesController], + providers: [SequencesService], + exports: [SequencesService], +}) +export class SequencesModule {} diff --git a/apps/moodlit/apps/backend/src/sequences/sequences.service.ts b/apps/moodlit/apps/backend/src/sequences/sequences.service.ts new file mode 100644 index 000000000..1506028fc --- /dev/null +++ b/apps/moodlit/apps/backend/src/sequences/sequences.service.ts @@ -0,0 +1,63 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { eq, and } from 'drizzle-orm'; +import { DATABASE_CONNECTION } from '../db/database.module'; +import { type Database } from '../db/connection'; +import { sequences, type Sequence, type NewSequence } from '../db/schema/sequences.schema'; +import { CreateSequenceDto, UpdateSequenceDto } from './dto'; + +@Injectable() +export class SequencesService { + constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + + async findAllByUser(userId: string): Promise { + return this.db.select().from(sequences).where(eq(sequences.userId, userId)); + } + + async findOne(id: string, userId: string): Promise { + const [sequence] = await this.db + .select() + .from(sequences) + .where(and(eq(sequences.id, id), eq(sequences.userId, userId))); + + if (!sequence) { + throw new NotFoundException(`Sequence with ID ${id} not found`); + } + + return sequence; + } + + async create(userId: string, dto: CreateSequenceDto): Promise { + const newSequence: NewSequence = { + userId, + name: dto.name, + moodIds: dto.moodIds, + duration: dto.duration ?? 30, + }; + + const [sequence] = await this.db.insert(sequences).values(newSequence).returning(); + return sequence; + } + + async update(id: string, userId: string, dto: UpdateSequenceDto): Promise { + // Verify the sequence exists and belongs to the user + await this.findOne(id, userId); + + const [updated] = await this.db + .update(sequences) + .set({ + ...dto, + updatedAt: new Date(), + }) + .where(and(eq(sequences.id, id), eq(sequences.userId, userId))) + .returning(); + + return updated; + } + + async delete(id: string, userId: string): Promise { + // Verify the sequence exists and belongs to the user + await this.findOne(id, userId); + + await this.db.delete(sequences).where(and(eq(sequences.id, id), eq(sequences.userId, userId))); + } +} diff --git a/apps/moodlit/apps/backend/tsconfig.json b/apps/moodlit/apps/backend/tsconfig.json new file mode 100644 index 000000000..44b209041 --- /dev/null +++ b/apps/moodlit/apps/backend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/moodlit/apps/landing/astro.config.mjs b/apps/moodlit/apps/landing/astro.config.mjs new file mode 100644 index 000000000..5611104d6 --- /dev/null +++ b/apps/moodlit/apps/landing/astro.config.mjs @@ -0,0 +1,19 @@ +import { defineConfig } from 'astro/config'; +import tailwind from '@astrojs/tailwind'; + +// https://astro.build/config +export default defineConfig({ + integrations: [tailwind()], + output: 'static', + build: { + inlineStylesheets: 'auto' + }, + vite: { + resolve: { + alias: { + '@components': '/src/components', + '@layouts': '/src/layouts' + } + } + } +}); diff --git a/apps/moodlit/apps/landing/package.json b/apps/moodlit/apps/landing/package.json new file mode 100644 index 000000000..64f5c4124 --- /dev/null +++ b/apps/moodlit/apps/landing/package.json @@ -0,0 +1,35 @@ +{ + "name": "@moodlit/landing", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev --port 4332", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "astro": "astro", + "type-check": "astro check", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", + "clean": "rm -rf dist .astro node_modules" + }, + "dependencies": { + "@astrojs/check": "^0.9.0", + "@manacore/shared-landing-ui": "workspace:*", + "astro": "^5.16.0", + "typescript": "^5.9.2" + }, + "devDependencies": { + "@astrojs/tailwind": "^6.0.2", + "@tailwindcss/typography": "^0.5.18", + "@types/node": "^20.0.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-astro": "^1.0.0", + "prettier": "^3.6.2", + "prettier-plugin-astro": "^0.14.1", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^3.4.0" + } +} diff --git a/apps/moodlit/apps/landing/src/layouts/Layout.astro b/apps/moodlit/apps/landing/src/layouts/Layout.astro new file mode 100644 index 000000000..246f532b9 --- /dev/null +++ b/apps/moodlit/apps/landing/src/layouts/Layout.astro @@ -0,0 +1,35 @@ +--- +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + + + {title} + + + + + + + diff --git a/apps/moodlit/apps/landing/src/pages/index.astro b/apps/moodlit/apps/landing/src/pages/index.astro new file mode 100644 index 000000000..777a06087 --- /dev/null +++ b/apps/moodlit/apps/landing/src/pages/index.astro @@ -0,0 +1,117 @@ +--- +import Layout from '../layouts/Layout.astro'; +--- + + +
+ +
+

+ Moodlit +

+

+ Transform your space with ambient lighting. Create custom moods, chain sequences, and let + the colors flow. +

+ +
+ + +
+

Features

+
+
+
+ 🎨 +
+

Custom Moods

+

+ Create your own lighting effects with custom colors and animations. +

+
+
+
+ 🔗 +
+

Sequences

+

+ Chain multiple moods together with configurable durations and transitions. +

+
+
+
+ 🔦 +
+

Dual Output

+

Toggle between screen-based lighting and device flashlight.

+
+
+
+ + +
+

Ready to set the mood?

+

Download Moodlit and transform your environment.

+ +
+ + +
+
+

© 2024 Moodlit. All rights reserved.

+
+
+
+
diff --git a/apps/moodlit/apps/landing/tailwind.config.mjs b/apps/moodlit/apps/landing/tailwind.config.mjs new file mode 100644 index 000000000..0146e6eda --- /dev/null +++ b/apps/moodlit/apps/landing/tailwind.config.mjs @@ -0,0 +1,24 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], + theme: { + extend: { + colors: { + primary: { + 50: '#fdf4ff', + 100: '#fae8ff', + 200: '#f5d0fe', + 300: '#f0abfc', + 400: '#e879f9', + 500: '#d946ef', + 600: '#c026d3', + 700: '#a21caf', + 800: '#86198f', + 900: '#701a75', + 950: '#4a044e', + }, + }, + }, + }, + plugins: [require('@tailwindcss/typography')], +}; diff --git a/apps/moodlit/apps/landing/tsconfig.json b/apps/moodlit/apps/landing/tsconfig.json new file mode 100644 index 000000000..4b0f22d55 --- /dev/null +++ b/apps/moodlit/apps/landing/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@components/*": ["src/components/*"], + "@layouts/*": ["src/layouts/*"] + } + } +} diff --git a/apps/moodlit/apps/landing/wrangler.toml b/apps/moodlit/apps/landing/wrangler.toml new file mode 100644 index 000000000..15c40bab0 --- /dev/null +++ b/apps/moodlit/apps/landing/wrangler.toml @@ -0,0 +1,3 @@ +name = "moodlit-landing" +compatibility_date = "2024-12-01" +pages_build_output_dir = "dist" diff --git a/apps/moodlit/apps/mobile/.gitignore b/apps/moodlit/apps/mobile/.gitignore new file mode 100644 index 000000000..1861e0868 --- /dev/null +++ b/apps/moodlit/apps/mobile/.gitignore @@ -0,0 +1,25 @@ +node_modules/ +.expo/ +dist/ +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ +# expo router +expo-env.d.ts + +# firebase/supabase/vexo +.env + +ios +android + +# macOS +.DS_Store + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* \ No newline at end of file diff --git a/apps/moodlit/apps/mobile/CLAUDE.md b/apps/moodlit/apps/mobile/CLAUDE.md new file mode 100644 index 000000000..bf2917d95 --- /dev/null +++ b/apps/moodlit/apps/mobile/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Moodlit** is a React Native mobile application built with Expo Router, targeting iOS and Android platforms. The app creates ambient lighting effects using the device's screen and flashlight with customizable color gradients and animations. It uses NativeWind (TailwindCSS for React Native) for styling and Zustand for state management. + +### Key Features +- **Mood Library**: Pre-configured lighting moods (Fire, Breath, Northern Lights, Thunder, etc.) with different color gradients and animation types +- **Custom Moods**: Create custom lighting effects with personalized colors and animations +- **Sequences**: Chain multiple moods together with configurable durations and transitions +- **Dual Output**: Toggle between screen-based lighting and device flashlight +- **Settings**: Adjustable animation speed, haptic feedback, brightness, and auto-timer functionality + +## Development Commands + +### Starting the Development Server +```bash +npm start # Start Expo dev server with dev client +npm run ios # Build and run on iOS simulator +npm run android # Build and run on Android emulator +npm run web # Run web version +``` + +### Building +```bash +npm run prebuild # Generate native directories for iOS/Android +npm run build:dev # Build development build via EAS +npm run build:preview # Build preview version via EAS +npm run build:prod # Build production version via EAS +``` + +### Code Quality +```bash +npm run lint # Run ESLint and Prettier check +npm run format # Auto-fix with ESLint and format with Prettier +``` + +## Architecture + +### Routing +- **Expo Router** (file-based routing): Routes are defined by file structure in the `app/` directory + - `app/_layout.tsx`: Root layout component that wraps all screens + - `app/index.tsx`: Home screen + - `app/details.tsx`: Details screen + - Route navigation uses `expo-router` Link component with typed routes enabled + +### State Management +- **Zustand**: Global state management in `store/store.ts` + - Store definitions follow a pattern of state + action methods + - Example store structure includes state interface and create function + +### Backend Integration +- **Supabase Client**: Configured in `utils/supabase.ts` + - Uses AsyncStorage for session persistence + - Environment variables required: + - `EXPO_PUBLIC_SUPABASE_URL` + - `EXPO_PUBLIC_SUPABASE_ANON_KEY` + - Auto-refresh tokens and persistent sessions enabled + +### Styling System +- **NativeWind**: TailwindCSS for React Native + - Global styles imported via `global.css` in root layout + - Tailwind config includes `app/**` and `components/**` content paths + - Styles defined as string literals with `className` prop (not `style`) + - Example: `className="flex flex-1 bg-white"` + +### Path Aliases +- TypeScript configured with `@/*` path alias mapping to root directory +- Import components/utils with `@/components/...` or `@/utils/...` + +### Components Structure +- Reusable components in `components/` directory: + - `Button.tsx`: Touchable button component + - `Container.tsx`: Layout wrapper + - `ScreenContent.tsx`: Screen template with title and separator + - `EditScreenInfo.tsx`: Info display component + +## Key Configuration Files + +- `app.json`: Expo configuration with typed routes and tsconfigPaths experiments enabled +- `tsconfig.json`: TypeScript with strict mode and path aliases +- `tailwind.config.js`: NativeWind preset with custom content paths +- `babel.config.js`: Babel configuration for Expo +- `metro.config.js`: Metro bundler configuration +- `.env`: Environment variables (not committed, contains Supabase credentials) + +## Development Notes + +- This project uses React 19.1.0 and React Native 0.81.5 +- Expo SDK version 54 +- TypeScript strict mode is enabled +- The app requires Expo Dev Client (not Expo Go) due to custom native dependencies +- Web support is available via Metro bundler with static output diff --git a/apps/moodlit/apps/mobile/app.json b/apps/moodlit/apps/mobile/app.json new file mode 100644 index 000000000..ea43e7168 --- /dev/null +++ b/apps/moodlit/apps/mobile/app.json @@ -0,0 +1,81 @@ +{ + "expo": { + "name": "Moodlit", + "slug": "moods", + "version": "1.0.0", + "scheme": "moods", + "platforms": ["ios", "android"], + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-camera", + { + "cameraPermission": "Erlaubt $(PRODUCT_NAME) die Kamera für die Taschenlampen-Funktion zu nutzen." + } + ], + [ + "expo-splash-screen", + { + "backgroundColor": "#000000", + "image": "./assets/splash.png", + "imageWidth": 200 + } + ] + ], + "experiments": { + "typedRoutes": true, + "tsconfigPaths": true + }, + "orientation": "default", + "icon": "./assets/mood-light-logo.png", + "userInterfaceStyle": "dark", + "assetBundlePatterns": ["**/*"], + "ios": { + "supportsTablet": true, + "requireFullScreen": false, + "bundleIdentifier": "com.tilljs.moodlight", + "icon": "./assets/mood-light.icon", + "infoPlist": { + "ITSAppUsesNonExemptEncryption": false, + "UIRequiresFullScreen": false, + "UIUserInterfaceStyle": "Dark", + "UISupportedInterfaceOrientations": [ + "UIInterfaceOrientationPortrait", + "UIInterfaceOrientationLandscapeLeft", + "UIInterfaceOrientationLandscapeRight" + ], + "UISupportedInterfaceOrientations~ipad": [ + "UIInterfaceOrientationPortrait", + "UIInterfaceOrientationPortraitUpsideDown", + "UIInterfaceOrientationLandscapeLeft", + "UIInterfaceOrientationLandscapeRight" + ] + } + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/mood-light-logo.png", + "backgroundColor": "#000000" + }, + "package": "com.tilljs.moodlight", + "permissions": [ + "CAMERA", + "FLASHLIGHT", + "android.permission.CAMERA", + "android.permission.RECORD_AUDIO" + ] + }, + "extra": { + "router": {}, + "eas": { + "projectId": "faec0f17-97e2-4be5-9a85-d281b5635e7a" + } + }, + "owner": "memoro" + } +} diff --git a/apps/moodlit/apps/mobile/app/+html.tsx b/apps/moodlit/apps/mobile/app/+html.tsx new file mode 100644 index 000000000..447d6a0a5 --- /dev/null +++ b/apps/moodlit/apps/mobile/app/+html.tsx @@ -0,0 +1,46 @@ +import { ScrollViewStyleReset } from 'expo-router/html'; + +// This file is web-only and used to configure the root HTML for every +// web page during static rendering. +// The contents of this function only run in Node.js environments and +// do not have access to the DOM or browser APIs. +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + {/* + This viewport disables scaling which makes the mobile website act more like a native app. + However this does reduce built-in accessibility. If you want to enable scaling, use this instead: + + */} + + {/* + Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. + However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. + */} + + + {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} + diff --git a/apps/moodlit/apps/web/src/lib/components/mood/MoodFullscreen.svelte b/apps/moodlit/apps/web/src/lib/components/mood/MoodFullscreen.svelte new file mode 100644 index 000000000..49c074c1b --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/components/mood/MoodFullscreen.svelte @@ -0,0 +1,589 @@ + + + + + + + diff --git a/apps/moodlit/apps/web/src/lib/data/default-moods.ts b/apps/moodlit/apps/web/src/lib/data/default-moods.ts new file mode 100644 index 000000000..fb39322a0 --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/data/default-moods.ts @@ -0,0 +1,195 @@ +import type { Mood } from '$lib/types/mood'; + +// 24 preset moods matching the mobile app +export const DEFAULT_MOODS: Mood[] = [ + { + id: 'fire', + name: 'Fire', + colors: ['#ff6b35', '#ff4500', '#dc143c', '#8b0000'], + animationType: 'candle', + order: 0, + }, + { + id: 'breath', + name: 'Breath', + colors: ['#667eea', '#764ba2', '#f093fb'], + animationType: 'breath', + order: 1, + }, + { + id: 'northern-lights', + name: 'Northern Lights', + colors: ['#5f27cd', '#341f97', '#8854d0', '#a29bfe'], + animationType: 'wave', + order: 2, + }, + { + id: 'thunder', + name: 'Thunder', + colors: ['#2c3e50', '#34495e', '#ffffff', '#95a5a6'], + animationType: 'thunder', + order: 3, + }, + { + id: 'light', + name: 'Light', + colors: ['#ffffff', '#f8f9fa', '#e9ecef'], + animationType: 'gradient', + order: 4, + }, + { + id: 'flash', + name: 'Flash', + colors: ['#ffffff'], + animationType: 'flash', + order: 5, + }, + { + id: 'sos', + name: 'SOS', + colors: ['#ffffff'], + animationType: 'sos', + order: 6, + }, + { + id: 'ocean', + name: 'Ocean', + colors: ['#48dbfb', '#0abde3', '#10ac84', '#1dd1a1'], + animationType: 'wave', + order: 7, + }, + { + id: 'candle', + name: 'Candle', + colors: ['#ff9f43', '#ee5a24', '#ffeaa7'], + animationType: 'candle', + order: 8, + }, + { + id: 'police', + name: 'Police', + colors: ['#e74c3c', '#3498db'], + animationType: 'police', + order: 9, + }, + { + id: 'warning', + name: 'Warning', + colors: ['#f39c12', '#e67e22'], + animationType: 'warning', + order: 10, + }, + { + id: 'disco', + name: 'Disco', + colors: ['#e74c3c', '#9b59b6', '#3498db', '#1abc9c', '#f1c40f', '#e67e22'], + animationType: 'disco', + order: 11, + }, + { + id: 'sunrise', + name: 'Sunrise', + colors: ['#1a1a2e', '#16213e', '#e94560', '#ff6b6b', '#feca57', '#fffacd'], + animationType: 'sunrise', + order: 12, + }, + { + id: 'sunset', + name: 'Sunset', + colors: ['#ff6b6b', '#feca57', '#ff9ff3', '#a29bfe', '#341f97', '#1a1a2e'], + animationType: 'sunset', + order: 13, + }, + { + id: 'forest', + name: 'Forest', + colors: ['#27ae60', '#2ecc71', '#1abc9c', '#16a085'], + animationType: 'pulse', + order: 14, + }, + { + id: 'rave', + name: 'Rave', + colors: [ + '#ff0000', + '#ff00ff', + '#00ffff', + '#00ff00', + '#ffff00', + '#ff6600', + '#0066ff', + '#ff0066', + ], + animationType: 'rave', + order: 15, + }, + { + id: 'scanner', + name: 'Scanner', + colors: ['#e74c3c'], + animationType: 'scanner', + order: 16, + }, + { + id: 'matrix', + name: 'Matrix', + colors: ['#00ff00'], + animationType: 'matrix', + order: 17, + }, + { + id: 'lavender', + name: 'Lavender', + colors: ['#e6e6fa', '#dda0dd', '#da70d6', '#ba55d3'], + animationType: 'pulse', + order: 18, + }, + { + id: 'cherry-blossom', + name: 'Cherry Blossom', + colors: ['#ffb7c5', '#ff69b4', '#ff1493', '#db7093'], + animationType: 'wave', + order: 19, + }, + { + id: 'autumn', + name: 'Autumn', + colors: ['#d35400', '#e67e22', '#f39c12', '#c0392b'], + animationType: 'gradient', + order: 20, + }, + { + id: 'ice', + name: 'Ice', + colors: ['#74b9ff', '#0984e3', '#81ecec', '#00cec9'], + animationType: 'wave', + order: 21, + }, + { + id: 'romance', + name: 'Romance', + colors: ['#fd79a8', '#e84393', '#d63031', '#ff7675'], + animationType: 'pulse', + order: 22, + }, + { + id: 'midnight', + name: 'Midnight', + colors: ['#0c0c0c', '#1a1a2e', '#16213e', '#0f3460'], + animationType: 'breath', + order: 23, + }, +]; + +// Get mood by ID +export function getMoodById(id: string): Mood | undefined { + return DEFAULT_MOODS.find((m) => m.id === id); +} + +// Get gradient CSS for a mood +export function getMoodGradient(mood: Mood): string { + if (mood.colors.length === 1) { + return mood.colors[0]; + } + return `linear-gradient(135deg, ${mood.colors.join(', ')})`; +} diff --git a/apps/moodlit/apps/web/src/lib/i18n/index.ts b/apps/moodlit/apps/web/src/lib/i18n/index.ts new file mode 100644 index 000000000..efad3acc3 --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/i18n/index.ts @@ -0,0 +1,49 @@ +import { browser } from '$app/environment'; +import { init, register, locale, waitLocale } from 'svelte-i18n'; + +// List of supported locales +export const supportedLocales = ['de', 'en'] as const; +export type SupportedLocale = (typeof supportedLocales)[number]; + +// Default locale +const defaultLocale = 'de'; + +// Register all available locales +register('de', () => import('./locales/de.json')); +register('en', () => import('./locales/en.json')); + +// Get initial locale from browser or localStorage +function getInitialLocale(): SupportedLocale { + if (browser) { + // Check localStorage first + const stored = localStorage.getItem('moodlit_locale'); + if (stored && supportedLocales.includes(stored as SupportedLocale)) { + return stored as SupportedLocale; + } + + // Fall back to browser language + const browserLang = navigator.language.split('-')[0]; + if (supportedLocales.includes(browserLang as SupportedLocale)) { + return browserLang as SupportedLocale; + } + } + + return defaultLocale; +} + +// Initialize i18n at module scope (required for SSR) +init({ + fallbackLocale: defaultLocale, + initialLocale: getInitialLocale(), +}); + +// Set locale and persist to localStorage +export function setLocale(newLocale: SupportedLocale) { + locale.set(newLocale); + if (browser) { + localStorage.setItem('moodlit_locale', newLocale); + } +} + +// Wait for locale to be loaded (useful for SSR) +export { waitLocale }; diff --git a/apps/moodlit/apps/web/src/lib/i18n/locales/de.json b/apps/moodlit/apps/web/src/lib/i18n/locales/de.json new file mode 100644 index 000000000..dd65374e9 --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/i18n/locales/de.json @@ -0,0 +1,78 @@ +{ + "app": { + "name": "Moodlit", + "tagline": "Ambient Lighting & Moods" + }, + "nav": { + "home": "Startseite", + "moods": "Moods", + "sequences": "Sequenzen", + "settings": "Einstellungen", + "feedback": "Feedback" + }, + "home": { + "title": "Deine Moods", + "subtitle": "Wähle eine Lichtstimmung", + "sequences": "Sequenzen", + "sequencesDescription": "Verkette mehrere Moods zu einer Sequenz", + "favorites": "Favoriten", + "all": "Alle Moods", + "custom": "Eigene Moods" + }, + "sequences": { + "title": "Sequenzen", + "subtitle": "Spiele mehrere Moods nacheinander ab", + "moods": "Moods", + "empty": "Noch keine Sequenzen", + "emptyDescription": "Erstelle eine Sequenz, indem du mehrere Moods verkettest." + }, + "mood": { + "play": "Abspielen", + "pause": "Pause", + "edit": "Bearbeiten", + "delete": "Löschen", + "addToFavorites": "Zu Favoriten", + "removeFromFavorites": "Aus Favoriten", + "animation": "Animation", + "colors": "Farben", + "startTimer": "Start", + "stopTimer": "Timer stoppen", + "timerRunning": "Timer läuft", + "stop": "Stopp" + }, + "settings": { + "title": "Einstellungen", + "animationSpeed": "Animationsgeschwindigkeit", + "slow": "Langsam", + "normal": "Normal", + "fast": "Schnell", + "brightness": "Helligkeit", + "autoTimer": "Auto-Timer", + "autoTimerOff": "Aus", + "autoTimerMinutes": "{minutes} Minuten", + "autoMoodSwitch": "Auto-Mood-Wechsel", + "autoMoodSwitchInterval": "Wechsel-Intervall", + "reset": "Zurücksetzen", + "resetConfirm": "Alle Einstellungen zurücksetzen?" + }, + "createMood": { + "title": "Mood erstellen", + "editTitle": "Mood bearbeiten", + "name": "Name", + "namePlaceholder": "Mood-Name eingeben...", + "colors": "Farben", + "addColor": "Farbe hinzufügen", + "animation": "Animationstyp", + "preview": "Vorschau" + }, + "common": { + "save": "Speichern", + "cancel": "Abbrechen", + "delete": "Löschen", + "confirm": "Bestätigen", + "loading": "Lädt...", + "error": "Fehler", + "success": "Erfolgreich", + "create": "Erstellen" + } +} diff --git a/apps/moodlit/apps/web/src/lib/i18n/locales/en.json b/apps/moodlit/apps/web/src/lib/i18n/locales/en.json new file mode 100644 index 000000000..f145db185 --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/i18n/locales/en.json @@ -0,0 +1,78 @@ +{ + "app": { + "name": "Moodlit", + "tagline": "Ambient Lighting & Moods" + }, + "nav": { + "home": "Home", + "moods": "Moods", + "sequences": "Sequences", + "settings": "Settings", + "feedback": "Feedback" + }, + "home": { + "title": "Your Moods", + "subtitle": "Choose a lighting mood", + "sequences": "Sequences", + "sequencesDescription": "Chain multiple moods into a sequence", + "favorites": "Favorites", + "all": "All Moods", + "custom": "Custom Moods" + }, + "sequences": { + "title": "Sequences", + "subtitle": "Play multiple moods in sequence", + "moods": "moods", + "empty": "No Sequences Yet", + "emptyDescription": "Create a sequence by chaining multiple moods together." + }, + "mood": { + "play": "Play", + "pause": "Pause", + "edit": "Edit", + "delete": "Delete", + "addToFavorites": "Add to Favorites", + "removeFromFavorites": "Remove from Favorites", + "animation": "Animation", + "colors": "Colors", + "startTimer": "Start", + "stopTimer": "Stop Timer", + "timerRunning": "Timer running", + "stop": "Stop" + }, + "settings": { + "title": "Settings", + "animationSpeed": "Animation Speed", + "slow": "Slow", + "normal": "Normal", + "fast": "Fast", + "brightness": "Brightness", + "autoTimer": "Auto Timer", + "autoTimerOff": "Off", + "autoTimerMinutes": "{minutes} minutes", + "autoMoodSwitch": "Auto Mood Switch", + "autoMoodSwitchInterval": "Switch Interval", + "reset": "Reset", + "resetConfirm": "Reset all settings?" + }, + "createMood": { + "title": "Create Mood", + "editTitle": "Edit Mood", + "name": "Name", + "namePlaceholder": "Enter mood name...", + "colors": "Colors", + "addColor": "Add Color", + "animation": "Animation Type", + "preview": "Preview" + }, + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "confirm": "Confirm", + "loading": "Loading...", + "error": "Error", + "success": "Success", + "create": "Create" + } +} diff --git a/apps/moodlit/apps/web/src/lib/stores/authStore.svelte.ts b/apps/moodlit/apps/web/src/lib/stores/authStore.svelte.ts new file mode 100644 index 000000000..f34d6d8da --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/stores/authStore.svelte.ts @@ -0,0 +1,118 @@ +import type { MoodlitUser } from '$lib/types/auth'; +import { authService, type UserData } from '$lib/auth'; + +// Svelte 5 runes-based auth store +let user = $state(null); +let loading = $state(true); + +/** + * Convert UserData from shared-auth to MoodlitUser + */ +function toMoodlitUser(userData: UserData | null): MoodlitUser | null { + if (!userData) return null; + return { + id: userData.id, + email: userData.email, + role: userData.role, + }; +} + +export const authStore = { + get user() { + return user; + }, + get loading() { + return loading; + }, + get isAuthenticated() { + return !!user; + }, + + /** + * Initialize auth state from stored tokens + */ + async initialize() { + loading = true; + try { + const isAuth = await authService.isAuthenticated(); + if (isAuth) { + const userData = await authService.getUserFromToken(); + user = toMoodlitUser(userData); + } + } catch (error) { + console.error('Failed to initialize auth:', error); + user = null; + } finally { + loading = false; + } + }, + + /** + * Set user + */ + setUser(newUser: MoodlitUser | null) { + user = newUser; + }, + + /** + * Sign out + */ + async signOut() { + try { + await authService.signOut(); + user = null; + } catch (error) { + console.error('Sign out failed:', error); + } + }, + + /** + * Check authentication status + */ + async checkAuth() { + const isAuth = await authService.isAuthenticated(); + if (!isAuth) { + user = null; + return false; + } + return true; + }, + + /** + * Sign in with email and password + */ + async signIn(email: string, password: string) { + const result = await authService.signIn(email, password); + if (result.success) { + const userData = await authService.getUserFromToken(); + user = toMoodlitUser(userData); + } + return result; + }, + + /** + * Sign up with email and password + */ + async signUp(email: string, password: string) { + const result = await authService.signUp(email, password); + if (result.success && !result.needsVerification) { + const userData = await authService.getUserFromToken(); + user = toMoodlitUser(userData); + } + return result; + }, + + /** + * Send password reset email + */ + async forgotPassword(email: string) { + return authService.forgotPassword(email); + }, + + /** + * Get access token for API calls + */ + async getAccessToken(): Promise { + return authService.getAppToken(); + }, +}; diff --git a/apps/moodlit/apps/web/src/lib/stores/moods.svelte.ts b/apps/moodlit/apps/web/src/lib/stores/moods.svelte.ts new file mode 100644 index 000000000..10f8a96b7 --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/stores/moods.svelte.ts @@ -0,0 +1,116 @@ +import type { Mood, MoodSettings } from '$lib/types/mood'; + +// Default settings +const DEFAULT_SETTINGS: MoodSettings = { + animationSpeed: 'normal', + brightness: 100, + autoTimer: 0, + autoMoodSwitch: false, + autoMoodSwitchInterval: 5, +}; + +// Moods store using Svelte 5 runes +function createMoodsStore() { + let customMoods = $state([]); + let favoriteIds = $state([]); + let settings = $state({ ...DEFAULT_SETTINGS }); + let activeMood = $state(null); + + // Load from localStorage on init + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('moodlit-store'); + if (saved) { + try { + const parsed = JSON.parse(saved); + if (parsed.customMoods) customMoods = parsed.customMoods; + if (parsed.favoriteIds) favoriteIds = parsed.favoriteIds; + if (parsed.settings) settings = { ...DEFAULT_SETTINGS, ...parsed.settings }; + } catch (e) { + console.error('Failed to load moods from localStorage', e); + } + } + } + + // Save to localStorage + function persist() { + if (typeof window !== 'undefined') { + localStorage.setItem('moodlit-store', JSON.stringify({ customMoods, favoriteIds, settings })); + } + } + + return { + get customMoods() { + return customMoods; + }, + get favoriteIds() { + return favoriteIds; + }, + get settings() { + return settings; + }, + get activeMood() { + return activeMood; + }, + + // Check if a mood is a favorite + isFavorite(moodId: string): boolean { + return favoriteIds.includes(moodId); + }, + + setActiveMood(mood: Mood | null) { + activeMood = mood; + }, + + addMood(mood: Mood) { + customMoods = [...customMoods, mood]; + persist(); + }, + + updateMood(id: string, updates: Partial) { + customMoods = customMoods.map((m) => (m.id === id ? { ...m, ...updates } : m)); + persist(); + }, + + removeMood(id: string) { + customMoods = customMoods.filter((m) => m.id !== id); + // Also remove from favorites + favoriteIds = favoriteIds.filter((fid) => fid !== id); + persist(); + }, + + toggleFavorite(moodId: string) { + if (favoriteIds.includes(moodId)) { + favoriteIds = favoriteIds.filter((id) => id !== moodId); + } else { + favoriteIds = [...favoriteIds, moodId]; + } + persist(); + }, + + addToFavorites(moodId: string) { + if (!favoriteIds.includes(moodId)) { + favoriteIds = [...favoriteIds, moodId]; + persist(); + } + }, + + removeFromFavorites(moodId: string) { + favoriteIds = favoriteIds.filter((id) => id !== moodId); + persist(); + }, + + updateSettings(updates: Partial) { + settings = { ...settings, ...updates }; + persist(); + }, + + resetToDefaults() { + customMoods = []; + favoriteIds = []; + settings = { ...DEFAULT_SETTINGS }; + persist(); + }, + }; +} + +export const moodsStore = createMoodsStore(); diff --git a/apps/moodlit/apps/web/src/lib/stores/navigation.ts b/apps/moodlit/apps/web/src/lib/stores/navigation.ts new file mode 100644 index 000000000..83da2edfa --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/stores/navigation.ts @@ -0,0 +1,7 @@ +import { writable } from 'svelte/store'; + +// Store for sidebar mode (pill vs sidebar navigation) +export const isSidebarMode = writable(false); + +// Store for collapsed state +export const isNavCollapsed = writable(false); diff --git a/apps/moodlit/apps/web/src/lib/stores/sequences.svelte.ts b/apps/moodlit/apps/web/src/lib/stores/sequences.svelte.ts new file mode 100644 index 000000000..8754dac8a --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/stores/sequences.svelte.ts @@ -0,0 +1,129 @@ +import type { MoodSequence } from '$lib/types/mood'; + +// Default sequences for demo purposes +const DEFAULT_SEQUENCES: MoodSequence[] = [ + { + id: 'relaxation', + name: 'Relaxation', + items: [ + { moodId: 'breath', duration: 60 }, + { moodId: 'ocean', duration: 60 }, + { moodId: 'lavender', duration: 60 }, + ], + transitionDuration: 5, + }, + { + id: 'focus', + name: 'Focus Flow', + items: [ + { moodId: 'forest', duration: 120 }, + { moodId: 'northern-lights', duration: 120 }, + ], + transitionDuration: 10, + }, + { + id: 'party', + name: 'Party Mode', + items: [ + { moodId: 'disco', duration: 30 }, + { moodId: 'rave', duration: 30 }, + { moodId: 'police', duration: 15 }, + ], + transitionDuration: 2, + }, +]; + +// Sequences store using Svelte 5 runes +function createSequencesStore() { + let sequences = $state([...DEFAULT_SEQUENCES]); + let customSequences = $state([]); + let activeSequence = $state(null); + let currentItemIndex = $state(0); + let isPlaying = $state(false); + + // Load from localStorage on init + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('moodlit-sequences'); + if (saved) { + try { + const parsed = JSON.parse(saved); + if (parsed.customSequences) customSequences = parsed.customSequences; + } catch (e) { + console.error('Failed to load sequences from localStorage', e); + } + } + } + + // Save to localStorage + function persist() { + if (typeof window !== 'undefined') { + localStorage.setItem('moodlit-sequences', JSON.stringify({ customSequences })); + } + } + + return { + get sequences() { + return [...sequences, ...customSequences]; + }, + get customSequences() { + return customSequences; + }, + get activeSequence() { + return activeSequence; + }, + get currentItemIndex() { + return currentItemIndex; + }, + get isPlaying() { + return isPlaying; + }, + + addSequence(sequence: MoodSequence) { + customSequences = [...customSequences, { ...sequence, isCustom: true }]; + persist(); + }, + + updateSequence(id: string, updates: Partial) { + customSequences = customSequences.map((s) => (s.id === id ? { ...s, ...updates } : s)); + persist(); + }, + + removeSequence(id: string) { + customSequences = customSequences.filter((s) => s.id !== id); + persist(); + }, + + playSequence(sequence: MoodSequence) { + activeSequence = sequence; + currentItemIndex = 0; + isPlaying = true; + }, + + stopSequence() { + activeSequence = null; + currentItemIndex = 0; + isPlaying = false; + }, + + nextItem() { + if (activeSequence && currentItemIndex < activeSequence.items.length - 1) { + currentItemIndex++; + } else { + // Loop back to start + currentItemIndex = 0; + } + }, + + previousItem() { + if (currentItemIndex > 0) { + currentItemIndex--; + } + }, + + togglePlay() { + isPlaying = !isPlaying; + }, + }; +} + +export const sequencesStore = createSequencesStore(); diff --git a/apps/moodlit/apps/web/src/lib/stores/theme.ts b/apps/moodlit/apps/web/src/lib/stores/theme.ts new file mode 100644 index 000000000..64618bc3b --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/stores/theme.ts @@ -0,0 +1,8 @@ +import { createThemeStore } from '@manacore/shared-theme'; + +// Create the theme store for Moodlit +export const theme = createThemeStore({ + appId: 'moodlit', + defaultMode: 'system', + defaultVariant: 'lume', +}); diff --git a/apps/moodlit/apps/web/src/lib/types/auth.ts b/apps/moodlit/apps/web/src/lib/types/auth.ts new file mode 100644 index 000000000..2e8f4585f --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/types/auth.ts @@ -0,0 +1,9 @@ +/** + * Auth types for Moodlit + */ + +export interface MoodlitUser { + id: string; + email: string; + role: string; +} diff --git a/apps/moodlit/apps/web/src/lib/types/mood.ts b/apps/moodlit/apps/web/src/lib/types/mood.ts new file mode 100644 index 000000000..168e46222 --- /dev/null +++ b/apps/moodlit/apps/web/src/lib/types/mood.ts @@ -0,0 +1,90 @@ +// Animation types available for moods +export type AnimationType = + | 'gradient' + | 'pulse' + | 'wave' + | 'flash' + | 'sos' + | 'candle' + | 'police' + | 'warning' + | 'disco' + | 'thunder' + | 'breath' + | 'rave' + | 'scanner' + | 'matrix' + | 'sunrise' + | 'sunset' + | 'aurora' + | 'fire' + | 'ocean' + | 'forest' + | 'sparkle'; + +// Mood interface +export interface Mood { + id: string; + name: string; + colors: string[]; + animationType: AnimationType; + isCustom?: boolean; + order?: number; + createdAt?: string; +} + +// Sequence item (mood with duration) +export interface MoodSequenceItem { + moodId: string; + duration: number; // seconds +} + +// Mood sequence +export interface MoodSequence { + id: string; + name: string; + items: MoodSequenceItem[]; + transitionDuration: number; // 2, 5, or 10 seconds + isCustom?: boolean; +} + +// Settings +export interface MoodSettings { + animationSpeed: 'slow' | 'normal' | 'fast'; + brightness: number; // 0-100 + autoTimer: number; // 0 = off, else minutes + autoMoodSwitch: boolean; + autoMoodSwitchInterval: number; // minutes +} + +// Animation metadata for UI +export interface AnimationInfo { + id: AnimationType; + name: string; + description: string; +} + +// Available animations with descriptions +export const ANIMATIONS: AnimationInfo[] = [ + { id: 'gradient', name: 'Gradient', description: 'Smooth color gradient' }, + { id: 'pulse', name: 'Pulse', description: 'Breathing opacity effect' }, + { id: 'wave', name: 'Wave', description: 'Smooth wave oscillation' }, + { id: 'breath', name: 'Breath', description: '4-second breathing cycle' }, + { id: 'aurora', name: 'Aurora', description: 'Northern lights effect' }, + { id: 'fire', name: 'Fire', description: 'Warm flickering flames' }, + { id: 'candle', name: 'Candle', description: 'Soft candlelight flicker' }, + { id: 'ocean', name: 'Ocean', description: 'Calm ocean waves' }, + { id: 'forest', name: 'Forest', description: 'Peaceful forest ambience' }, + { id: 'thunder', name: 'Thunder', description: 'Random lightning flashes' }, + { id: 'sparkle', name: 'Sparkle', description: 'Twinkling star effect' }, + { id: 'sunrise', name: 'Sunrise', description: 'Slow warming colors' }, + { id: 'sunset', name: 'Sunset', description: 'Evening color transition' }, + { id: 'disco', name: 'Disco', description: 'Fast color cycling' }, + { id: 'rave', name: 'Rave', description: 'Very fast chaotic colors' }, + { id: 'scanner', name: 'Scanner', description: 'Light wave sweep' }, + { id: 'matrix', name: 'Matrix', description: 'Digital green blinking' }, + { id: 'flash', name: 'Flash', description: 'Quick white flashes' }, + { id: 'sos', name: 'SOS', description: 'Morse code pattern' }, + { id: 'police', name: 'Police', description: 'Red/blue alternating' }, + { id: 'warning', name: 'Warning', description: 'Blinking orange/yellow' }, +]; diff --git a/apps/moodlit/apps/web/src/routes/(app)/+layout.svelte b/apps/moodlit/apps/web/src/routes/(app)/+layout.svelte new file mode 100644 index 000000000..5f247981c --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/(app)/+layout.svelte @@ -0,0 +1,174 @@ + + +{#if authStore.loading} +
+
+
+

Loading...

+
+
+{:else if authStore.isAuthenticated} +
+ + + + +
+
+ {@render children()} +
+
+
+{/if} diff --git a/apps/moodlit/apps/web/src/routes/(app)/+page.svelte b/apps/moodlit/apps/web/src/routes/(app)/+page.svelte new file mode 100644 index 000000000..f6456b7be --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/(app)/+page.svelte @@ -0,0 +1,177 @@ + + +
+ +
+

{$_('home.title')}

+

{$_('home.subtitle')}

+
+ + +
+ + + +
+ + + {#if showFullscreen && fullscreenMood} + + {/if} + + +
+
+ {#each displayedMoods() as mood (mood.id)} + handleMoodClick(mood)} + onFavoriteToggle={() => handleFavoriteToggle(mood)} + /> + {/each} +
+ + {#if displayedMoods().length === 0} +
+ {#if selectedCategory === 'favorites'} +

No favorites yet. Click the heart icon on a mood to add it to favorites.

+ {:else if selectedCategory === 'custom'} +

No custom moods yet. Create your own mood to get started.

+ {:else} +

No moods available.

+ {/if} +
+ {/if} +
+ + +
+
+

{$_('home.sequences')}

+ View all +
+

{$_('home.sequencesDescription')}

+
+
+ + + + + + (showCreateDialog = false)} + onSave={handleCreateMood} +/> diff --git a/apps/moodlit/apps/web/src/routes/(app)/feedback/+page.svelte b/apps/moodlit/apps/web/src/routes/(app)/feedback/+page.svelte new file mode 100644 index 000000000..7e32f88fe --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/(app)/feedback/+page.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/moodlit/apps/web/src/routes/(app)/sequences/+page.svelte b/apps/moodlit/apps/web/src/routes/(app)/sequences/+page.svelte new file mode 100644 index 000000000..2735d4e21 --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/(app)/sequences/+page.svelte @@ -0,0 +1,206 @@ + + +
+
+
+

{$_('sequences.title')}

+

{$_('sequences.subtitle')}

+
+
+ + +
+ {#each sequencesStore.sequences as sequence (sequence.id)} +
+ +
+ + +
+ + +
+
+
+ + {formatDuration(getTotalDuration(sequence))} +
+ {#if sequence.isCustom} + + {/if} +
+ +
+
+

+ {sequence.name} +

+

+ {sequence.items.length} + {$_('sequences.moods')} +

+
+ + +
+
+ + +
+ {#each sequence.items.slice(0, 5) as item} + {@const mood = getMood(item.moodId)} + {#if mood} +
+ {/if} + {/each} + {#if sequence.items.length > 5} +
+ +{sequence.items.length - 5} +
+ {/if} +
+
+ {/each} +
+ + {#if sequencesStore.sequences.length === 0} +
+
+
+ +
+

{$_('sequences.empty')}

+

{$_('sequences.emptyDescription')}

+
+
+ {/if} +
+ + +{#if showPlayer && playerMood} + moodsStore.toggleFavorite(playerMood?.id || '')} + /> +{/if} diff --git a/apps/moodlit/apps/web/src/routes/(app)/settings/+page.svelte b/apps/moodlit/apps/web/src/routes/(app)/settings/+page.svelte new file mode 100644 index 000000000..bf339390d --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/(app)/settings/+page.svelte @@ -0,0 +1,150 @@ + + +
+
+

{$_('settings.title')}

+
+ + +
+

{$_('settings.brightness')}

+
+ + + {moodsStore.settings.brightness}% + +
+
+ + +
+

{$_('settings.animationSpeed')}

+
+ {#each speedOptions as option} + + {/each} +
+
+ + +
+

{$_('settings.autoTimer')}

+
+ {#each autoTimerOptions as option} + + {/each} +
+
+ + +
+

Theme

+
+ + + +
+
+ + +
+ +
+
diff --git a/apps/moodlit/apps/web/src/routes/(auth)/forgot-password/+page.svelte b/apps/moodlit/apps/web/src/routes/(auth)/forgot-password/+page.svelte new file mode 100644 index 000000000..a107bba4f --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/(auth)/forgot-password/+page.svelte @@ -0,0 +1,36 @@ + + + + {#snippet headerControls()} + + {/snippet} + {#snippet appSlider()} + + {/snippet} + diff --git a/apps/moodlit/apps/web/src/routes/(auth)/login/+page.svelte b/apps/moodlit/apps/web/src/routes/(auth)/login/+page.svelte new file mode 100644 index 000000000..da211d5db --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/(auth)/login/+page.svelte @@ -0,0 +1,40 @@ + + + + {#snippet headerControls()} + + {/snippet} + {#snippet appSlider()} + + {/snippet} + diff --git a/apps/moodlit/apps/web/src/routes/(auth)/register/+page.svelte b/apps/moodlit/apps/web/src/routes/(auth)/register/+page.svelte new file mode 100644 index 000000000..f09128db8 --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/(auth)/register/+page.svelte @@ -0,0 +1,37 @@ + + + + {#snippet headerControls()} + + {/snippet} + {#snippet appSlider()} + + {/snippet} + diff --git a/apps/moodlit/apps/web/src/routes/+layout.svelte b/apps/moodlit/apps/web/src/routes/+layout.svelte new file mode 100644 index 000000000..700961f32 --- /dev/null +++ b/apps/moodlit/apps/web/src/routes/+layout.svelte @@ -0,0 +1,10 @@ + + +
+ {@render children()} +
diff --git a/apps/moodlit/apps/web/svelte.config.js b/apps/moodlit/apps/web/svelte.config.js new file mode 100644 index 000000000..c8b303bb6 --- /dev/null +++ b/apps/moodlit/apps/web/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + }, +}; + +export default config; diff --git a/apps/moodlit/apps/web/tsconfig.json b/apps/moodlit/apps/web/tsconfig.json new file mode 100644 index 000000000..a8f10c8e3 --- /dev/null +++ b/apps/moodlit/apps/web/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/apps/moodlit/apps/web/vite.config.ts b/apps/moodlit/apps/web/vite.config.ts new file mode 100644 index 000000000..3bbe8eff1 --- /dev/null +++ b/apps/moodlit/apps/web/vite.config.ts @@ -0,0 +1,43 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()], + server: { + port: 5182, + strictPort: true, + }, + ssr: { + noExternal: [ + '@manacore/shared-icons', + '@manacore/shared-ui', + '@manacore/shared-tailwind', + '@manacore/shared-theme', + '@manacore/shared-theme-ui', + '@manacore/shared-feedback-ui', + '@manacore/shared-feedback-service', + '@manacore/shared-feedback-types', + '@manacore/shared-auth', + '@manacore/shared-auth-ui', + '@manacore/shared-branding', + '@manacore/shared-subscription-ui', + ], + }, + optimizeDeps: { + exclude: [ + '@manacore/shared-icons', + '@manacore/shared-ui', + '@manacore/shared-tailwind', + '@manacore/shared-theme', + '@manacore/shared-theme-ui', + '@manacore/shared-feedback-ui', + '@manacore/shared-feedback-service', + '@manacore/shared-feedback-types', + '@manacore/shared-auth', + '@manacore/shared-auth-ui', + '@manacore/shared-branding', + '@manacore/shared-subscription-ui', + ], + }, +}); diff --git a/packages/shared-branding/src/config.ts b/packages/shared-branding/src/config.ts index 6097f2c5d..522320d38 100644 --- a/packages/shared-branding/src/config.ts +++ b/packages/shared-branding/src/config.ts @@ -207,6 +207,19 @@ export const APP_BRANDING: Record = { logoStroke: true, logoStrokeWidth: 1.5, }, + moodlit: { + id: 'moodlit', + name: 'Moodlit', + tagline: 'Ambient Lighting', + primaryColor: '#8b5cf6', + secondaryColor: '#a78bfa', + // Lightbulb/ambient light icon + logoPath: + 'M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18', + logoViewBox: '0 0 24 24', + logoStroke: true, + logoStrokeWidth: 1.5, + }, }; /** diff --git a/packages/shared-branding/src/index.ts b/packages/shared-branding/src/index.ts index 3edc9bfc4..d5f3e3c69 100644 --- a/packages/shared-branding/src/index.ts +++ b/packages/shared-branding/src/index.ts @@ -29,6 +29,7 @@ export { StorageLogo, TodoLogo, MailLogo, + MoodlitLogo, } from './logos'; // Configuration diff --git a/packages/shared-branding/src/logos/MoodlitLogo.svelte b/packages/shared-branding/src/logos/MoodlitLogo.svelte new file mode 100644 index 000000000..7f4635b38 --- /dev/null +++ b/packages/shared-branding/src/logos/MoodlitLogo.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/shared-branding/src/logos/index.ts b/packages/shared-branding/src/logos/index.ts index 74f772272..b6a1d361a 100644 --- a/packages/shared-branding/src/logos/index.ts +++ b/packages/shared-branding/src/logos/index.ts @@ -16,3 +16,4 @@ export { default as CalendarLogo } from './CalendarLogo.svelte'; export { default as StorageLogo } from './StorageLogo.svelte'; export { default as TodoLogo } from './TodoLogo.svelte'; export { default as MailLogo } from './MailLogo.svelte'; +export { default as MoodlitLogo } from './MoodlitLogo.svelte'; diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index e804bd3f7..20c5d9c5f 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -276,6 +276,22 @@ export const MANA_APPS: ManaApp[] = [ comingSoon: false, status: 'development', }, + { + id: 'moodlit', + name: 'Moodlit', + description: { + de: 'Ambient Lighting & Moods', + en: 'Ambient Lighting & Moods', + }, + longDescription: { + de: 'Erstelle beruhigende Lichtstimmungen mit animierten Farbverläufen für entspannte Atmosphäre.', + en: 'Create calming ambient lighting with animated color gradients for a relaxed atmosphere.', + }, + icon: APP_ICONS.moodlit, + color: '#8b5cf6', + comingSoon: false, + status: 'development', + }, ]; /** @@ -355,7 +371,7 @@ export const APP_URLS: Record = { nutriphi: { dev: 'http://localhost:5182', prod: 'https://nutriphi.manacore.app' }, manacore: { dev: 'http://localhost:5173', prod: 'https://manacore.app' }, mana: { dev: 'http://localhost:5173', prod: 'https://manacore.app' }, - moodlit: { dev: 'http://localhost:5183', prod: 'https://moodlit.manacore.app' }, + moodlit: { dev: 'http://localhost:5182', prod: 'https://moodlit.manacore.app' }, contacts: { dev: 'http://localhost:5184', prod: 'https://contacts.manacore.app' }, calendar: { dev: 'http://localhost:5179', prod: 'https://calendar.manacore.app' }, storage: { dev: 'http://localhost:5185', prod: 'https://storage.manacore.app' }, diff --git a/packages/shared-branding/src/types.ts b/packages/shared-branding/src/types.ts index 649ab6997..8dedf3b10 100644 --- a/packages/shared-branding/src/types.ts +++ b/packages/shared-branding/src/types.ts @@ -17,7 +17,8 @@ export type AppId = | 'storage' | 'clock' | 'todo' - | 'mail'; + | 'mail' + | 'moodlit'; /** * App branding configuration