From 10f4da819b5196aa7cc729761b2e32429d8ab8bf Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:41:24 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(manacore):=20add=20configurabl?= =?UTF-8?q?e=20cross-app=20dashboard=20with=20widgets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add widget system with 9 widget types (credits, tasks, calendar, chat, contacts, quotes, etc.) - Implement drag-and-drop grid layout with edit mode - Create API services for todo, calendar, chat, contacts, and zitare backends - Add dashboard store with localStorage persistence - Include German and English i18n translations - Replace legacy dashboard with new configurable widget-based UI --- apps/manacore/apps/web/package.json | 1 + .../apps/web/src/lib/api/base-client.ts | 157 ++++++++++++ .../apps/web/src/lib/api/services/calendar.ts | 94 ++++++++ .../apps/web/src/lib/api/services/chat.ts | 110 +++++++++ .../apps/web/src/lib/api/services/contacts.ts | 139 +++++++++++ .../apps/web/src/lib/api/services/index.ts | 11 + .../apps/web/src/lib/api/services/todo.ts | 95 ++++++++ .../apps/web/src/lib/api/services/zitare.ts | 92 +++++++ .../components/dashboard/DashboardGrid.svelte | 59 +++++ .../dashboard/WidgetContainer.svelte | 138 +++++++++++ .../components/dashboard/WidgetError.svelte | 52 ++++ .../dashboard/WidgetSkeleton.svelte | 37 +++ .../widgets/CalendarEventsWidget.svelte | 130 ++++++++++ .../dashboard/widgets/ChatRecentWidget.svelte | 117 +++++++++ .../widgets/ContactsFavoritesWidget.svelte | 133 +++++++++++ .../dashboard/widgets/CreditsWidget.svelte | 68 ++++++ .../widgets/QuickActionsWidget.svelte | 49 ++++ .../dashboard/widgets/TasksTodayWidget.svelte | 140 +++++++++++ .../widgets/TasksUpcomingWidget.svelte | 116 +++++++++ .../widgets/TransactionsWidget.svelte | 98 ++++++++ .../widgets/ZitareQuoteWidget.svelte | 115 +++++++++ .../web/src/lib/config/default-dashboard.ts | 73 ++++++ .../apps/web/src/lib/i18n/locales/de.json | 69 ++++++ .../apps/web/src/lib/i18n/locales/en.json | 69 ++++++ .../web/src/lib/stores/dashboard.svelte.ts | 223 +++++++++++++++++ .../apps/web/src/lib/types/dashboard.ts | 208 ++++++++++++++++ .../src/routes/(app)/dashboard/+page.svelte | 224 ++++-------------- 27 files changed, 2641 insertions(+), 176 deletions(-) create mode 100644 apps/manacore/apps/web/src/lib/api/base-client.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/calendar.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/chat.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/contacts.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/index.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/todo.ts create mode 100644 apps/manacore/apps/web/src/lib/api/services/zitare.ts create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/DashboardGrid.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/WidgetError.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/WidgetSkeleton.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/CalendarEventsWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/ChatRecentWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/ContactsFavoritesWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/CreditsWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/QuickActionsWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksUpcomingWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/TransactionsWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/ZitareQuoteWidget.svelte create mode 100644 apps/manacore/apps/web/src/lib/config/default-dashboard.ts create mode 100644 apps/manacore/apps/web/src/lib/stores/dashboard.svelte.ts create mode 100644 apps/manacore/apps/web/src/lib/types/dashboard.ts diff --git a/apps/manacore/apps/web/package.json b/apps/manacore/apps/web/package.json index 1fde200a4..ae3c273cf 100644 --- a/apps/manacore/apps/web/package.json +++ b/apps/manacore/apps/web/package.json @@ -59,6 +59,7 @@ "@manacore/shared-utils": "workspace:*", "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.81.1", + "svelte-dnd-action": "^0.9.68", "svelte-i18n": "^4.0.0" }, "type": "module" diff --git a/apps/manacore/apps/web/src/lib/api/base-client.ts b/apps/manacore/apps/web/src/lib/api/base-client.ts new file mode 100644 index 000000000..122f54e33 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/base-client.ts @@ -0,0 +1,157 @@ +/** + * Base API Client with Retry Logic + * + * Provides authenticated fetch with exponential backoff retry. + */ + +import { authStore } from '$lib/stores/authStore.svelte'; + +/** + * Retry configuration + */ +export interface RetryConfig { + /** Maximum number of retry attempts (default: 3) */ + maxRetries: number; + /** Initial delay in milliseconds (default: 1000) */ + retryDelay: number; + /** Multiplier for exponential backoff (default: 2) */ + backoffMultiplier: number; +} + +/** + * API response wrapper + */ +export interface ApiResult { + data: T | null; + error: string | null; +} + +const DEFAULT_RETRY_CONFIG: RetryConfig = { + maxRetries: 3, + retryDelay: 1000, + backoffMultiplier: 2, +}; + +/** + * Sleep utility + */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Fetch with authentication and retry logic + * + * @param url - Full URL to fetch + * @param options - Fetch options (optional) + * @param retryConfig - Retry configuration (optional) + * @returns Promise with data or error + */ +export async function fetchWithRetry( + url: string, + options: RequestInit = {}, + retryConfig: Partial = {} +): Promise> { + const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; + let lastError: string | null = null; + + for (let attempt = 0; attempt <= config.maxRetries; attempt++) { + try { + // Get fresh token for each attempt + const token = await authStore.getAccessToken(); + + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...options.headers, + }, + }); + + if (!response.ok) { + // Don't retry on auth errors + if (response.status === 401 || response.status === 403) { + return { + data: null, + error: `Authentication failed (${response.status})`, + }; + } + + // Don't retry on client errors (except rate limiting) + if (response.status >= 400 && response.status < 500 && response.status !== 429) { + const errorBody = await response.json().catch(() => ({ message: 'Request failed' })); + return { + data: null, + error: errorBody.message || `HTTP ${response.status}`, + }; + } + + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + return { data, error: null }; + } catch (e) { + lastError = e instanceof Error ? e.message : 'Unknown error'; + + // Don't retry on last attempt + if (attempt < config.maxRetries) { + const delay = config.retryDelay * Math.pow(config.backoffMultiplier, attempt); + console.warn(`API request failed, retrying in ${delay}ms...`, { + url, + attempt, + error: lastError, + }); + await sleep(delay); + } + } + } + + return { data: null, error: lastError }; +} + +/** + * Create an API client for a specific backend + */ +export function createApiClient(baseUrl: string) { + return { + get(endpoint: string, retryConfig?: Partial): Promise> { + return fetchWithRetry(`${baseUrl}${endpoint}`, { method: 'GET' }, retryConfig); + }, + + post( + endpoint: string, + body?: unknown, + retryConfig?: Partial + ): Promise> { + return fetchWithRetry( + `${baseUrl}${endpoint}`, + { + method: 'POST', + body: body ? JSON.stringify(body) : undefined, + }, + retryConfig + ); + }, + + put( + endpoint: string, + body?: unknown, + retryConfig?: Partial + ): Promise> { + return fetchWithRetry( + `${baseUrl}${endpoint}`, + { + method: 'PUT', + body: body ? JSON.stringify(body) : undefined, + }, + retryConfig + ); + }, + + delete(endpoint: string, retryConfig?: Partial): Promise> { + return fetchWithRetry(`${baseUrl}${endpoint}`, { method: 'DELETE' }, retryConfig); + }, + }; +} diff --git a/apps/manacore/apps/web/src/lib/api/services/calendar.ts b/apps/manacore/apps/web/src/lib/api/services/calendar.ts new file mode 100644 index 000000000..1c89bafc3 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/calendar.ts @@ -0,0 +1,94 @@ +/** + * Calendar API Service + * + * Fetches events from the Calendar backend for dashboard widgets. + */ + +import { createApiClient, type ApiResult } from '../base-client'; + +// Backend URL - falls back to localhost for development +const CALENDAR_API_URL = import.meta.env.PUBLIC_CALENDAR_API_URL || 'http://localhost:3014'; + +const client = createApiClient(CALENDAR_API_URL); + +/** + * Calendar entity from Calendar backend + */ +export interface Calendar { + id: string; + userId: string; + name: string; + description?: string; + color: string; + isDefault: boolean; + isVisible: boolean; + timezone: string; + createdAt: string; + updatedAt: string; +} + +/** + * Event entity from Calendar backend + */ +export interface CalendarEvent { + id: string; + calendarId: string; + userId: string; + title: string; + description?: string; + location?: string; + startTime: string; + endTime: string; + isAllDay: boolean; + timezone: string; + recurrenceRule?: string; + color?: string; + status: 'confirmed' | 'tentative' | 'cancelled'; + createdAt: string; + updatedAt: string; +} + +/** + * Calendar service for dashboard widgets + */ +export const calendarService = { + /** + * Get upcoming events for the next N days + */ + async getUpcomingEvents(days: number = 7): Promise> { + const startDate = new Date().toISOString().split('T')[0]; + const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + return client.get(`/events?startDate=${startDate}&endDate=${endDate}`); + }, + + /** + * Get today's events + */ + async getTodayEvents(): Promise> { + const today = new Date().toISOString().split('T')[0]; + return client.get(`/events?startDate=${today}&endDate=${today}`); + }, + + /** + * Get all calendars + */ + async getCalendars(): Promise> { + return client.get('/calendars'); + }, + + /** + * Get events for a specific calendar + */ + async getCalendarEvents( + calendarId: string, + days: number = 7 + ): Promise> { + const startDate = new Date().toISOString().split('T')[0]; + const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + return client.get( + `/events?calendarIds=${calendarId}&startDate=${startDate}&endDate=${endDate}` + ); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/api/services/chat.ts b/apps/manacore/apps/web/src/lib/api/services/chat.ts new file mode 100644 index 000000000..51a201c46 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/chat.ts @@ -0,0 +1,110 @@ +/** + * Chat API Service + * + * Fetches conversations from the Chat backend for dashboard widgets. + */ + +import { createApiClient, type ApiResult } from '../base-client'; + +// Backend URL - falls back to localhost for development +const CHAT_API_URL = import.meta.env.PUBLIC_CHAT_API_URL || 'http://localhost:3002'; + +const client = createApiClient(CHAT_API_URL); + +/** + * Conversation entity from Chat backend + */ +export interface Conversation { + id: string; + userId: string; + title: string; + modelId: string; + spaceId?: string; + conversationMode: 'free' | 'guided' | 'template'; + documentMode: boolean; + isArchived: boolean; + isPinned: boolean; + createdAt: string; + updatedAt: string; +} + +/** + * Message entity from Chat backend + */ +export interface Message { + id: string; + conversationId: string; + sender: 'user' | 'assistant' | 'system'; + messageText: string; + createdAt: string; +} + +/** + * AI Model entity from Chat backend + */ +export interface AiModel { + id: string; + name: string; + description: string; +} + +/** + * Chat service for dashboard widgets + */ +export const chatService = { + /** + * Get recent conversations + */ + async getRecentConversations(limit: number = 5): Promise> { + const result = await client.get('/conversations'); + + if (result.error || !result.data) { + return result; + } + + // Sort by updatedAt and limit + const sorted = result.data + .filter((c) => !c.isArchived) + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) + .slice(0, limit); + + return { data: sorted, error: null }; + }, + + /** + * Get pinned conversations + */ + async getPinnedConversations(): Promise> { + const result = await client.get('/conversations'); + + if (result.error || !result.data) { + return result; + } + + const pinned = result.data.filter((c) => c.isPinned && !c.isArchived); + return { data: pinned, error: null }; + }, + + /** + * Get available AI models + */ + async getModels(): Promise> { + return client.get('/chat/models'); + }, + + /** + * Get conversation count + */ + async getConversationCount(): Promise> { + const result = await client.get('/conversations'); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + const active = result.data.filter((c) => !c.isArchived); + const pinned = active.filter((c) => c.isPinned); + + return { data: { total: active.length, pinned: pinned.length }, error: null }; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/api/services/contacts.ts b/apps/manacore/apps/web/src/lib/api/services/contacts.ts new file mode 100644 index 000000000..1ad60be60 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/contacts.ts @@ -0,0 +1,139 @@ +/** + * Contacts API Service + * + * Fetches contacts from the Contacts backend for dashboard widgets. + */ + +import { createApiClient, type ApiResult } from '../base-client'; + +// Backend URL - falls back to localhost for development +const CONTACTS_API_URL = import.meta.env.PUBLIC_CONTACTS_API_URL || 'http://localhost:3015'; + +const client = createApiClient(CONTACTS_API_URL); + +/** + * Contact entity from Contacts backend + */ +export interface Contact { + id: string; + userId: string; + firstName?: string; + lastName?: string; + displayName?: string; + nickname?: string; + email?: string; + phone?: string; + mobile?: string; + company?: string; + jobTitle?: string; + birthday?: string; + notes?: string; + isFavorite: boolean; + isArchived: boolean; + createdAt: string; + updatedAt: string; +} + +/** + * Activity entity from Contacts backend + */ +export interface ContactActivity { + id: string; + contactId: string; + userId: string; + activityType: 'created' | 'updated' | 'called' | 'emailed' | 'met' | 'note_added'; + description?: string; + metadata?: Record; + createdAt: string; +} + +/** + * Contacts service for dashboard widgets + */ +export const contactsService = { + /** + * Get favorite contacts + */ + async getFavoriteContacts(limit: number = 5): Promise> { + const result = await client.get(`/contacts?isFavorite=true&limit=${limit}`); + return result; + }, + + /** + * Get recent contacts (by updatedAt) + */ + async getRecentContacts(limit: number = 5): Promise> { + const result = await client.get(`/contacts?limit=${limit}`); + + if (result.error || !result.data) { + return result; + } + + // Sort by updatedAt and filter archived + const sorted = result.data + .filter((c) => !c.isArchived) + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) + .slice(0, limit); + + return { data: sorted, error: null }; + }, + + /** + * Get contacts with upcoming birthdays + */ + async getUpcomingBirthdays(days: number = 30): Promise> { + const result = await client.get('/contacts'); + + if (result.error || !result.data) { + return result; + } + + const today = new Date(); + const futureDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000); + + const withBirthdays = result.data.filter((c) => { + if (!c.birthday || c.isArchived) return false; + + const birthday = new Date(c.birthday); + // Set birthday to this year + birthday.setFullYear(today.getFullYear()); + + // If birthday already passed this year, check next year + if (birthday < today) { + birthday.setFullYear(today.getFullYear() + 1); + } + + return birthday >= today && birthday <= futureDate; + }); + + return { data: withBirthdays, error: null }; + }, + + /** + * Get contact count + */ + async getContactCount(): Promise> { + const result = await client.get('/contacts'); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + const active = result.data.filter((c) => !c.isArchived); + const favorites = active.filter((c) => c.isFavorite); + + return { data: { total: active.length, favorites: favorites.length }, error: null }; + }, + + /** + * Get display name for a contact + */ + getDisplayName(contact: Contact): string { + if (contact.displayName) return contact.displayName; + if (contact.firstName && contact.lastName) return `${contact.firstName} ${contact.lastName}`; + if (contact.firstName) return contact.firstName; + if (contact.lastName) return contact.lastName; + if (contact.email) return contact.email; + return 'Unknown'; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/api/services/index.ts b/apps/manacore/apps/web/src/lib/api/services/index.ts new file mode 100644 index 000000000..b7f99db74 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/index.ts @@ -0,0 +1,11 @@ +/** + * Dashboard API Services + * + * Re-exports all app-specific services for the dashboard. + */ + +export { todoService, type Task, type Project } from './todo'; +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'; diff --git a/apps/manacore/apps/web/src/lib/api/services/todo.ts b/apps/manacore/apps/web/src/lib/api/services/todo.ts new file mode 100644 index 000000000..c3ee6936c --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/todo.ts @@ -0,0 +1,95 @@ +/** + * Todo API Service + * + * Fetches tasks from the Todo backend for dashboard widgets. + */ + +import { createApiClient, type ApiResult } from '../base-client'; + +// Backend URL - falls back to localhost for development +const TODO_API_URL = import.meta.env.PUBLIC_TODO_API_URL || 'http://localhost:3017'; + +const client = createApiClient(TODO_API_URL); + +/** + * Task entity from Todo backend + */ +export interface Task { + id: string; + title: string; + description?: string; + projectId?: string | null; + priority: 'low' | 'medium' | 'high' | 'urgent'; + dueDate?: string; + dueTime?: string; + isCompleted: boolean; + status: 'pending' | 'in_progress' | 'completed' | 'cancelled'; + labelIds: string[]; + createdAt: string; + updatedAt: string; +} + +/** + * Project entity from Todo backend + */ +export interface Project { + id: string; + name: string; + color: string; + icon?: string; + order: number; + isArchived: boolean; +} + +/** + * Todo service for dashboard widgets + */ +export const todoService = { + /** + * Get today's tasks + */ + async getTodayTasks(): Promise> { + return client.get('/tasks/today'); + }, + + /** + * Get upcoming tasks for the next N days + */ + async getUpcomingTasks(days: number = 7): Promise> { + return client.get(`/tasks/upcoming?days=${days}`); + }, + + /** + * Get inbox tasks (unassigned to project) + */ + async getInboxTasks(): Promise> { + return client.get('/tasks/inbox'); + }, + + /** + * Get all projects + */ + async getProjects(): Promise> { + return client.get('/projects'); + }, + + /** + * Get task count summary + */ + async getTaskCounts(): Promise> { + // This might need a dedicated endpoint - for now we fetch and count + const todayResult = await this.getTodayTasks(); + const upcomingResult = await this.getUpcomingTasks(); + + if (todayResult.error || upcomingResult.error) { + return { data: null, error: todayResult.error || upcomingResult.error }; + } + + const today = todayResult.data?.length || 0; + const upcoming = upcomingResult.data?.length || 0; + const overdue = + todayResult.data?.filter((t) => t.dueDate && new Date(t.dueDate) < new Date()).length || 0; + + return { data: { today, upcoming, overdue }, error: null }; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/api/services/zitare.ts b/apps/manacore/apps/web/src/lib/api/services/zitare.ts new file mode 100644 index 000000000..56bf27874 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/services/zitare.ts @@ -0,0 +1,92 @@ +/** + * Zitare API Service + * + * Fetches favorite quotes from the Zitare backend for dashboard widgets. + */ + +import { createApiClient, type ApiResult } from '../base-client'; + +// Backend URL - falls back to localhost for development +const ZITARE_API_URL = import.meta.env.PUBLIC_ZITARE_API_URL || 'http://localhost:3007'; + +const client = createApiClient(ZITARE_API_URL); + +/** + * Favorite entity from Zitare backend + */ +export interface Favorite { + id: string; + userId: string; + quoteId: string; + createdAt: string; +} + +/** + * Quote data (may need to be enriched from a quotes API) + */ +export interface Quote { + id: string; + text: string; + author?: string; + source?: string; + category?: string; +} + +/** + * List entity from Zitare backend + */ +export interface QuoteList { + id: string; + userId: string; + name: string; + description?: string; + quoteIds: string[]; + createdAt: string; + updatedAt: string; +} + +/** + * Zitare service for dashboard widgets + */ +export const zitareService = { + /** + * Get user's favorite quotes + */ + async getFavorites(): Promise> { + return client.get('/favorites'); + }, + + /** + * Get a random favorite quote + */ + async getRandomFavorite(): Promise> { + const result = await this.getFavorites(); + + if (result.error || !result.data || result.data.length === 0) { + return { data: null, error: result.error || 'No favorites found' }; + } + + const randomIndex = Math.floor(Math.random() * result.data.length); + return { data: result.data[randomIndex], error: null }; + }, + + /** + * Get user's quote lists + */ + async getLists(): Promise> { + return client.get('/lists'); + }, + + /** + * Get favorite count + */ + async getFavoriteCount(): Promise> { + const result = await this.getFavorites(); + + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + + return { data: result.data.length, error: null }; + }, +}; diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/DashboardGrid.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/DashboardGrid.svelte new file mode 100644 index 000000000..6bc176d2a --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/DashboardGrid.svelte @@ -0,0 +1,59 @@ + + +
+ {#each items as widget (widget.id)} +
+ +
+ {/each} +
+ +{#if items.length === 0} +
+
📊
+

Keine Widgets

+

Klicke auf "Anpassen" um Widgets hinzuzufügen.

+
+{/if} diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte new file mode 100644 index 000000000..340c3e2d0 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte @@ -0,0 +1,138 @@ + + +
+ + + {#if dashboardStore.isEditing} +
+ +
+ + + + + + + + + {meta?.icon} {$_(widget.title)} +
+ + +
+ {#each sizes as size} + + {/each} +
+ + +
+ +
+
+ {/if} + + +
+ {#if WidgetComponent} + + {:else} +

Unknown widget type: {widget.type}

+ {/if} +
+
+
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetError.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetError.svelte new file mode 100644 index 000000000..5f5e733c7 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetError.svelte @@ -0,0 +1,52 @@ + + +
+
⚠️
+

+ {error || $_('dashboard.widget_error')} +

+ +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetSkeleton.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetSkeleton.svelte new file mode 100644 index 000000000..2489a2891 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetSkeleton.svelte @@ -0,0 +1,37 @@ + + +
+ {#if showHeader} +
+
+
+
+ {/if} + + {#each Array(lines) as _, i} +
+ {#if i === 0} +
+ {:else if i === lines - 1} +
+ {:else} +
+ {/if} +
+ {/each} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CalendarEventsWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CalendarEventsWidget.svelte new file mode 100644 index 000000000..b4ab205d1 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CalendarEventsWidget.svelte @@ -0,0 +1,130 @@ + + +
+
+

+ 🗓️ + {$_('dashboard.widgets.calendar.title')} +

+ {#if data.length > 0} + + {data.length} + + {/if} +
+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if data.length === 0} +
+
📅
+

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

+
+ {:else} +
+ {#each displayedEvents as event} +
+
+
+

{event.title}

+

{formatEventTime(event)}

+ {#if event.location} +

📍 {event.location}

+ {/if} +
+
+ {/each} + + {#if remainingCount > 0} + + +{remainingCount} weitere + + {/if} +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ChatRecentWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ChatRecentWidget.svelte new file mode 100644 index 000000000..8fbf50212 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ChatRecentWidget.svelte @@ -0,0 +1,117 @@ + + +
+
+

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

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

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

+ + Chat starten + +
+ {:else} + + {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ContactsFavoritesWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ContactsFavoritesWidget.svelte new file mode 100644 index 000000000..a70cbe526 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ContactsFavoritesWidget.svelte @@ -0,0 +1,133 @@ + + +
+
+

+ =e + {$_('dashboard.widgets.contacts.title')} +

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

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

+ + {$_('dashboard.widgets.contacts.add_favorites')} + +
+ {:else} + + + + {$_('dashboard.widgets.contacts.view_all')} + + {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CreditsWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CreditsWidget.svelte new file mode 100644 index 000000000..465b030b3 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CreditsWidget.svelte @@ -0,0 +1,68 @@ + + +
+

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

+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if data} +
+
+ {$_('dashboard.widgets.credits.available')} + {formatCredits(data.balance)} +
+
+ {$_('dashboard.widgets.credits.free_today')} + {data.freeCreditsRemaining}/{data.dailyFreeCredits} +
+ + {$_('dashboard.widgets.credits.manage')} + +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/QuickActionsWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/QuickActionsWidget.svelte new file mode 100644 index 000000000..c8aaeeefc --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/QuickActionsWidget.svelte @@ -0,0 +1,49 @@ + + +
+

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

+ + +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte new file mode 100644 index 000000000..17d7949b2 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte @@ -0,0 +1,140 @@ + + +
+
+

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

+ {#if data.length > 0} + + {data.length} + + {/if} +
+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if data.length === 0} +
+
🎉
+

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

+
+ {:else} +
+ {#each displayedTasks as task} +
+
+ {#if task.isCompleted} + + + + {/if} +
+
+

+ {task.title} +

+ {#if task.dueTime} +

{task.dueTime}

+ {/if} +
+ {#if !task.isCompleted && task.priority !== 'low'} + + {/if} +
+ {/each} + + {#if remainingCount > 0} + + {$_('dashboard.widgets.tasks_today.view_all', { values: { count: remainingCount } })} + + {/if} +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksUpcomingWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksUpcomingWidget.svelte new file mode 100644 index 000000000..2e65c4462 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksUpcomingWidget.svelte @@ -0,0 +1,116 @@ + + +
+
+

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

+ {#if data.length > 0} + + {data.length} + + {/if} +
+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if data.length === 0} +
+
📭
+

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

+
+ {:else} +
+ {#each displayedTasks as task} +
+
+

{task.title}

+ {#if task.dueDate} +

{formatDate(task.dueDate)}

+ {/if} +
+
+ {/each} + + {#if remainingCount > 0} + + +{remainingCount} weitere + + {/if} +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TransactionsWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TransactionsWidget.svelte new file mode 100644 index 000000000..9a8b1fc07 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TransactionsWidget.svelte @@ -0,0 +1,98 @@ + + +
+
+

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

+ + {$_('common.view_all')} → + +
+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if data.length === 0} +

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

+ {:else} +
+ {#each data as tx} +
+
+ {getTransactionIcon(tx.type)} +
+

{tx.description || tx.type}

+

+ {new Date(tx.createdAt).toLocaleDateString('de-DE')} +

+
+
+ + {tx.amount > 0 ? '+' : ''}{formatCredits(tx.amount)} + +
+ {/each} +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ZitareQuoteWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ZitareQuoteWidget.svelte new file mode 100644 index 000000000..1ad0c29bc --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ZitareQuoteWidget.svelte @@ -0,0 +1,115 @@ + + +
+
+

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

+ {#if state === 'success' && data} + + {/if} +
+ + {#if state === 'loading'} + + {:else if state === 'error'} + + {:else if !data} +
+
(
+

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

+ + {$_('dashboard.widgets.zitare.explore')} + +
+ {:else} +
+ +
+

+ "{data.quoteId}" +

+
+ + + + + {$_('dashboard.widgets.zitare.view_all')} + +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/config/default-dashboard.ts b/apps/manacore/apps/web/src/lib/config/default-dashboard.ts new file mode 100644 index 000000000..daa6da39a --- /dev/null +++ b/apps/manacore/apps/web/src/lib/config/default-dashboard.ts @@ -0,0 +1,73 @@ +/** + * Default Dashboard Configuration + * + * Provides the initial widget layout for new users. + */ + +import type { DashboardConfig } from '$lib/types/dashboard'; + +/** + * Default dashboard configuration with 6 widgets in a 2-column layout + */ +export const DEFAULT_DASHBOARD_CONFIG: DashboardConfig = { + widgets: [ + // Row 0: Credits and Tasks Today + { + id: 'credits-1', + type: 'credits', + title: 'dashboard.widgets.credits.title', + size: 'medium', + position: { x: 0, y: 0 }, + visible: true, + }, + { + id: 'tasks-today-1', + type: 'tasks-today', + title: 'dashboard.widgets.tasks_today.title', + size: 'medium', + position: { x: 6, y: 0 }, + visible: true, + }, + // Row 1: Calendar and Quick Actions + { + id: 'calendar-events-1', + type: 'calendar-events', + title: 'dashboard.widgets.calendar.title', + size: 'medium', + position: { x: 0, y: 1 }, + visible: true, + }, + { + id: 'quick-actions-1', + type: 'quick-actions', + title: 'dashboard.widgets.quick_actions.title', + size: 'medium', + position: { x: 6, y: 1 }, + visible: true, + }, + // Row 2: Chat and Contacts + { + id: 'chat-recent-1', + type: 'chat-recent', + title: 'dashboard.widgets.chat.title', + size: 'medium', + position: { x: 0, y: 2 }, + visible: true, + }, + { + id: 'contacts-favorites-1', + type: 'contacts-favorites', + title: 'dashboard.widgets.contacts.title', + size: 'medium', + position: { x: 6, y: 2 }, + visible: true, + }, + ], + gridColumns: 12, + lastModified: new Date().toISOString(), +}; + +/** + * LocalStorage key for dashboard configuration + */ +export const DASHBOARD_STORAGE_KEY = 'manacore-dashboard-config'; 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 a54c5dce7..d62a2f79f 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/de.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/de.json @@ -6,6 +6,75 @@ "back": "Zurück", "loading": "Lädt..." }, + "dashboard": { + "title": "Dashboard", + "welcome": "Willkommen zurück", + "customize": "Anpassen", + "done": "Fertig", + "no_data": "Keine Daten verfügbar", + "retry": "Erneut versuchen", + "widget_error": "Fehler beim Laden", + "remove_widget": "Entfernen", + "widgets": { + "credits": { + "title": "Credits", + "description": "Dein Kontostand", + "available": "Verfügbar", + "free_today": "Gratis heute", + "manage": "Verwalten" + }, + "quick_actions": { + "title": "Schnellzugriff", + "description": "Schnelle Aktionen" + }, + "transactions": { + "title": "Transaktionen", + "description": "Letzte Aktivitäten", + "empty": "Keine Transaktionen" + }, + "tasks_today": { + "title": "Aufgaben heute", + "description": "Deine heutigen Aufgaben", + "empty": "Keine Aufgaben für heute" + }, + "tasks_upcoming": { + "title": "Kommende Aufgaben", + "description": "Die nächsten 7 Tage", + "empty": "Keine kommenden Aufgaben" + }, + "calendar": { + "title": "Kalender", + "description": "Anstehende Termine", + "empty": "Keine Termine" + }, + "chat": { + "title": "Chat", + "description": "Letzte Unterhaltungen", + "empty": "Keine Unterhaltungen" + }, + "contacts": { + "title": "Kontakte", + "description": "Deine Favoriten", + "empty": "Keine Favoriten", + "add_favorites": "Favoriten hinzufügen", + "view_all": "Alle anzeigen" + }, + "zitare": { + "title": "Inspiration", + "description": "Zitat des Tages", + "empty": "Keine Favoriten", + "explore": "Zitate entdecken", + "refresh": "Neues Zitat", + "view_all": "Alle Zitate" + } + } + }, + "credits": { + "available": "Verfügbar", + "daily_free": "Gratis heute", + "total_spent": "Verbraucht", + "manage": "Credits verwalten" + }, "app_slider": { "title": "Teil des Mana Ökosystems", "memoro_desc": "KI-gestützte Sprachnotizen", 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 1bb480851..5a6ca3930 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/en.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/en.json @@ -6,6 +6,75 @@ "back": "Back", "loading": "Loading..." }, + "dashboard": { + "title": "Dashboard", + "welcome": "Welcome back", + "customize": "Customize", + "done": "Done", + "no_data": "No data available", + "retry": "Try again", + "widget_error": "Failed to load", + "remove_widget": "Remove", + "widgets": { + "credits": { + "title": "Credits", + "description": "Your balance", + "available": "Available", + "free_today": "Free today", + "manage": "Manage" + }, + "quick_actions": { + "title": "Quick Actions", + "description": "Quick access" + }, + "transactions": { + "title": "Transactions", + "description": "Recent activity", + "empty": "No transactions" + }, + "tasks_today": { + "title": "Tasks Today", + "description": "Your tasks for today", + "empty": "No tasks for today" + }, + "tasks_upcoming": { + "title": "Upcoming Tasks", + "description": "Next 7 days", + "empty": "No upcoming tasks" + }, + "calendar": { + "title": "Calendar", + "description": "Upcoming events", + "empty": "No events" + }, + "chat": { + "title": "Chat", + "description": "Recent conversations", + "empty": "No conversations" + }, + "contacts": { + "title": "Contacts", + "description": "Your favorites", + "empty": "No favorites", + "add_favorites": "Add favorites", + "view_all": "View all" + }, + "zitare": { + "title": "Inspiration", + "description": "Quote of the day", + "empty": "No favorites", + "explore": "Explore quotes", + "refresh": "New quote", + "view_all": "All quotes" + } + } + }, + "credits": { + "available": "Available", + "daily_free": "Free today", + "total_spent": "Spent", + "manage": "Manage credits" + }, "app_slider": { "title": "Part of the Mana Ecosystem", "memoro_desc": "AI-powered voice notes", diff --git a/apps/manacore/apps/web/src/lib/stores/dashboard.svelte.ts b/apps/manacore/apps/web/src/lib/stores/dashboard.svelte.ts new file mode 100644 index 000000000..89e27b6ad --- /dev/null +++ b/apps/manacore/apps/web/src/lib/stores/dashboard.svelte.ts @@ -0,0 +1,223 @@ +/** + * Dashboard Store - Manages dashboard configuration using Svelte 5 runes + * + * Handles widget layout, edit mode, and persistence to localStorage. + */ + +import { browser } from '$app/environment'; +import type { DashboardConfig, WidgetConfig, WidgetSize, WidgetType } from '$lib/types/dashboard'; +import { DEFAULT_DASHBOARD_CONFIG, DASHBOARD_STORAGE_KEY } from '$lib/config/default-dashboard'; +import { getWidgetMeta } from '$lib/types/dashboard'; + +// State +let config = $state(structuredClone(DEFAULT_DASHBOARD_CONFIG)); +let isEditing = $state(false); +let initialized = $state(false); + +/** + * Dashboard store with Svelte 5 runes + */ +export const dashboardStore = { + // Getters + get config() { + return config; + }, + get widgets() { + return config.widgets.filter((w) => w.visible); + }, + get allWidgets() { + return config.widgets; + }, + get isEditing() { + return isEditing; + }, + get initialized() { + return initialized; + }, + + /** + * Initialize dashboard from localStorage + */ + initialize() { + if (!browser || initialized) return; + + try { + const stored = localStorage.getItem(DASHBOARD_STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored) as DashboardConfig; + // Validate structure + if (parsed.widgets && Array.isArray(parsed.widgets)) { + config = parsed; + } + } + } catch (e) { + console.error('Failed to load dashboard config:', e); + } + + initialized = true; + }, + + /** + * Persist current config to localStorage + */ + persist() { + if (!browser) return; + + try { + config.lastModified = new Date().toISOString(); + localStorage.setItem(DASHBOARD_STORAGE_KEY, JSON.stringify(config)); + } catch (e) { + console.error('Failed to save dashboard config:', e); + } + }, + + /** + * Enter edit mode + */ + startEditing() { + isEditing = true; + }, + + /** + * Exit edit mode and save changes + */ + stopEditing() { + isEditing = false; + this.persist(); + }, + + /** + * Toggle edit mode + */ + toggleEditing() { + if (isEditing) { + this.stopEditing(); + } else { + this.startEditing(); + } + }, + + /** + * Update widgets array (called during drag-and-drop) + */ + updateWidgets(widgets: WidgetConfig[]) { + config.widgets = widgets; + }, + + /** + * Update a single widget's position + */ + updateWidgetPosition(widgetId: string, position: { x: number; y: number }) { + const widget = config.widgets.find((w) => w.id === widgetId); + if (widget) { + widget.position = position; + } + }, + + /** + * Update a widget's size + */ + updateWidgetSize(widgetId: string, size: WidgetSize) { + const widget = config.widgets.find((w) => w.id === widgetId); + if (widget) { + widget.size = size; + this.persist(); + } + }, + + /** + * Toggle widget visibility + */ + toggleWidgetVisibility(widgetId: string) { + const widget = config.widgets.find((w) => w.id === widgetId); + if (widget) { + widget.visible = !widget.visible; + this.persist(); + } + }, + + /** + * Add a new widget + */ + addWidget(type: WidgetType) { + const meta = getWidgetMeta(type); + if (!meta) return; + + // Check if multiple instances are allowed + if (!meta.allowMultiple) { + const existing = config.widgets.find((w) => w.type === type); + if (existing) { + // Just make it visible + existing.visible = true; + this.persist(); + return; + } + } + + // Generate unique ID + const existingCount = config.widgets.filter((w) => w.type === type).length; + const id = `${type}-${existingCount + 1}`; + + // Find the next available row + const maxY = Math.max(...config.widgets.map((w) => w.position.y), -1); + + const newWidget: WidgetConfig = { + id, + type, + title: meta.nameKey, + size: meta.defaultSize, + position: { x: 0, y: maxY + 1 }, + visible: true, + }; + + config.widgets = [...config.widgets, newWidget]; + this.persist(); + }, + + /** + * Remove a widget + */ + removeWidget(widgetId: string) { + config.widgets = config.widgets.filter((w) => w.id !== widgetId); + this.persist(); + }, + + /** + * Reset to default configuration + */ + resetToDefault() { + config = structuredClone(DEFAULT_DASHBOARD_CONFIG); + this.persist(); + }, + + /** + * Check if a widget type is currently active (visible) + */ + isWidgetActive(type: WidgetType): boolean { + return config.widgets.some((w) => w.type === type && w.visible); + }, + + /** + * Get available widgets that can be added + */ + getAvailableWidgets(): WidgetType[] { + const activeTypes = new Set(config.widgets.filter((w) => w.visible).map((w) => w.type)); + return ( + [ + 'credits', + 'quick-actions', + 'transactions', + 'tasks-today', + 'tasks-upcoming', + 'calendar-events', + 'chat-recent', + 'contacts-favorites', + 'zitare-quote', + ] as WidgetType[] + ).filter((type) => { + const meta = getWidgetMeta(type); + // Allow if multiple instances allowed or not currently active + return meta?.allowMultiple || !activeTypes.has(type); + }); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/types/dashboard.ts b/apps/manacore/apps/web/src/lib/types/dashboard.ts new file mode 100644 index 000000000..a34a2f26b --- /dev/null +++ b/apps/manacore/apps/web/src/lib/types/dashboard.ts @@ -0,0 +1,208 @@ +/** + * Dashboard Widget System Types + * + * Defines the type system for the configurable cross-app dashboard. + */ + +/** + * Available widget types - each represents a different data source + */ +export type WidgetType = + | 'credits' // Credits balance from mana-core-auth + | 'quick-actions' // Quick action links + | 'transactions' // Recent credit transactions + | 'tasks-today' // Todo API: today's tasks + | 'tasks-upcoming' // Todo API: upcoming 7 days + | 'calendar-events' // Calendar API: upcoming events + | 'chat-recent' // Chat API: recent conversations + | 'contacts-favorites' // Contacts API: favorite contacts + | 'zitare-quote'; // Zitare API: favorite quotes + +/** + * Widget size - maps to CSS Grid columns + * - small: 4 cols (1/3 width on desktop) + * - medium: 6 cols (1/2 width on desktop) + * - large: 8 cols (2/3 width on desktop) + * - full: 12 cols (full width) + */ +export type WidgetSize = 'small' | 'medium' | 'large' | 'full'; + +/** + * Individual widget instance configuration + */ +export interface WidgetConfig { + /** Unique instance ID (e.g., "tasks-today-1") */ + id: string; + /** Widget type */ + type: WidgetType; + /** i18n key for title (e.g., "dashboard.widgets.credits.title") */ + title: string; + /** Grid size */ + size: WidgetSize; + /** Grid position */ + position: { + /** Column (0-11 for 12-col grid) */ + x: number; + /** Row index */ + y: number; + }; + /** Show/hide toggle */ + visible: boolean; + /** Widget-specific settings */ + settings?: Record; +} + +/** + * Complete dashboard state + */ +export interface DashboardConfig { + /** List of widget configurations */ + widgets: WidgetConfig[]; + /** Number of grid columns (default: 12) */ + gridColumns: number; + /** ISO timestamp of last modification */ + lastModified: string; +} + +/** + * Widget loading state + */ +export type WidgetLoadState = 'idle' | 'loading' | 'success' | 'error'; + +/** + * Generic widget data state wrapper + */ +export interface WidgetDataState { + /** Current loading state */ + state: WidgetLoadState; + /** Fetched data (null if not loaded or error) */ + data: T | null; + /** Error message (null if no error) */ + error: string | null; + /** Number of retry attempts made */ + retryCount: number; + /** ISO timestamp of last fetch attempt */ + lastFetch: string | null; +} + +/** + * Widget metadata for the widget picker + */ +export interface WidgetMeta { + type: WidgetType; + /** i18n key for display name */ + nameKey: string; + /** i18n key for description */ + descriptionKey: string; + /** Icon identifier */ + icon: string; + /** Default size for new instances */ + defaultSize: WidgetSize; + /** Whether multiple instances are allowed */ + allowMultiple: boolean; + /** Required backend (for status display) */ + requiredBackend?: 'todo' | 'calendar' | 'chat' | 'contacts' | 'zitare' | 'mana-core-auth'; +} + +/** + * Widget registry - metadata for all available widgets + */ +export const WIDGET_REGISTRY: WidgetMeta[] = [ + { + type: 'credits', + nameKey: 'dashboard.widgets.credits.title', + descriptionKey: 'dashboard.widgets.credits.description', + icon: '💰', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'mana-core-auth', + }, + { + type: 'quick-actions', + nameKey: 'dashboard.widgets.quick_actions.title', + descriptionKey: 'dashboard.widgets.quick_actions.description', + icon: '⚡', + defaultSize: 'medium', + allowMultiple: false, + }, + { + type: 'transactions', + nameKey: 'dashboard.widgets.transactions.title', + descriptionKey: 'dashboard.widgets.transactions.description', + icon: '📊', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'mana-core-auth', + }, + { + type: 'tasks-today', + nameKey: 'dashboard.widgets.tasks_today.title', + descriptionKey: 'dashboard.widgets.tasks_today.description', + icon: '✅', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'todo', + }, + { + type: 'tasks-upcoming', + nameKey: 'dashboard.widgets.tasks_upcoming.title', + descriptionKey: 'dashboard.widgets.tasks_upcoming.description', + icon: '📅', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'todo', + }, + { + type: 'calendar-events', + nameKey: 'dashboard.widgets.calendar.title', + descriptionKey: 'dashboard.widgets.calendar.description', + icon: '🗓️', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'calendar', + }, + { + type: 'chat-recent', + nameKey: 'dashboard.widgets.chat.title', + descriptionKey: 'dashboard.widgets.chat.description', + icon: '💬', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'chat', + }, + { + type: 'contacts-favorites', + nameKey: 'dashboard.widgets.contacts.title', + descriptionKey: 'dashboard.widgets.contacts.description', + icon: '👥', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'contacts', + }, + { + type: 'zitare-quote', + nameKey: 'dashboard.widgets.zitare.title', + descriptionKey: 'dashboard.widgets.zitare.description', + icon: '💡', + defaultSize: 'medium', + allowMultiple: false, + requiredBackend: 'zitare', + }, +]; + +/** + * Get widget metadata by type + */ +export function getWidgetMeta(type: WidgetType): WidgetMeta | undefined { + return WIDGET_REGISTRY.find((w) => w.type === type); +} + +/** + * Size to Tailwind class mapping + */ +export const WIDGET_SIZE_CLASSES: Record = { + small: 'col-span-12 sm:col-span-6 lg:col-span-4', + medium: 'col-span-12 lg:col-span-6', + large: 'col-span-12 lg:col-span-8', + full: 'col-span-12', +}; diff --git a/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte index d1a92822d..d50db0b41 100644 --- a/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte @@ -1,188 +1,60 @@
- - - - - -
- - -

Schnellzugriff

- -
- - - -
-

Letzte Transaktionen

- - Alle → - -
- {#if loadingCredits} -
- {#each [1, 2, 3] as _} -
-
-
-
-
-
-
- {/each} -
- {:else if recentTransactions.length === 0} -

Noch keine Transaktionen vorhanden.

+
+ +
+ +