mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-27 18:52:54 +02:00
chore: archive finance, mail, moodlit apps and rename voxel-lava
- Move finance, mail, moodlit to apps-archived for later development - Rename games/voxel-lava to games/voxelava 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c3c272abc9
commit
ace7fa8f7f
427 changed files with 0 additions and 0 deletions
153
apps-archived/moodlit/apps/web/src/app.css
Normal file
153
apps-archived/moodlit/apps/web/src/app.css
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
@import "tailwindcss";
|
||||
@import "@manacore/shared-tailwind/themes.css";
|
||||
|
||||
/* Scan shared packages for Tailwind classes */
|
||||
@source "../../../../../packages/shared-ui/src";
|
||||
@source "../../../../../packages/shared-theme-ui/src";
|
||||
|
||||
/* Moods-specific CSS Variables */
|
||||
@layer base {
|
||||
:root {
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
--transition-slow: 300ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mood Card Styles */
|
||||
.mood-card {
|
||||
background-color: hsl(var(--color-surface));
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
transition: all var(--transition-base);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mood-card:hover {
|
||||
border-color: hsl(var(--color-primary) / 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Color Preview */
|
||||
.color-preview {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Animated Background */
|
||||
.animated-background {
|
||||
background-size: 400% 400%;
|
||||
animation: gradient-shift 8s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Color Picker */
|
||||
.color-picker-swatch {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px solid hsl(var(--color-border));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.color-picker-swatch:hover {
|
||||
border-color: hsl(var(--color-primary));
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card {
|
||||
background-color: hsl(var(--color-surface));
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
transition: all var(--transition-base);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: hsl(var(--color-primary) / 0.9);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: hsl(var(--color-secondary));
|
||||
color: hsl(var(--color-secondary-foreground));
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: hsl(var(--color-secondary) / 0.8);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
|
||||
/* Input styles */
|
||||
.input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 2px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
background-color: hsl(var(--color-background));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.875rem;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
13
apps-archived/moodlit/apps/web/src/app.html
Normal file
13
apps-archived/moodlit/apps/web/src/app.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Moodlit</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
14
apps-archived/moodlit/apps/web/src/lib/api/feedback.ts
Normal file
14
apps-archived/moodlit/apps/web/src/lib/api/feedback.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Feedback Service Instance for Moodlit Web App
|
||||
*/
|
||||
|
||||
import { createFeedbackService } from '@manacore/shared-feedback-service';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
|
||||
export const feedbackService = createFeedbackService({
|
||||
apiUrl: MANA_AUTH_URL,
|
||||
appId: 'moodlit',
|
||||
getAuthToken: async () => authStore.getAccessToken(),
|
||||
});
|
||||
172
apps-archived/moodlit/apps/web/src/lib/auth.ts
Normal file
172
apps-archived/moodlit/apps/web/src/lib/auth.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* Moodlit Web Auth Configuration
|
||||
*
|
||||
* This file initializes the shared auth package for the moodlit web app.
|
||||
*/
|
||||
|
||||
import { PUBLIC_MANA_CORE_AUTH_URL, PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
import {
|
||||
createAuthService,
|
||||
createTokenManager,
|
||||
setStorageAdapter,
|
||||
setDeviceAdapter,
|
||||
setNetworkAdapter,
|
||||
setupFetchInterceptor,
|
||||
type StorageAdapter,
|
||||
type DeviceManagerAdapter,
|
||||
type NetworkAdapter,
|
||||
type DeviceInfo,
|
||||
} from '@manacore/shared-auth';
|
||||
|
||||
// Storage keys
|
||||
const STORAGE_KEYS = {
|
||||
APP_TOKEN: 'moodlit_appToken',
|
||||
REFRESH_TOKEN: 'moodlit_refreshToken',
|
||||
USER_EMAIL: 'moodlit_userEmail',
|
||||
DEVICE_ID: 'moodlit_device_id',
|
||||
};
|
||||
|
||||
/**
|
||||
* Session storage adapter for moodlit web
|
||||
* Uses sessionStorage for tokens (clears on tab close)
|
||||
* Uses localStorage for device ID (persists)
|
||||
*/
|
||||
const sessionStorageAdapter: StorageAdapter = {
|
||||
async getItem<T = string>(key: string): Promise<T | null> {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
const value = sessionStorage.getItem(key);
|
||||
if (value === null) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return value as T;
|
||||
}
|
||||
},
|
||||
|
||||
async setItem(key: string, value: string): Promise<void> {
|
||||
if (typeof window === 'undefined') return;
|
||||
sessionStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
|
||||
},
|
||||
|
||||
async removeItem(key: string): Promise<void> {
|
||||
if (typeof window === 'undefined') return;
|
||||
sessionStorage.removeItem(key);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Device manager adapter for web
|
||||
*/
|
||||
const webDeviceAdapter: DeviceManagerAdapter = {
|
||||
async getDeviceInfo(): Promise<DeviceInfo> {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
deviceId: '',
|
||||
deviceName: 'Server',
|
||||
deviceType: 'web',
|
||||
};
|
||||
}
|
||||
|
||||
const deviceId = (await webDeviceAdapter.getStoredDeviceId()) || generateDeviceId();
|
||||
localStorage.setItem(STORAGE_KEYS.DEVICE_ID, deviceId);
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
let deviceName = 'Web Browser';
|
||||
|
||||
if (userAgent.includes('Mac')) deviceName = 'Mac';
|
||||
else if (userAgent.includes('Windows')) deviceName = 'Windows';
|
||||
else if (userAgent.includes('Linux')) deviceName = 'Linux';
|
||||
|
||||
return {
|
||||
deviceId,
|
||||
deviceName,
|
||||
deviceType: 'web',
|
||||
platform: 'web',
|
||||
};
|
||||
},
|
||||
|
||||
async getStoredDeviceId(): Promise<string | null> {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem(STORAGE_KEYS.DEVICE_ID);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Network adapter for web
|
||||
*/
|
||||
const webNetworkAdapter: NetworkAdapter = {
|
||||
async isDeviceConnected(): Promise<boolean> {
|
||||
if (typeof navigator === 'undefined') return true;
|
||||
return navigator.onLine;
|
||||
},
|
||||
|
||||
async hasStableConnection(): Promise<boolean> {
|
||||
if (typeof navigator === 'undefined') return true;
|
||||
return navigator.onLine;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a unique device ID
|
||||
*/
|
||||
function generateDeviceId(): string {
|
||||
return `moodlit_web_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
}
|
||||
|
||||
// Initialize adapters
|
||||
setStorageAdapter(sessionStorageAdapter);
|
||||
setDeviceAdapter(webDeviceAdapter);
|
||||
setNetworkAdapter(webNetworkAdapter);
|
||||
|
||||
// Create auth service instance
|
||||
export const authService = createAuthService({
|
||||
baseUrl: PUBLIC_MANA_CORE_AUTH_URL,
|
||||
storageKeys: {
|
||||
APP_TOKEN: STORAGE_KEYS.APP_TOKEN,
|
||||
REFRESH_TOKEN: STORAGE_KEYS.REFRESH_TOKEN,
|
||||
USER_EMAIL: STORAGE_KEYS.USER_EMAIL,
|
||||
},
|
||||
endpoints: {
|
||||
signIn: '/api/v1/auth/login',
|
||||
signUp: '/api/v1/auth/register',
|
||||
signOut: '/api/v1/auth/logout',
|
||||
refresh: '/api/v1/auth/refresh',
|
||||
validate: '/api/v1/auth/validate',
|
||||
forgotPassword: '/api/v1/auth/forgot-password',
|
||||
googleSignIn: '/api/v1/auth/google-signin',
|
||||
appleSignIn: '/api/v1/auth/apple-signin',
|
||||
credits: '/api/v1/credits/balance',
|
||||
},
|
||||
});
|
||||
|
||||
// Create token manager instance
|
||||
export const tokenManager = createTokenManager(authService);
|
||||
|
||||
// Setup fetch interceptor (only in browser)
|
||||
if (typeof window !== 'undefined') {
|
||||
setupFetchInterceptor(authService, tokenManager, {
|
||||
backendUrl: PUBLIC_BACKEND_URL,
|
||||
});
|
||||
}
|
||||
|
||||
// Re-export useful utilities from shared-auth
|
||||
export {
|
||||
decodeToken,
|
||||
isTokenValidLocally,
|
||||
isTokenExpired,
|
||||
getUserFromToken,
|
||||
isB2BUser,
|
||||
getB2BInfo,
|
||||
TokenState,
|
||||
} from '@manacore/shared-auth';
|
||||
|
||||
// Re-export types
|
||||
export type {
|
||||
UserData,
|
||||
DecodedToken,
|
||||
AuthResult,
|
||||
CreditBalance,
|
||||
B2BInfo,
|
||||
} from '@manacore/shared-auth';
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { AppSlider, type AppItem } from '@manacore/shared-ui';
|
||||
import {
|
||||
MANA_APPS,
|
||||
APP_STATUS_LABELS,
|
||||
APP_SLIDER_LABELS,
|
||||
getActiveManaApps,
|
||||
} from '@manacore/shared-branding';
|
||||
|
||||
// Get current language
|
||||
let currentLocale = $derived(($locale || 'de') as 'de' | 'en');
|
||||
|
||||
// Convert MANA_APPS to AppItem format (based on current locale)
|
||||
let apps = $derived<AppItem[]>(
|
||||
getActiveManaApps().map((app) => ({
|
||||
name: app.name,
|
||||
description: app.description[currentLocale],
|
||||
longDescription: app.longDescription[currentLocale],
|
||||
icon: app.icon,
|
||||
color: app.color,
|
||||
comingSoon: app.comingSoon,
|
||||
status: app.status,
|
||||
}))
|
||||
);
|
||||
|
||||
let statusLabels = $derived(APP_STATUS_LABELS[currentLocale]);
|
||||
let labels = $derived(APP_SLIDER_LABELS[currentLocale]);
|
||||
|
||||
function handleAppClick(app: AppItem, index: number) {
|
||||
console.log('Opening app:', app.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppSlider
|
||||
{apps}
|
||||
title={labels.title}
|
||||
isDark={false}
|
||||
{statusLabels}
|
||||
comingSoonLabel={labels.comingSoon}
|
||||
openAppLabel={labels.openApp}
|
||||
onAppClick={handleAppClick}
|
||||
/>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillDropdown } from '@manacore/shared-ui';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as any);
|
||||
}
|
||||
|
||||
let languageItems = $derived(
|
||||
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
|
||||
);
|
||||
let currentLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
</script>
|
||||
|
||||
<PillDropdown items={languageItems} label={currentLabel} direction="down" />
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { X, Plus, Trash } from '@manacore/shared-icons';
|
||||
import type { Mood, AnimationType } from '$lib/types/mood';
|
||||
import { ANIMATIONS } from '$lib/types/mood';
|
||||
import { getMoodGradient } from '$lib/data/default-moods';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (mood: Omit<Mood, 'id' | 'isCustom' | 'order' | 'createdAt'>) => void;
|
||||
editMood?: Mood | null;
|
||||
}
|
||||
|
||||
let { isOpen, onClose, onSave, editMood = null }: Props = $props();
|
||||
|
||||
let name = $state('');
|
||||
let colors = $state<string[]>(['#667eea', '#764ba2']);
|
||||
let animationType = $state<AnimationType>('gradient');
|
||||
|
||||
// Preview mood
|
||||
let previewMood = $derived<Mood>({
|
||||
id: 'preview',
|
||||
name: name || 'Preview',
|
||||
colors,
|
||||
animationType,
|
||||
});
|
||||
|
||||
// Reset form when dialog opens/closes or when editing different mood
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
if (editMood) {
|
||||
name = editMood.name;
|
||||
colors = [...editMood.colors];
|
||||
animationType = editMood.animationType;
|
||||
} else {
|
||||
name = '';
|
||||
colors = ['#667eea', '#764ba2'];
|
||||
animationType = 'gradient';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function addColor() {
|
||||
if (colors.length < 8) {
|
||||
// Generate a random color
|
||||
const randomColor =
|
||||
'#' +
|
||||
Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0');
|
||||
colors = [...colors, randomColor];
|
||||
}
|
||||
}
|
||||
|
||||
function removeColor(index: number) {
|
||||
if (colors.length > 1) {
|
||||
colors = colors.filter((_, i) => i !== index);
|
||||
}
|
||||
}
|
||||
|
||||
function updateColor(index: number, value: string) {
|
||||
colors = colors.map((c, i) => (i === index ? value : c));
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!name.trim()) return;
|
||||
if (colors.length === 0) return;
|
||||
|
||||
onSave({
|
||||
name: name.trim(),
|
||||
colors,
|
||||
animationType,
|
||||
});
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"
|
||||
onclick={onClose}
|
||||
role="presentation"
|
||||
></div>
|
||||
|
||||
<!-- Dialog -->
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
|
||||
<div
|
||||
class="bg-[hsl(var(--color-background))] rounded-2xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto pointer-events-auto"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-border">
|
||||
<h2 class="text-xl font-semibold">
|
||||
{editMood ? $_('createMood.editTitle') : $_('createMood.title')}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-lg hover:bg-muted transition-colors"
|
||||
onclick={onClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-4 space-y-6">
|
||||
<!-- Preview -->
|
||||
<div class="relative rounded-xl overflow-hidden aspect-video">
|
||||
<div class="absolute inset-0" style="background: {getMoodGradient(previewMood)};"></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent"
|
||||
></div>
|
||||
<div class="absolute inset-x-0 bottom-0 p-4">
|
||||
<h3 class="text-lg font-semibold text-white drop-shadow-md">
|
||||
{previewMood.name}
|
||||
</h3>
|
||||
<p class="text-sm text-white/70 capitalize">{previewMood.animationType}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name Input -->
|
||||
<div class="space-y-2">
|
||||
<label for="mood-name" class="text-sm font-medium">
|
||||
{$_('createMood.name')}
|
||||
</label>
|
||||
<input
|
||||
id="mood-name"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$_('createMood.namePlaceholder')}
|
||||
class="w-full px-4 py-2 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Colors -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm font-medium">{$_('createMood.colors')}</label>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 px-2 py-1 text-sm rounded-lg hover:bg-muted transition-colors"
|
||||
onclick={addColor}
|
||||
disabled={colors.length >= 8}
|
||||
>
|
||||
<Plus size={16} />
|
||||
{$_('createMood.addColor')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each colors as color, i}
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
type="color"
|
||||
value={color}
|
||||
onchange={(e) => updateColor(i, e.currentTarget.value)}
|
||||
class="w-10 h-10 rounded-lg border border-border cursor-pointer"
|
||||
/>
|
||||
{#if colors.length > 1}
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 rounded hover:bg-red-500/20 text-red-500 transition-colors"
|
||||
onclick={() => removeColor(i)}
|
||||
aria-label="Remove color"
|
||||
>
|
||||
<Trash size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Animation Type -->
|
||||
<div class="space-y-2">
|
||||
<label for="animation-type" class="text-sm font-medium">
|
||||
{$_('createMood.animation')}
|
||||
</label>
|
||||
<select
|
||||
id="animation-type"
|
||||
bind:value={animationType}
|
||||
class="w-full px-4 py-2 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>
|
||||
{#each ANIMATIONS as anim}
|
||||
<option value={anim.id}>{anim.name} - {anim.description}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 p-4 border-t border-border">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 rounded-lg hover:bg-muted transition-colors"
|
||||
onclick={onClose}
|
||||
>
|
||||
{$_('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick={handleSubmit}
|
||||
disabled={!name.trim() || colors.length === 0}
|
||||
>
|
||||
{$_('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
<script lang="ts">
|
||||
import type { Mood } from '$lib/types/mood';
|
||||
import { getMoodGradient } from '$lib/data/default-moods';
|
||||
import { Heart } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
mood: Mood;
|
||||
isActive?: boolean;
|
||||
isFavorite?: boolean;
|
||||
showFavorite?: boolean;
|
||||
onClick?: () => void;
|
||||
onFavoriteToggle?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
mood,
|
||||
isActive = false,
|
||||
isFavorite = false,
|
||||
showFavorite = true,
|
||||
onClick,
|
||||
onFavoriteToggle,
|
||||
}: Props = $props();
|
||||
|
||||
const gradient = $derived(getMoodGradient(mood));
|
||||
const animationClass = $derived(getAnimationClass(mood.animationType));
|
||||
|
||||
function getAnimationClass(type: string): string {
|
||||
switch (type) {
|
||||
case 'pulse':
|
||||
case 'breath':
|
||||
return 'animate-pulse-slow';
|
||||
case 'wave':
|
||||
return 'animate-wave';
|
||||
case 'candle':
|
||||
return 'animate-candle';
|
||||
case 'disco':
|
||||
case 'rave':
|
||||
return 'animate-disco';
|
||||
case 'thunder':
|
||||
return 'animate-thunder';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleFavoriteClick(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
onFavoriteToggle?.();
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
onClick?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="mood-card group relative w-full overflow-hidden rounded-2xl transition-all duration-200 hover:scale-[1.02] hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
class:ring-2={isActive}
|
||||
class:ring-primary={isActive}
|
||||
onclick={handleClick}
|
||||
>
|
||||
<!-- Gradient Background -->
|
||||
<div class="aspect-[16/10] w-full {animationClass}" style="background: {gradient};"></div>
|
||||
|
||||
<!-- Overlay gradient for text readability -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="absolute inset-x-0 bottom-0 p-4">
|
||||
<div class="flex items-end justify-between">
|
||||
<div class="text-left">
|
||||
<h3 class="font-semibold text-white drop-shadow-md">{mood.name}</h3>
|
||||
<p class="text-xs text-white/70 capitalize">{mood.animationType}</p>
|
||||
</div>
|
||||
|
||||
{#if showFavorite}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full p-1.5 transition-colors hover:bg-white/20"
|
||||
onclick={handleFavoriteClick}
|
||||
aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
<Heart
|
||||
size={20}
|
||||
weight={isFavorite ? 'fill' : 'regular'}
|
||||
class={isFavorite ? 'text-red-500' : 'text-white/70'}
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom badge -->
|
||||
{#if mood.isCustom}
|
||||
<div class="absolute right-2 top-2">
|
||||
<span class="rounded-full bg-primary/80 px-2 py-0.5 text-xs font-medium text-white">
|
||||
Custom
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
@keyframes pulse-slow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes candle {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: brightness(1);
|
||||
}
|
||||
25% {
|
||||
opacity: 0.9;
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.95;
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes disco {
|
||||
0%,
|
||||
100% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
filter: hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes thunder {
|
||||
0%,
|
||||
95%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
97% {
|
||||
opacity: 1;
|
||||
filter: brightness(3);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse-slow 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-wave {
|
||||
animation: wave 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-candle {
|
||||
animation: candle 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-disco {
|
||||
animation: disco 2s linear infinite;
|
||||
}
|
||||
|
||||
.animate-thunder {
|
||||
animation: thunder 5s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,589 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { Mood } from '$lib/types/mood';
|
||||
import { getMoodGradient } from '$lib/data/default-moods';
|
||||
import { X, Pause, Play, Heart, Timer } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
mood: Mood;
|
||||
isFavorite?: boolean;
|
||||
onClose: () => void;
|
||||
onFavoriteToggle?: () => void;
|
||||
}
|
||||
|
||||
let { mood, isFavorite = false, onClose, onFavoriteToggle }: Props = $props();
|
||||
|
||||
let isPlaying = $state(true);
|
||||
let showControls = $state(true);
|
||||
let controlsTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let timerActive = $state(false);
|
||||
let timerMinutes = $state(5);
|
||||
let timerRemaining = $state(0);
|
||||
let timerInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const gradient = $derived(getMoodGradient(mood));
|
||||
const animationClass = $derived(getAnimationClass(mood.animationType));
|
||||
|
||||
function getAnimationClass(type: string): string {
|
||||
switch (type) {
|
||||
case 'pulse':
|
||||
case 'breath':
|
||||
return 'animate-breath';
|
||||
case 'wave':
|
||||
return 'animate-wave';
|
||||
case 'candle':
|
||||
case 'fire':
|
||||
return 'animate-candle';
|
||||
case 'disco':
|
||||
case 'rave':
|
||||
return 'animate-disco';
|
||||
case 'thunder':
|
||||
return 'animate-thunder';
|
||||
case 'police':
|
||||
return 'animate-police';
|
||||
case 'warning':
|
||||
return 'animate-warning';
|
||||
case 'flash':
|
||||
return 'animate-flash';
|
||||
case 'sos':
|
||||
return 'animate-sos';
|
||||
case 'scanner':
|
||||
return 'animate-scanner';
|
||||
case 'matrix':
|
||||
return 'animate-matrix';
|
||||
case 'sunrise':
|
||||
return 'animate-sunrise';
|
||||
case 'sunset':
|
||||
return 'animate-sunset';
|
||||
default:
|
||||
return 'animate-gradient';
|
||||
}
|
||||
}
|
||||
|
||||
function showControlsTemporarily() {
|
||||
showControls = true;
|
||||
if (controlsTimeout) {
|
||||
clearTimeout(controlsTimeout);
|
||||
}
|
||||
controlsTimeout = setTimeout(() => {
|
||||
if (isPlaying) {
|
||||
showControls = false;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
isPlaying = !isPlaying;
|
||||
if (isPlaying) {
|
||||
showControlsTemporarily();
|
||||
} else {
|
||||
showControls = true;
|
||||
}
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
timerActive = true;
|
||||
timerRemaining = timerMinutes * 60;
|
||||
timerInterval = setInterval(() => {
|
||||
timerRemaining--;
|
||||
if (timerRemaining <= 0) {
|
||||
stopTimer();
|
||||
onClose();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
timerActive = false;
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval);
|
||||
timerInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
} else if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
togglePlay();
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
showControlsTemporarily();
|
||||
return () => {
|
||||
if (controlsTimeout) clearTimeout(controlsTimeout);
|
||||
if (timerInterval) clearInterval(timerInterval);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center cursor-pointer select-none"
|
||||
onclick={showControlsTemporarily}
|
||||
onmousemove={showControlsTemporarily}
|
||||
role="presentation"
|
||||
>
|
||||
<!-- Animated Background -->
|
||||
<div
|
||||
class="absolute inset-0 {animationClass}"
|
||||
class:paused={!isPlaying}
|
||||
style="background: {gradient}; background-size: 400% 400%;"
|
||||
></div>
|
||||
|
||||
<!-- Particle Effects for certain animations -->
|
||||
{#if mood.animationType === 'sparkle' || mood.animationType === 'matrix'}
|
||||
<div class="particles absolute inset-0 pointer-events-none overflow-hidden">
|
||||
{#each Array(20) as _, i}
|
||||
<div
|
||||
class="particle absolute w-1 h-1 bg-white/60 rounded-full"
|
||||
style="left: {Math.random() * 100}%; animation-delay: {Math.random() *
|
||||
5}s; animation-duration: {3 + Math.random() * 2}s;"
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Controls Overlay -->
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col transition-opacity duration-300 pointer-events-none"
|
||||
class:opacity-0={!showControls}
|
||||
class:opacity-100={showControls}
|
||||
>
|
||||
<!-- Top Bar -->
|
||||
<div
|
||||
class="flex items-center justify-between p-4 bg-gradient-to-b from-black/40 to-transparent pointer-events-auto"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={24} class="text-white" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-white drop-shadow-lg">{mood.name}</h1>
|
||||
<p class="text-sm text-white/70 capitalize">{mood.animationType}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
{#if timerActive}
|
||||
<div class="px-3 py-1.5 rounded-full bg-white/20 backdrop-blur-sm text-white font-mono">
|
||||
{formatTime(timerRemaining)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFavoriteToggle?.();
|
||||
}}
|
||||
aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
<Heart
|
||||
size={20}
|
||||
weight={isFavorite ? 'fill' : 'regular'}
|
||||
class={isFavorite ? 'text-red-500' : 'text-white'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Play/Pause -->
|
||||
<div class="flex-1 flex items-center justify-center pointer-events-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="p-6 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-all hover:scale-110"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
togglePlay();
|
||||
}}
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{#if isPlaying}
|
||||
<Pause size={48} class="text-white" />
|
||||
{:else}
|
||||
<Play size={48} class="text-white" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<div class="p-4 bg-gradient-to-t from-black/40 to-transparent pointer-events-auto">
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
{#if !timerActive}
|
||||
<div class="flex items-center gap-2 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2">
|
||||
<Timer size={20} class="text-white" />
|
||||
<select
|
||||
class="bg-transparent text-white border-none outline-none cursor-pointer"
|
||||
bind:value={timerMinutes}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<option value={1}>1 min</option>
|
||||
<option value={5}>5 min</option>
|
||||
<option value={10}>10 min</option>
|
||||
<option value={15}>15 min</option>
|
||||
<option value={30}>30 min</option>
|
||||
<option value={60}>60 min</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1 bg-white/20 hover:bg-white/30 rounded-full text-sm text-white transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
startTimer();
|
||||
}}
|
||||
>
|
||||
{$_('mood.startTimer')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-white/20 hover:bg-white/30 backdrop-blur-sm rounded-full text-white transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
stopTimer();
|
||||
}}
|
||||
>
|
||||
{$_('mood.stopTimer')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Base animation styles */
|
||||
.animate-gradient {
|
||||
animation: gradient-shift 8s ease infinite;
|
||||
}
|
||||
|
||||
.animate-breath {
|
||||
animation: breath 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-wave {
|
||||
animation: wave 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-candle {
|
||||
animation: candle 0.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-disco {
|
||||
animation: disco 0.5s linear infinite;
|
||||
}
|
||||
|
||||
.animate-thunder {
|
||||
animation: thunder 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-police {
|
||||
animation: police 0.5s linear infinite;
|
||||
}
|
||||
|
||||
.animate-warning {
|
||||
animation: warning 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-flash {
|
||||
animation: flash 0.2s linear infinite;
|
||||
}
|
||||
|
||||
.animate-sos {
|
||||
animation: sos 2.5s linear infinite;
|
||||
}
|
||||
|
||||
.animate-scanner {
|
||||
animation: scanner 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-matrix {
|
||||
animation: matrix 0.1s steps(2) infinite;
|
||||
}
|
||||
|
||||
.animate-sunrise {
|
||||
animation: sunrise 30s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-sunset {
|
||||
animation: sunset 30s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.paused {
|
||||
animation-play-state: paused !important;
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes breath {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes candle {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: brightness(1);
|
||||
}
|
||||
25% {
|
||||
opacity: 0.9;
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.95;
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes disco {
|
||||
0% {
|
||||
filter: hue-rotate(0deg) saturate(1.2);
|
||||
}
|
||||
100% {
|
||||
filter: hue-rotate(360deg) saturate(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes thunder {
|
||||
0%,
|
||||
94%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: brightness(1);
|
||||
}
|
||||
95%,
|
||||
97% {
|
||||
opacity: 1;
|
||||
filter: brightness(3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes police {
|
||||
0%,
|
||||
49% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
filter: hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes warning {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0%,
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
51%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sos {
|
||||
/* S: ... */
|
||||
0%,
|
||||
5% {
|
||||
opacity: 1;
|
||||
}
|
||||
5.1%,
|
||||
10% {
|
||||
opacity: 0;
|
||||
}
|
||||
10.1%,
|
||||
15% {
|
||||
opacity: 1;
|
||||
}
|
||||
15.1%,
|
||||
20% {
|
||||
opacity: 0;
|
||||
}
|
||||
20.1%,
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
25.1%,
|
||||
35% {
|
||||
opacity: 0;
|
||||
}
|
||||
/* O: --- */
|
||||
35.1%,
|
||||
45% {
|
||||
opacity: 1;
|
||||
}
|
||||
45.1%,
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
50.1%,
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
60.1%,
|
||||
65% {
|
||||
opacity: 0;
|
||||
}
|
||||
65.1%,
|
||||
75% {
|
||||
opacity: 1;
|
||||
}
|
||||
75.1%,
|
||||
80% {
|
||||
opacity: 0;
|
||||
}
|
||||
/* S: ... */
|
||||
80.1%,
|
||||
82% {
|
||||
opacity: 1;
|
||||
}
|
||||
82.1%,
|
||||
85% {
|
||||
opacity: 0;
|
||||
}
|
||||
85.1%,
|
||||
87% {
|
||||
opacity: 1;
|
||||
}
|
||||
87.1%,
|
||||
90% {
|
||||
opacity: 0;
|
||||
}
|
||||
90.1%,
|
||||
92% {
|
||||
opacity: 1;
|
||||
}
|
||||
92.1%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scanner {
|
||||
0%,
|
||||
100% {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes matrix {
|
||||
0% {
|
||||
filter: brightness(1) contrast(1.1);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(0.8) contrast(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sunrise {
|
||||
0% {
|
||||
filter: brightness(0.3) saturate(0.5);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(1) saturate(1);
|
||||
}
|
||||
100% {
|
||||
filter: brightness(1.2) saturate(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sunset {
|
||||
0% {
|
||||
filter: brightness(1.2) saturate(1.2);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(0.8) saturate(1.5);
|
||||
}
|
||||
100% {
|
||||
filter: brightness(0.3) saturate(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Particle animation */
|
||||
.particle {
|
||||
animation: float-up linear infinite;
|
||||
}
|
||||
|
||||
@keyframes float-up {
|
||||
0% {
|
||||
transform: translateY(100vh) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-10vh) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
195
apps-archived/moodlit/apps/web/src/lib/data/default-moods.ts
Normal file
195
apps-archived/moodlit/apps/web/src/lib/data/default-moods.ts
Normal file
|
|
@ -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(', ')})`;
|
||||
}
|
||||
49
apps-archived/moodlit/apps/web/src/lib/i18n/index.ts
Normal file
49
apps-archived/moodlit/apps/web/src/lib/i18n/index.ts
Normal file
|
|
@ -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 };
|
||||
78
apps-archived/moodlit/apps/web/src/lib/i18n/locales/de.json
Normal file
78
apps-archived/moodlit/apps/web/src/lib/i18n/locales/de.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
78
apps-archived/moodlit/apps/web/src/lib/i18n/locales/en.json
Normal file
78
apps-archived/moodlit/apps/web/src/lib/i18n/locales/en.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
118
apps-archived/moodlit/apps/web/src/lib/stores/auth.svelte.ts
Normal file
118
apps-archived/moodlit/apps/web/src/lib/stores/auth.svelte.ts
Normal file
|
|
@ -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<MoodlitUser | null>(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<string | null> {
|
||||
return authService.getAppToken();
|
||||
},
|
||||
};
|
||||
116
apps-archived/moodlit/apps/web/src/lib/stores/moods.svelte.ts
Normal file
116
apps-archived/moodlit/apps/web/src/lib/stores/moods.svelte.ts
Normal file
|
|
@ -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<Mood[]>([]);
|
||||
let favoriteIds = $state<string[]>([]);
|
||||
let settings = $state<MoodSettings>({ ...DEFAULT_SETTINGS });
|
||||
let activeMood = $state<Mood | null>(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<Mood>) {
|
||||
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<MoodSettings>) {
|
||||
settings = { ...settings, ...updates };
|
||||
persist();
|
||||
},
|
||||
|
||||
resetToDefaults() {
|
||||
customMoods = [];
|
||||
favoriteIds = [];
|
||||
settings = { ...DEFAULT_SETTINGS };
|
||||
persist();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const moodsStore = createMoodsStore();
|
||||
|
|
@ -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);
|
||||
|
|
@ -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<MoodSequence[]>([...DEFAULT_SEQUENCES]);
|
||||
let customSequences = $state<MoodSequence[]>([]);
|
||||
let activeSequence = $state<MoodSequence | null>(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<MoodSequence>) {
|
||||
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();
|
||||
8
apps-archived/moodlit/apps/web/src/lib/stores/theme.ts
Normal file
8
apps-archived/moodlit/apps/web/src/lib/stores/theme.ts
Normal file
|
|
@ -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',
|
||||
});
|
||||
9
apps-archived/moodlit/apps/web/src/lib/types/auth.ts
Normal file
9
apps-archived/moodlit/apps/web/src/lib/types/auth.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Auth types for Moodlit
|
||||
*/
|
||||
|
||||
export interface MoodlitUser {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
90
apps-archived/moodlit/apps/web/src/lib/types/mood.ts
Normal file
90
apps-archived/moodlit/apps/web/src/lib/types/mood.ts
Normal file
|
|
@ -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' },
|
||||
];
|
||||
174
apps-archived/moodlit/apps/web/src/routes/(app)/+layout.svelte
Normal file
174
apps-archived/moodlit/apps/web/src/routes/(app)/+layout.svelte
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale, _ } from 'svelte-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import {
|
||||
isSidebarMode as sidebarModeStore,
|
||||
isNavCollapsed as collapsedStore,
|
||||
} from '$lib/stores/navigation';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('moodlit');
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Get theme state
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
// Navigation items for Moodlit
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Moods', icon: 'palette' },
|
||||
{ href: '/sequences', label: 'Sequences', icon: 'layers' },
|
||||
{ href: '/settings', label: 'Settings', icon: 'settings' },
|
||||
];
|
||||
|
||||
// Theme variant dropdown items
|
||||
let themeVariantItems = $derived<PillDropdownItem[]>([
|
||||
...theme.variants.map((variant) => ({
|
||||
id: variant,
|
||||
label: THEME_DEFINITIONS[variant]?.label || variant,
|
||||
icon: THEME_DEFINITIONS[variant]?.icon || 'circle',
|
||||
onClick: () => theme.setVariant(variant),
|
||||
active: theme.variant === variant,
|
||||
})),
|
||||
]);
|
||||
|
||||
// Current theme variant label
|
||||
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant]?.label || theme.variant);
|
||||
|
||||
// Language selector items
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as any);
|
||||
}
|
||||
let languageItems = $derived(
|
||||
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
|
||||
);
|
||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
|
||||
// User email for user dropdown
|
||||
let userEmail = $derived(authStore.user?.email);
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
sidebarModeStore.set(isSidebar);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('moodlit-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
collapsedStore.set(collapsed);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('moodlit-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
|
||||
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
|
||||
theme.setMode(mode);
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('moodlit-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
sidebarModeStore.set(true);
|
||||
}
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('moodlit-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if authStore.loading}
|
||||
<div class="min-h-screen flex items-center justify-center bg-background">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="inline-block animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"
|
||||
></div>
|
||||
<p class="mt-4 text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if authStore.isAuthenticated}
|
||||
<div class="min-h-screen bg-background">
|
||||
<!-- Pill Navigation -->
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Moodlit"
|
||||
homeRoute="/"
|
||||
onLogout={handleSignOut}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={true}
|
||||
primaryColor="#8b5cf6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<!-- Main content with dynamic padding -->
|
||||
<main
|
||||
class="transition-all duration-300 {isCollapsed
|
||||
? ''
|
||||
: isSidebarMode
|
||||
? 'pl-[180px]'
|
||||
: 'pt-20'}"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
177
apps-archived/moodlit/apps/web/src/routes/(app)/+page.svelte
Normal file
177
apps-archived/moodlit/apps/web/src/routes/(app)/+page.svelte
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { moodsStore } from '$lib/stores/moods.svelte';
|
||||
import { DEFAULT_MOODS, getMoodGradient } from '$lib/data/default-moods';
|
||||
import MoodCard from '$lib/components/mood/MoodCard.svelte';
|
||||
import MoodFullscreen from '$lib/components/mood/MoodFullscreen.svelte';
|
||||
import CreateMoodDialog from '$lib/components/mood/CreateMoodDialog.svelte';
|
||||
import { Plus } from '@manacore/shared-icons';
|
||||
import type { Mood, AnimationType } from '$lib/types/mood';
|
||||
|
||||
// Combine default moods with custom moods
|
||||
let allMoods = $derived([...DEFAULT_MOODS, ...moodsStore.customMoods]);
|
||||
|
||||
// Get favorites (moods that are in the favorites list)
|
||||
let favoriteMoods = $derived(allMoods.filter((m) => moodsStore.isFavorite(m.id)));
|
||||
|
||||
// Filter by category
|
||||
let selectedCategory = $state<'all' | 'favorites' | 'custom'>('all');
|
||||
|
||||
// Fullscreen state
|
||||
let showFullscreen = $state(false);
|
||||
let fullscreenMood = $state<Mood | null>(null);
|
||||
|
||||
// Create mood dialog state
|
||||
let showCreateDialog = $state(false);
|
||||
|
||||
let displayedMoods = $derived(() => {
|
||||
switch (selectedCategory) {
|
||||
case 'favorites':
|
||||
return favoriteMoods;
|
||||
case 'custom':
|
||||
return moodsStore.customMoods;
|
||||
default:
|
||||
return allMoods;
|
||||
}
|
||||
});
|
||||
|
||||
function handleMoodClick(mood: Mood) {
|
||||
fullscreenMood = mood;
|
||||
showFullscreen = true;
|
||||
moodsStore.setActiveMood(mood);
|
||||
}
|
||||
|
||||
function handleCloseFullscreen() {
|
||||
showFullscreen = false;
|
||||
moodsStore.setActiveMood(null);
|
||||
}
|
||||
|
||||
function handleFavoriteToggle(mood: Mood) {
|
||||
moodsStore.toggleFavorite(mood.id);
|
||||
}
|
||||
|
||||
function handleFullscreenFavoriteToggle() {
|
||||
if (fullscreenMood) {
|
||||
moodsStore.toggleFavorite(fullscreenMood.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateMood(moodData: {
|
||||
name: string;
|
||||
colors: string[];
|
||||
animationType: AnimationType;
|
||||
}) {
|
||||
const newMood: Mood = {
|
||||
id: `custom-${Date.now()}`,
|
||||
name: moodData.name,
|
||||
colors: moodData.colors,
|
||||
animationType: moodData.animationType,
|
||||
isCustom: true,
|
||||
order: moodsStore.customMoods.length,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
moodsStore.addMood(newMood);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<h1 class="text-3xl font-bold">{$_('home.title')}</h1>
|
||||
<p class="text-[hsl(var(--color-muted-foreground))] mt-1">{$_('home.subtitle')}</p>
|
||||
</header>
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="px-4 py-2 rounded-full text-sm font-medium transition-colors {selectedCategory ===
|
||||
'all'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => (selectedCategory = 'all')}
|
||||
>
|
||||
{$_('home.all')}
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-full text-sm font-medium transition-colors {selectedCategory ===
|
||||
'favorites'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => (selectedCategory = 'favorites')}
|
||||
>
|
||||
{$_('home.favorites')} ({favoriteMoods.length})
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-full text-sm font-medium transition-colors {selectedCategory ===
|
||||
'custom'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => (selectedCategory = 'custom')}
|
||||
>
|
||||
{$_('home.custom')} ({moodsStore.customMoods.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Fullscreen Mood View -->
|
||||
{#if showFullscreen && fullscreenMood}
|
||||
<MoodFullscreen
|
||||
mood={fullscreenMood}
|
||||
isFavorite={moodsStore.isFavorite(fullscreenMood.id)}
|
||||
onClose={handleCloseFullscreen}
|
||||
onFavoriteToggle={handleFullscreenFavoriteToggle}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Mood Grid -->
|
||||
<section>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{#each displayedMoods() as mood (mood.id)}
|
||||
<MoodCard
|
||||
{mood}
|
||||
isActive={moodsStore.activeMood?.id === mood.id}
|
||||
isFavorite={moodsStore.isFavorite(mood.id)}
|
||||
onClick={() => handleMoodClick(mood)}
|
||||
onFavoriteToggle={() => handleFavoriteToggle(mood)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if displayedMoods().length === 0}
|
||||
<div class="text-center py-12 text-muted-foreground">
|
||||
{#if selectedCategory === 'favorites'}
|
||||
<p>No favorites yet. Click the heart icon on a mood to add it to favorites.</p>
|
||||
{:else if selectedCategory === 'custom'}
|
||||
<p>No custom moods yet. Create your own mood to get started.</p>
|
||||
{:else}
|
||||
<p>No moods available.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Sequences Section -->
|
||||
<section class="border-t border-border pt-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold">{$_('home.sequences')}</h2>
|
||||
<a href="/sequences" class="text-sm text-primary hover:underline"> View all </a>
|
||||
</div>
|
||||
<p class="text-muted-foreground">{$_('home.sequencesDescription')}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Floating Action Button -->
|
||||
<button
|
||||
type="button"
|
||||
class="fixed bottom-24 right-6 z-30 p-4 rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 hover:scale-110 transition-all"
|
||||
onclick={() => (showCreateDialog = true)}
|
||||
aria-label={$_('createMood.title')}
|
||||
>
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
|
||||
<!-- Create Mood Dialog -->
|
||||
<CreateMoodDialog
|
||||
isOpen={showCreateDialog}
|
||||
onClose={() => (showCreateDialog = false)}
|
||||
onSave={handleCreateMood}
|
||||
/>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { FeedbackPage } from '@manacore/shared-feedback-ui';
|
||||
import { feedbackService } from '$lib/api/feedback';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
</script>
|
||||
|
||||
<FeedbackPage {feedbackService} appName="Moodlit" currentUserId={authStore.user?.id} />
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { sequencesStore } from '$lib/stores/sequences.svelte';
|
||||
import { DEFAULT_MOODS, getMoodById, getMoodGradient } from '$lib/data/default-moods';
|
||||
import { moodsStore } from '$lib/stores/moods.svelte';
|
||||
import { Play, Pause, Plus, Trash, Clock } from '@manacore/shared-icons';
|
||||
import type { MoodSequence, Mood } from '$lib/types/mood';
|
||||
import MoodFullscreen from '$lib/components/mood/MoodFullscreen.svelte';
|
||||
|
||||
// Get mood by ID from both default and custom moods
|
||||
function getMood(moodId: string): Mood | undefined {
|
||||
return getMoodById(moodId) || moodsStore.customMoods.find((m) => m.id === moodId);
|
||||
}
|
||||
|
||||
// Get sequence preview gradient (first 3 moods)
|
||||
function getSequenceGradient(sequence: MoodSequence): string {
|
||||
const colors = sequence.items.slice(0, 3).map((item) => {
|
||||
const mood = getMood(item.moodId);
|
||||
return mood?.colors[0] || '#8b5cf6';
|
||||
});
|
||||
return `linear-gradient(135deg, ${colors.join(', ')})`;
|
||||
}
|
||||
|
||||
// Get total duration
|
||||
function getTotalDuration(sequence: MoodSequence): number {
|
||||
return sequence.items.reduce((sum, item) => sum + item.duration, 0);
|
||||
}
|
||||
|
||||
// Format duration
|
||||
function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
||||
}
|
||||
|
||||
// Active sequence player state
|
||||
let showPlayer = $state(false);
|
||||
let playerMood = $state<Mood | null>(null);
|
||||
let sequenceInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function playSequence(sequence: MoodSequence) {
|
||||
sequencesStore.playSequence(sequence);
|
||||
startPlayback();
|
||||
}
|
||||
|
||||
function startPlayback() {
|
||||
if (!sequencesStore.activeSequence) return;
|
||||
|
||||
const currentItem = sequencesStore.activeSequence.items[sequencesStore.currentItemIndex];
|
||||
const mood = getMood(currentItem.moodId);
|
||||
|
||||
if (mood) {
|
||||
playerMood = mood;
|
||||
showPlayer = true;
|
||||
}
|
||||
|
||||
// Clear any existing interval
|
||||
if (sequenceInterval) clearInterval(sequenceInterval);
|
||||
|
||||
// Start timer for current item
|
||||
sequenceInterval = setInterval(() => {
|
||||
if (sequencesStore.isPlaying && sequencesStore.activeSequence) {
|
||||
sequencesStore.nextItem();
|
||||
const nextItem = sequencesStore.activeSequence.items[sequencesStore.currentItemIndex];
|
||||
const nextMood = getMood(nextItem.moodId);
|
||||
if (nextMood) {
|
||||
playerMood = nextMood;
|
||||
}
|
||||
}
|
||||
}, currentItem.duration * 1000);
|
||||
}
|
||||
|
||||
function stopPlayback() {
|
||||
if (sequenceInterval) {
|
||||
clearInterval(sequenceInterval);
|
||||
sequenceInterval = null;
|
||||
}
|
||||
sequencesStore.stopSequence();
|
||||
showPlayer = false;
|
||||
playerMood = null;
|
||||
}
|
||||
|
||||
function handlePlayerClose() {
|
||||
stopPlayback();
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
$effect(() => {
|
||||
return () => {
|
||||
if (sequenceInterval) clearInterval(sequenceInterval);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-8">
|
||||
<header class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">{$_('sequences.title')}</h1>
|
||||
<p class="text-muted-foreground mt-1">{$_('sequences.subtitle')}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Sequences Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{#each sequencesStore.sequences as sequence (sequence.id)}
|
||||
<div
|
||||
class="relative rounded-2xl overflow-hidden transition-all hover:scale-[1.02] hover:shadow-lg"
|
||||
>
|
||||
<!-- Gradient Background -->
|
||||
<div class="aspect-video" style="background: {getSequenceGradient(sequence)};"></div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent"
|
||||
></div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="absolute inset-0 p-4 flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-2 text-white/80 text-sm">
|
||||
<Clock size={16} />
|
||||
{formatDuration(getTotalDuration(sequence))}
|
||||
</div>
|
||||
{#if sequence.isCustom}
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 rounded-full bg-white/20 hover:bg-red-500/50 transition-colors"
|
||||
onclick={() => sequencesStore.removeSequence(sequence.id)}
|
||||
aria-label="Delete sequence"
|
||||
>
|
||||
<Trash size={16} class="text-white" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-end justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white drop-shadow-md">
|
||||
{sequence.name}
|
||||
</h3>
|
||||
<p class="text-sm text-white/70">
|
||||
{sequence.items.length}
|
||||
{$_('sequences.moods')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="p-3 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm transition-all hover:scale-110"
|
||||
onclick={() => playSequence(sequence)}
|
||||
aria-label="Play sequence"
|
||||
>
|
||||
<Play size={24} class="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mood Preview Dots -->
|
||||
<div class="absolute bottom-16 left-4 flex gap-1">
|
||||
{#each sequence.items.slice(0, 5) as item}
|
||||
{@const mood = getMood(item.moodId)}
|
||||
{#if mood}
|
||||
<div
|
||||
class="w-4 h-4 rounded-full border-2 border-white/50"
|
||||
style="background: {mood.colors[0]};"
|
||||
title={mood.name}
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if sequence.items.length > 5}
|
||||
<div
|
||||
class="w-4 h-4 rounded-full bg-white/30 flex items-center justify-center text-[8px] text-white font-bold"
|
||||
>
|
||||
+{sequence.items.length - 5}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if sequencesStore.sequences.length === 0}
|
||||
<section class="bg-muted/50 rounded-2xl p-8 text-center">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div
|
||||
class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/20 flex items-center justify-center"
|
||||
>
|
||||
<Plus size={32} class="text-primary" />
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold mb-2">{$_('sequences.empty')}</h2>
|
||||
<p class="text-muted-foreground">{$_('sequences.emptyDescription')}</p>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Sequence Player (Fullscreen) -->
|
||||
{#if showPlayer && playerMood}
|
||||
<MoodFullscreen
|
||||
mood={playerMood}
|
||||
isFavorite={moodsStore.isFavorite(playerMood.id)}
|
||||
onClose={handlePlayerClose}
|
||||
onFavoriteToggle={() => moodsStore.toggleFavorite(playerMood?.id || '')}
|
||||
/>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { moodsStore } from '$lib/stores/moods.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
|
||||
// Animation speed options
|
||||
const speedOptions = [
|
||||
{ value: 'slow', label: 'Slow' },
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'fast', label: 'Fast' },
|
||||
];
|
||||
|
||||
// Auto timer options (in minutes)
|
||||
const autoTimerOptions = [
|
||||
{ value: 0, label: 'Off' },
|
||||
{ value: 15, label: '15 min' },
|
||||
{ value: 30, label: '30 min' },
|
||||
{ value: 60, label: '1 hour' },
|
||||
{ value: 120, label: '2 hours' },
|
||||
];
|
||||
|
||||
function handleBrightnessChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
moodsStore.updateSettings({ brightness: parseInt(target.value) });
|
||||
}
|
||||
|
||||
function handleSpeedChange(speed: string) {
|
||||
moodsStore.updateSettings({ animationSpeed: speed as 'slow' | 'normal' | 'fast' });
|
||||
}
|
||||
|
||||
function handleAutoTimerChange(minutes: number) {
|
||||
moodsStore.updateSettings({ autoTimer: minutes });
|
||||
}
|
||||
|
||||
function handleResetSettings() {
|
||||
if (confirm($_('settings.resetConfirm'))) {
|
||||
moodsStore.updateSettings({
|
||||
brightness: 100,
|
||||
animationSpeed: 'normal',
|
||||
autoTimer: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-8 max-w-2xl">
|
||||
<header>
|
||||
<h1 class="text-3xl font-bold">{$_('settings.title')}</h1>
|
||||
</header>
|
||||
|
||||
<!-- Brightness -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-semibold">{$_('settings.brightness')}</h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="100"
|
||||
step="5"
|
||||
value={moodsStore.settings.brightness}
|
||||
onchange={handleBrightnessChange}
|
||||
class="flex-1 h-2 bg-muted rounded-full appearance-none cursor-pointer accent-primary"
|
||||
/>
|
||||
<span class="w-12 text-right text-sm text-muted-foreground">
|
||||
{moodsStore.settings.brightness}%
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Animation Speed -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-semibold">{$_('settings.animationSpeed')}</h2>
|
||||
<div class="flex gap-2">
|
||||
{#each speedOptions as option}
|
||||
<button
|
||||
class="flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors {moodsStore
|
||||
.settings.animationSpeed === option.value
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => handleSpeedChange(option.value)}
|
||||
>
|
||||
{$_(`settings.${option.value}`)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Auto Timer -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-semibold">{$_('settings.autoTimer')}</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each autoTimerOptions as option}
|
||||
<button
|
||||
class="py-2 px-4 rounded-lg text-sm font-medium transition-colors {moodsStore.settings
|
||||
.autoTimer === option.value
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => handleAutoTimerChange(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Theme -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-semibold">Theme</h2>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors {theme.mode ===
|
||||
'light'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('light')}
|
||||
>
|
||||
Light
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors {theme.mode ===
|
||||
'dark'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('dark')}
|
||||
>
|
||||
Dark
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors {theme.mode ===
|
||||
'system'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('system')}
|
||||
>
|
||||
System
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reset -->
|
||||
<section class="pt-4 border-t border-border">
|
||||
<button
|
||||
class="py-2 px-4 rounded-lg text-sm font-medium text-destructive hover:bg-destructive/10 transition-colors"
|
||||
onclick={handleResetSettings}
|
||||
>
|
||||
{$_('settings.reset')}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
|
||||
import { MoodlitLogo } from '@manacore/shared-branding';
|
||||
import { getForgotPasswordTranslations } from '@manacore/shared-i18n';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
// Get translations based on current locale
|
||||
const translations = $derived(getForgotPasswordTranslations($locale || 'de'));
|
||||
|
||||
async function handleForgotPassword(email: string) {
|
||||
return authStore.forgotPassword(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<ForgotPasswordPage
|
||||
appName="Moodlit"
|
||||
logo={MoodlitLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onForgotPassword={handleForgotPassword}
|
||||
{goto}
|
||||
loginPath="/login"
|
||||
lightBackground="#faf5ff"
|
||||
darkBackground="#1a1625"
|
||||
{translations}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
{/snippet}
|
||||
{#snippet appSlider()}
|
||||
<AppSlider />
|
||||
{/snippet}
|
||||
</ForgotPasswordPage>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { MoodlitLogo } from '@manacore/shared-branding';
|
||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
// Get translations based on current locale
|
||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
return authStore.signIn(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginPage
|
||||
appName="Moodlit"
|
||||
logo={MoodlitLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignIn={handleSignIn}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect="/"
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#faf5ff"
|
||||
darkBackground="#1a1625"
|
||||
{translations}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
{/snippet}
|
||||
{#snippet appSlider()}
|
||||
<AppSlider />
|
||||
{/snippet}
|
||||
</LoginPage>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import { MoodlitLogo } from '@manacore/shared-branding';
|
||||
import { getRegisterTranslations } from '@manacore/shared-i18n';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
// Get translations based on current locale
|
||||
const translations = $derived(getRegisterTranslations($locale || 'de'));
|
||||
|
||||
async function handleSignUp(email: string, password: string) {
|
||||
return authStore.signUp(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<RegisterPage
|
||||
appName="Moodlit"
|
||||
logo={MoodlitLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignUp={handleSignUp}
|
||||
{goto}
|
||||
successRedirect="/"
|
||||
loginPath="/login"
|
||||
lightBackground="#faf5ff"
|
||||
darkBackground="#1a1625"
|
||||
{translations}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
{/snippet}
|
||||
{#snippet appSlider()}
|
||||
<AppSlider />
|
||||
{/snippet}
|
||||
</RegisterPage>
|
||||
10
apps-archived/moodlit/apps/web/src/routes/+layout.svelte
Normal file
10
apps-archived/moodlit/apps/web/src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import '$lib/i18n';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-[hsl(var(--color-background))] text-[hsl(var(--color-foreground))]">
|
||||
{@render children()}
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue