From 5fd5423f8e8361995fa5b712cc0d78d49d091a0a Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 5 Dec 2025 03:45:07 +0100 Subject: [PATCH] feat(manacore): add Picture, ManaDeck, and Clock dashboard widgets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 3 new widget types: picture-recent, manadeck-progress, clock-timers - Create API services for Picture, ManaDeck, and Clock apps - Add PictureRecentWidget showing recent AI-generated images - Add ManadeckProgressWidget showing learning progress and due cards - Add ClockTimersWidget showing active timers and alarms - Update WidgetContainer to include new widget components - Add i18n translations (DE/EN) for all new widgets - Extend WIDGET_REGISTRY with metadata for new widgets 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../apps/web/src/lib/api/services/clock.ts | 199 ++++++++++++++++++ .../apps/web/src/lib/api/services/index.ts | 3 + .../apps/web/src/lib/api/services/manadeck.ts | 110 ++++++++++ .../apps/web/src/lib/api/services/picture.ts | 81 +++++++ .../dashboard/WidgetContainer.svelte | 6 + .../widgets/ClockTimersWidget.svelte | 184 ++++++++++++++++ .../widgets/ManadeckProgressWidget.svelte | 155 ++++++++++++++ .../widgets/PictureRecentWidget.svelte | 130 ++++++++++++ .../apps/web/src/lib/i18n/locales/de.json | 26 +++ .../apps/web/src/lib/i18n/locales/en.json | 26 +++ .../apps/web/src/lib/types/dashboard.ts | 43 +++- 11 files changed, 961 insertions(+), 2 deletions(-) create mode 100644 apps/manacore/apps/web/src/lib/api/services/clock.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/manadeck.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/picture.ts create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/ClockTimersWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/ManadeckProgressWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/PictureRecentWidget.svelte diff --git a/apps/manacore/apps/web/src/lib/api/services/clock.ts b/apps/manacore/apps/web/src/lib/api/services/clock.ts new file mode 100644 index 000000000..ad77a955d --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/clock.ts @@ -0,0 +1,199 @@ +/** + * Clock API Service + * + * Fetches timers and alarms from local storage for dashboard widgets. + * Note: Clock app stores data in localStorage, not a backend. + */ + +import type { ApiResult } from '../base-client'; + +/** + * Timer entity from Clock app + */ +export interface Timer { + id: string; + name: string; + duration: number; // Total duration in seconds + remaining: number; // Remaining time in seconds + isRunning: boolean; + isPaused: boolean; + createdAt: string; + completedAt?: string; +} + +/** + * Alarm entity from Clock app + */ +export interface Alarm { + id: string; + name: string; + time: string; // HH:MM format + days: number[]; // 0-6 (Sunday to Saturday) + isEnabled: boolean; + sound?: string; + createdAt: string; +} + +/** + * Pomodoro session from Clock app + */ +export interface PomodoroSession { + id: string; + type: 'work' | 'shortBreak' | 'longBreak'; + duration: number; + completedAt: string; +} + +/** + * Clock statistics + */ +export interface ClockStats { + activeTimers: number; + enabledAlarms: number; + pomodorosToday: number; + focusTimeToday: number; // In minutes +} + +// LocalStorage keys (matching Clock app's storage) +const STORAGE_KEYS = { + timers: 'clock-timers', + alarms: 'clock-alarms', + pomodoros: 'clock-pomodoros', +}; + +/** + * Clock service for dashboard widgets + * + * Since Clock stores data in localStorage, this service reads from there. + */ +export const clockService = { + /** + * Get all timers + */ + async getTimers(): Promise> { + try { + if (typeof window === 'undefined') { + return { data: [], error: null }; + } + + const stored = localStorage.getItem(STORAGE_KEYS.timers); + const timers = stored ? JSON.parse(stored) : []; + return { data: timers, error: null }; + } catch { + return { data: null, error: 'Failed to load timers' }; + } + }, + + /** + * Get active timers (running or paused with time remaining) + */ + async getActiveTimers(): Promise> { + const result = await this.getTimers(); + + if (result.error || !result.data) { + return result; + } + + const activeTimers = result.data.filter((t) => t.isRunning || (t.isPaused && t.remaining > 0)); + return { data: activeTimers, error: null }; + }, + + /** + * Get all alarms + */ + async getAlarms(): Promise> { + try { + if (typeof window === 'undefined') { + return { data: [], error: null }; + } + + const stored = localStorage.getItem(STORAGE_KEYS.alarms); + const alarms = stored ? JSON.parse(stored) : []; + return { data: alarms, error: null }; + } catch { + return { data: null, error: 'Failed to load alarms' }; + } + }, + + /** + * Get enabled alarms sorted by next trigger time + */ + async getEnabledAlarms(): Promise> { + const result = await this.getAlarms(); + + if (result.error || !result.data) { + return result; + } + + const now = new Date(); + const currentDay = now.getDay(); + const currentTime = now.getHours() * 60 + now.getMinutes(); + + const enabledAlarms = result.data + .filter((a) => a.isEnabled) + .sort((a, b) => { + // Parse alarm times + const [aHours, aMinutes] = a.time.split(':').map(Number); + const [bHours, bMinutes] = b.time.split(':').map(Number); + const aTime = aHours * 60 + aMinutes; + const bTime = bHours * 60 + bMinutes; + + // Sort by time of day + return aTime - bTime; + }); + + return { data: enabledAlarms, error: null }; + }, + + /** + * Get clock statistics + */ + async getStats(): Promise> { + try { + const [timersResult, alarmsResult] = await Promise.all([ + this.getActiveTimers(), + this.getEnabledAlarms(), + ]); + + // Get pomodoro sessions for today + const pomodorosStored = localStorage.getItem(STORAGE_KEYS.pomodoros); + const pomodoros: PomodoroSession[] = pomodorosStored ? JSON.parse(pomodorosStored) : []; + + const today = new Date().toDateString(); + const todayPomodoros = pomodoros.filter( + (p) => new Date(p.completedAt).toDateString() === today + ); + + const focusTimeToday = todayPomodoros + .filter((p) => p.type === 'work') + .reduce((sum, p) => sum + p.duration, 0); + + return { + data: { + activeTimers: timersResult.data?.length || 0, + enabledAlarms: alarmsResult.data?.length || 0, + pomodorosToday: todayPomodoros.filter((p) => p.type === 'work').length, + focusTimeToday: Math.round(focusTimeToday / 60), + }, + error: null, + }; + } catch { + return { data: null, error: 'Failed to load clock stats' }; + } + }, + + /** + * Get next alarm time as a formatted string + */ + async getNextAlarmTime(): Promise> { + const result = await this.getEnabledAlarms(); + + if (result.error || !result.data || result.data.length === 0) { + return { data: null, error: result.error }; + } + + // Get next alarm + const nextAlarm = result.data[0]; + return { data: nextAlarm.time, error: null }; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/api/services/index.ts b/apps/manacore/apps/web/src/lib/api/services/index.ts index b7f99db74..44e830e53 100644 --- a/apps/manacore/apps/web/src/lib/api/services/index.ts +++ b/apps/manacore/apps/web/src/lib/api/services/index.ts @@ -9,3 +9,6 @@ export { calendarService, type Calendar, type CalendarEvent } from './calendar'; export { chatService, type Conversation, type Message, type AiModel } from './chat'; export { contactsService, type Contact, type ContactActivity } from './contacts'; export { zitareService, type Favorite, type Quote, type QuoteList } from './zitare'; +export { pictureService, type GeneratedImage, type GenerationStats } from './picture'; +export { manadeckService, type Deck, type Card, type LearningProgress } from './manadeck'; +export { clockService, type Timer, type Alarm, type ClockStats } from './clock'; diff --git a/apps/manacore/apps/web/src/lib/api/services/manadeck.ts b/apps/manacore/apps/web/src/lib/api/services/manadeck.ts new file mode 100644 index 000000000..8ed5fab23 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/manadeck.ts @@ -0,0 +1,110 @@ +/** + * ManaDeck API Service + * + * Fetches learning progress and deck data from the ManaDeck backend for dashboard widgets. + */ + +import { createApiClient, type ApiResult } from '../base-client'; + +// Backend URL - falls back to localhost for development +const MANADECK_API_URL = import.meta.env.PUBLIC_MANADECK_API_URL || 'http://localhost:3009'; + +const client = createApiClient(MANADECK_API_URL); + +/** + * Deck entity from ManaDeck backend + */ +export interface Deck { + id: string; + userId: string; + name: string; + description?: string; + cardCount: number; + dueCount: number; + newCount: number; + createdAt: string; + updatedAt: string; + lastStudied?: string; +} + +/** + * Card entity from ManaDeck backend + */ +export interface Card { + id: string; + deckId: string; + front: string; + back: string; + nextReview: string; + interval: number; + easeFactor: number; + repetitions: number; + createdAt: string; + updatedAt: string; +} + +/** + * Learning progress statistics + */ +export interface LearningProgress { + totalCards: number; + cardsLearned: number; + cardsDueToday: number; + newCardsToday: number; + streakDays: number; + reviewsToday: number; + averageRetention: number; +} + +/** + * ManaDeck service for dashboard widgets + */ +export const manadeckService = { + /** + * Get user's decks + */ + async getDecks(): Promise> { + return client.get('/api/decks'); + }, + + /** + * Get learning progress + */ + async getLearningProgress(): Promise> { + return client.get('/api/progress'); + }, + + /** + * Get cards due for review today + */ + async getDueCards(limit = 10): Promise> { + return client.get(`/api/cards/due?limit=${limit}`); + }, + + /** + * Get total due cards count across all decks + */ + async getTotalDueCount(): Promise> { + const result = await this.getDecks(); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + const totalDue = result.data.reduce((sum, deck) => sum + deck.dueCount, 0); + return { data: totalDue, error: null }; + }, + + /** + * Get study streak + */ + async getStreak(): Promise> { + const result = await this.getLearningProgress(); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + return { data: result.data.streakDays, error: null }; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/api/services/picture.ts b/apps/manacore/apps/web/src/lib/api/services/picture.ts new file mode 100644 index 000000000..bfd903f88 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/picture.ts @@ -0,0 +1,81 @@ +/** + * Picture API Service + * + * Fetches recent AI-generated images from the Picture backend for dashboard widgets. + */ + +import { createApiClient, type ApiResult } from '../base-client'; + +// Backend URL - falls back to localhost for development +const PICTURE_API_URL = import.meta.env.PUBLIC_PICTURE_API_URL || 'http://localhost:3006'; + +const client = createApiClient(PICTURE_API_URL); + +/** + * Generated image entity from Picture backend + */ +export interface GeneratedImage { + id: string; + userId: string; + prompt: string; + negativePrompt?: string; + imageUrl: string; + thumbnailUrl?: string; + width: number; + height: number; + model: string; + seed?: number; + steps?: number; + cfgScale?: number; + createdAt: string; + isFavorite?: boolean; + tags?: string[]; +} + +/** + * Generation statistics + */ +export interface GenerationStats { + totalGenerations: number; + thisMonth: number; + favoriteCount: number; +} + +/** + * Picture service for dashboard widgets + */ +export const pictureService = { + /** + * Get user's recent generations + */ + async getRecentGenerations(limit = 6): Promise> { + return client.get(`/api/generations?limit=${limit}&sort=createdAt:desc`); + }, + + /** + * Get user's favorite images + */ + async getFavorites(limit = 6): Promise> { + return client.get(`/api/generations?favorite=true&limit=${limit}`); + }, + + /** + * Get generation statistics + */ + async getStats(): Promise> { + return client.get('/api/stats'); + }, + + /** + * Get total generation count + */ + async getGenerationCount(): Promise> { + const result = await this.getStats(); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + return { data: result.data.totalGenerations, error: null }; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte index 340c3e2d0..ce932ed91 100644 --- a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte +++ b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte @@ -22,6 +22,9 @@ import ChatRecentWidget from './widgets/ChatRecentWidget.svelte'; import ContactsFavoritesWidget from './widgets/ContactsFavoritesWidget.svelte'; import ZitareQuoteWidget from './widgets/ZitareQuoteWidget.svelte'; + import PictureRecentWidget from './widgets/PictureRecentWidget.svelte'; + import ManadeckProgressWidget from './widgets/ManadeckProgressWidget.svelte'; + import ClockTimersWidget from './widgets/ClockTimersWidget.svelte'; interface Props { widget: WidgetConfig; @@ -59,6 +62,9 @@ 'chat-recent': ChatRecentWidget, 'contacts-favorites': ContactsFavoritesWidget, 'zitare-quote': ZitareQuoteWidget, + 'picture-recent': PictureRecentWidget, + 'manadeck-progress': ManadeckProgressWidget, + 'clock-timers': ClockTimersWidget, } as const; const WidgetComponent = $derived(widgetComponents[widget.type]); diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ClockTimersWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ClockTimersWidget.svelte new file mode 100644 index 000000000..ee40d0fb6 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ClockTimersWidget.svelte @@ -0,0 +1,184 @@ + + +
+
+

+ + {$_('dashboard.widgets.clock.title')} +

+
+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if timers.length === 0 && alarms.length === 0} +
+
🕐
+

+ {$_('dashboard.widgets.clock.empty')} +

+ + {$_('dashboard.widgets.clock.open')} + +
+ {:else} + + {#if stats && stats.pomodorosToday > 0} +
+
+ 🍅 + {stats.pomodorosToday} + Pomodoros +
+
+ ⏱️ + {stats.focusTimeToday} + min +
+
+ {/if} + + + {#if timers.length > 0} +
+
+ {$_('dashboard.widgets.clock.active_timers')} +
+
+ {#each timers as timer} +
+
+ {#if timer.isRunning} + + + + + {:else} + + {/if} + {timer.name || 'Timer'} +
+ + {formatTime(timer.remaining)} + +
+ {/each} +
+
+ {/if} + + + {#if alarms.length > 0} +
+
+ {$_('dashboard.widgets.clock.alarms')} +
+
+ {#each alarms as alarm} +
+
+
{alarm.time}
+
+ {alarm.name || formatAlarmDays(alarm.days)} +
+
+
🔔
+
+ {/each} +
+
+ {/if} + + + {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ManadeckProgressWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ManadeckProgressWidget.svelte new file mode 100644 index 000000000..f36601c52 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ManadeckProgressWidget.svelte @@ -0,0 +1,155 @@ + + +
+
+

+ 🎴 + {$_('dashboard.widgets.manadeck.title')} +

+
+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if !progress || decks.length === 0} +
+
📚
+

+ {$_('dashboard.widgets.manadeck.empty')} +

+ + {$_('dashboard.widgets.manadeck.create_deck')} + +
+ {:else} + +
+
+
{progress.streakDays}
+
{$_('dashboard.widgets.manadeck.streak')}
+
+
+
{totalDue}
+
{$_('dashboard.widgets.manadeck.due')}
+
+
+
{progress.reviewsToday}
+
{$_('dashboard.widgets.manadeck.today')}
+
+
+ + +
+
+ {$_('dashboard.widgets.manadeck.learned')} + {progressPercent}% +
+
+
+
+
+ + + {#if decksWithDue.length > 0} + + {/if} + + {#if totalDue > 0} + + {/if} + {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/PictureRecentWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/PictureRecentWidget.svelte new file mode 100644 index 000000000..d7b1fb7df --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/PictureRecentWidget.svelte @@ -0,0 +1,130 @@ + + +
+
+

+ 🎨 + {$_('dashboard.widgets.picture.title')} +

+
+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if data.length === 0} +
+
🖼️
+

+ {$_('dashboard.widgets.picture.empty')} +

+ + {$_('dashboard.widgets.picture.create')} + +
+ {:else} + + + {/if} +
diff --git a/apps/manacore/apps/web/src/lib/i18n/locales/de.json b/apps/manacore/apps/web/src/lib/i18n/locales/de.json index d62a2f79f..4bd7d5853 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/de.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/de.json @@ -66,6 +66,32 @@ "explore": "Zitate entdecken", "refresh": "Neues Zitat", "view_all": "Alle Zitate" + }, + "picture": { + "title": "Bilder", + "description": "Letzte KI-Generierungen", + "empty": "Noch keine Bilder erstellt", + "create": "Bild erstellen", + "view_all": "Alle Bilder" + }, + "manadeck": { + "title": "Lernfortschritt", + "description": "Deine Lernkarten", + "empty": "Noch keine Decks erstellt", + "create_deck": "Deck erstellen", + "streak": "Tage Serie", + "due": "fällig", + "today": "heute gelernt", + "learned": "Gelernt", + "start_study": "Lernen starten" + }, + "clock": { + "title": "Timer & Wecker", + "description": "Aktive Timer und Wecker", + "empty": "Keine aktiven Timer oder Wecker", + "open": "Clock öffnen", + "active_timers": "Aktive Timer", + "alarms": "Wecker" } } }, diff --git a/apps/manacore/apps/web/src/lib/i18n/locales/en.json b/apps/manacore/apps/web/src/lib/i18n/locales/en.json index 5a6ca3930..4f4ce9120 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/en.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/en.json @@ -66,6 +66,32 @@ "explore": "Explore quotes", "refresh": "New quote", "view_all": "All quotes" + }, + "picture": { + "title": "Images", + "description": "Recent AI generations", + "empty": "No images created yet", + "create": "Create image", + "view_all": "View all images" + }, + "manadeck": { + "title": "Learning Progress", + "description": "Your flashcards", + "empty": "No decks created yet", + "create_deck": "Create deck", + "streak": "Day streak", + "due": "due", + "today": "learned today", + "learned": "Learned", + "start_study": "Start studying" + }, + "clock": { + "title": "Timers & Alarms", + "description": "Active timers and alarms", + "empty": "No active timers or alarms", + "open": "Open Clock", + "active_timers": "Active Timers", + "alarms": "Alarms" } } }, diff --git a/apps/manacore/apps/web/src/lib/types/dashboard.ts b/apps/manacore/apps/web/src/lib/types/dashboard.ts index a34a2f26b..364a5caaf 100644 --- a/apps/manacore/apps/web/src/lib/types/dashboard.ts +++ b/apps/manacore/apps/web/src/lib/types/dashboard.ts @@ -16,7 +16,10 @@ export type WidgetType = | 'calendar-events' // Calendar API: upcoming events | 'chat-recent' // Chat API: recent conversations | 'contacts-favorites' // Contacts API: favorite contacts - | 'zitare-quote'; // Zitare API: favorite quotes + | 'zitare-quote' // Zitare API: daily inspiration quote + | 'picture-recent' // Picture API: recent generations + | 'manadeck-progress' // ManaDeck API: learning progress + | 'clock-timers'; // Clock: active timers and alarms /** * Widget size - maps to CSS Grid columns @@ -101,7 +104,16 @@ export interface WidgetMeta { /** Whether multiple instances are allowed */ allowMultiple: boolean; /** Required backend (for status display) */ - requiredBackend?: 'todo' | 'calendar' | 'chat' | 'contacts' | 'zitare' | 'mana-core-auth'; + requiredBackend?: + | 'todo' + | 'calendar' + | 'chat' + | 'contacts' + | 'zitare' + | 'picture' + | 'manadeck' + | 'clock' + | 'mana-core-auth'; } /** @@ -188,6 +200,33 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [ allowMultiple: false, requiredBackend: 'zitare', }, + { + type: 'picture-recent', + nameKey: 'dashboard.widgets.picture.title', + descriptionKey: 'dashboard.widgets.picture.description', + icon: '🎨', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'picture', + }, + { + type: 'manadeck-progress', + nameKey: 'dashboard.widgets.manadeck.title', + descriptionKey: 'dashboard.widgets.manadeck.description', + icon: '🎴', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'manadeck', + }, + { + type: 'clock-timers', + nameKey: 'dashboard.widgets.clock.title', + descriptionKey: 'dashboard.widgets.clock.description', + icon: '⏰', + defaultSize: 'small', + allowMultiple: false, + requiredBackend: 'clock', + }, ]; /**