mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
feat(manacore): add Mukke, Presi, Context dashboard widgets
All apps now have dashboard widgets: - Mukke: music library stats, recent/favorite songs, formatDuration() - Presi: presentation decks, recent decks, deck counts - Context: spaces, recent documents, token balance Added 3 widget types to registry (16 total), 3 API services, i18n translations (DE + EN), and 17 new tests (120 total). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
14b6a8934a
commit
effa57fd61
12 changed files with 734 additions and 3 deletions
127
apps/manacore/apps/web/src/lib/api/services/context.test.ts
Normal file
127
apps/manacore/apps/web/src/lib/api/services/context.test.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock $app/environment
|
||||
vi.mock('$app/environment', () => ({
|
||||
browser: true,
|
||||
}));
|
||||
|
||||
// Mock auth store
|
||||
vi.mock('$lib/stores/auth.svelte', () => ({
|
||||
authStore: {
|
||||
getAccessToken: vi.fn().mockResolvedValue('test-token'),
|
||||
},
|
||||
}));
|
||||
|
||||
import { contextService, type ContextSpace, type ContextDocument } from './context';
|
||||
|
||||
describe('contextService', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getSpaces', () => {
|
||||
it('should fetch spaces', async () => {
|
||||
const spaces: ContextSpace[] = [
|
||||
{
|
||||
id: 's-1',
|
||||
userId: 'u-1',
|
||||
name: 'Research',
|
||||
pinned: true,
|
||||
prefix: 'R',
|
||||
createdAt: '2026-01-01',
|
||||
updatedAt: '2026-01-01',
|
||||
},
|
||||
];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(spaces),
|
||||
});
|
||||
|
||||
const result = await contextService.getSpaces();
|
||||
|
||||
expect(result.data).toEqual(spaces);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/spaces'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecentDocuments', () => {
|
||||
it('should fetch recent documents with limit', async () => {
|
||||
const docs: ContextDocument[] = [
|
||||
{
|
||||
id: 'd-1',
|
||||
userId: 'u-1',
|
||||
spaceId: 's-1',
|
||||
title: 'Notes',
|
||||
type: 'text',
|
||||
shortId: 'RT1',
|
||||
pinned: false,
|
||||
createdAt: '2026-01-01',
|
||||
updatedAt: '2026-03-01',
|
||||
},
|
||||
];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(docs),
|
||||
});
|
||||
|
||||
const result = await contextService.getRecentDocuments(3);
|
||||
|
||||
expect(result.data).toEqual(docs);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/documents/recent?limit=3'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTokenBalance', () => {
|
||||
it('should fetch token balance', async () => {
|
||||
const balance = { tokenBalance: 5000, monthlyFreeTokens: 10000 };
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(balance),
|
||||
});
|
||||
|
||||
const result = await contextService.getTokenBalance();
|
||||
|
||||
expect(result.data).toEqual(balance);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/tokens/balance'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCounts', () => {
|
||||
it('should return space count', async () => {
|
||||
const spaces = [
|
||||
{ id: 's-1', name: 'A' },
|
||||
{ id: 's-2', name: 'B' },
|
||||
];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(spaces),
|
||||
});
|
||||
|
||||
const result = await contextService.getCounts();
|
||||
|
||||
expect(result.data).toEqual({ spaces: 2, documents: 0 });
|
||||
});
|
||||
|
||||
it('should return error on failure', async () => {
|
||||
global.fetch = vi.fn().mockRejectedValue(new Error('Failed to fetch'));
|
||||
|
||||
const result = await contextService.getCounts();
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
113
apps/manacore/apps/web/src/lib/api/services/context.ts
Normal file
113
apps/manacore/apps/web/src/lib/api/services/context.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Context API Service
|
||||
*
|
||||
* Fetches documents and spaces from the Context backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Get Context API URL dynamically at runtime
|
||||
function getContextApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_CONTEXT_API_URL__?: string })
|
||||
.__PUBLIC_CONTEXT_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api/v1`;
|
||||
}
|
||||
}
|
||||
return 'http://localhost:3020/api/v1';
|
||||
}
|
||||
|
||||
// Lazy-initialized client
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getContextApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Space entity from Context backend
|
||||
*/
|
||||
export interface ContextSpace {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
pinned: boolean;
|
||||
prefix: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Document entity from Context backend
|
||||
*/
|
||||
export interface ContextDocument {
|
||||
id: string;
|
||||
userId: string;
|
||||
spaceId: string;
|
||||
title: string;
|
||||
content?: string;
|
||||
type: 'text' | 'context' | 'prompt';
|
||||
shortId: string;
|
||||
pinned: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token balance from Context backend
|
||||
*/
|
||||
export interface TokenBalance {
|
||||
tokenBalance: number;
|
||||
monthlyFreeTokens: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context service for dashboard widgets
|
||||
*/
|
||||
export const contextService = {
|
||||
/**
|
||||
* Get user's spaces
|
||||
*/
|
||||
async getSpaces(): Promise<ApiResult<ContextSpace[]>> {
|
||||
return getClient().get<ContextSpace[]>('/spaces');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get recent documents
|
||||
*/
|
||||
async getRecentDocuments(limit = 5): Promise<ApiResult<ContextDocument[]>> {
|
||||
return getClient().get<ContextDocument[]>(`/documents/recent?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get token balance
|
||||
*/
|
||||
async getTokenBalance(): Promise<ApiResult<TokenBalance>> {
|
||||
return getClient().get<TokenBalance>('/tokens/balance');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get document and space counts
|
||||
*/
|
||||
async getCounts(): Promise<ApiResult<{ spaces: number; documents: number }>> {
|
||||
const spacesResult = await this.getSpaces();
|
||||
|
||||
if (spacesResult.error || !spacesResult.data) {
|
||||
return { data: null, error: spacesResult.error };
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
spaces: spacesResult.data.length,
|
||||
documents: 0, // Would need a separate count endpoint
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -13,3 +13,11 @@ export { pictureService, type GeneratedImage, type GenerationStats } from './pic
|
|||
export { manadeckService, type Deck, type Card, type LearningProgress } from './manadeck';
|
||||
export { clockService, type Timer, type Alarm, type ClockStats } from './clock';
|
||||
export { storageService, type StorageFile, type StorageStats } from './storage';
|
||||
export { mukkeService, type Song, type MukkeStats } from './mukke';
|
||||
export { presiService, type PresiDeck, type PresiStats } from './presi';
|
||||
export {
|
||||
contextService,
|
||||
type ContextSpace,
|
||||
type ContextDocument,
|
||||
type TokenBalance,
|
||||
} from './context';
|
||||
|
|
|
|||
92
apps/manacore/apps/web/src/lib/api/services/mukke.test.ts
Normal file
92
apps/manacore/apps/web/src/lib/api/services/mukke.test.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock $app/environment
|
||||
vi.mock('$app/environment', () => ({
|
||||
browser: true,
|
||||
}));
|
||||
|
||||
// Mock auth store
|
||||
vi.mock('$lib/stores/auth.svelte', () => ({
|
||||
authStore: {
|
||||
getAccessToken: vi.fn().mockResolvedValue('test-token'),
|
||||
},
|
||||
}));
|
||||
|
||||
import { mukkeService } from './mukke';
|
||||
|
||||
describe('mukkeService', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('formatDuration', () => {
|
||||
it('should format 0 seconds', () => {
|
||||
expect(mukkeService.formatDuration(0)).toBe('0:00');
|
||||
});
|
||||
|
||||
it('should format seconds only', () => {
|
||||
expect(mukkeService.formatDuration(45)).toBe('0:45');
|
||||
});
|
||||
|
||||
it('should format minutes and seconds', () => {
|
||||
expect(mukkeService.formatDuration(185)).toBe('3:05');
|
||||
});
|
||||
|
||||
it('should format hours', () => {
|
||||
expect(mukkeService.formatDuration(3661)).toBe('1:01:01');
|
||||
});
|
||||
|
||||
it('should pad seconds with zero', () => {
|
||||
expect(mukkeService.formatDuration(60)).toBe('1:00');
|
||||
});
|
||||
|
||||
it('should handle negative values', () => {
|
||||
expect(mukkeService.formatDuration(-10)).toBe('0:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should fetch library stats', async () => {
|
||||
const mockStats = {
|
||||
totalSongs: 42,
|
||||
totalPlaylists: 5,
|
||||
totalProjects: 3,
|
||||
favoriteCount: 10,
|
||||
totalPlayTime: 7200,
|
||||
};
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockStats),
|
||||
});
|
||||
|
||||
const result = await mukkeService.getStats();
|
||||
|
||||
expect(result.data).toEqual(mockStats);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/library/stats'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecentSongs', () => {
|
||||
it('should fetch recent songs with default limit', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([{ id: 's-1', title: 'Song 1' }]),
|
||||
});
|
||||
|
||||
const result = await mukkeService.getRecentSongs();
|
||||
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/songs?limit=5'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
98
apps/manacore/apps/web/src/lib/api/services/mukke.ts
Normal file
98
apps/manacore/apps/web/src/lib/api/services/mukke.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Mukke API Service
|
||||
*
|
||||
* Fetches music library stats from the Mukke backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Get Mukke API URL dynamically at runtime
|
||||
function getMukkeApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MUKKE_API_URL__?: string })
|
||||
.__PUBLIC_MUKKE_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}`;
|
||||
}
|
||||
}
|
||||
return 'http://localhost:3010';
|
||||
}
|
||||
|
||||
// Lazy-initialized client
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getMukkeApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Song entity from Mukke backend
|
||||
*/
|
||||
export interface Song {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
artist?: string;
|
||||
album?: string;
|
||||
genre?: string;
|
||||
duration?: number;
|
||||
favorite: boolean;
|
||||
playCount: number;
|
||||
lastPlayedAt?: string;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Music library statistics
|
||||
*/
|
||||
export interface MukkeStats {
|
||||
totalSongs: number;
|
||||
totalPlaylists: number;
|
||||
totalProjects: number;
|
||||
favoriteCount: number;
|
||||
totalPlayTime: number; // In seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Mukke service for dashboard widgets
|
||||
*/
|
||||
export const mukkeService = {
|
||||
/**
|
||||
* Get library statistics
|
||||
*/
|
||||
async getStats(): Promise<ApiResult<MukkeStats>> {
|
||||
return getClient().get<MukkeStats>('/library/stats');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get recent songs
|
||||
*/
|
||||
async getRecentSongs(limit = 5): Promise<ApiResult<Song[]>> {
|
||||
return getClient().get<Song[]>(`/songs?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get favorite songs
|
||||
*/
|
||||
async getFavoriteSongs(limit = 5): Promise<ApiResult<Song[]>> {
|
||||
return getClient().get<Song[]>(`/songs?favorite=true&limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Format duration for display (seconds → MM:SS or HH:MM:SS)
|
||||
*/
|
||||
formatDuration(seconds: number): string {
|
||||
if (seconds <= 0) return '0:00';
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
if (h > 0) {
|
||||
return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||||
}
|
||||
return `${m}:${String(s).padStart(2, '0')}`;
|
||||
},
|
||||
};
|
||||
100
apps/manacore/apps/web/src/lib/api/services/presi.test.ts
Normal file
100
apps/manacore/apps/web/src/lib/api/services/presi.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock $app/environment
|
||||
vi.mock('$app/environment', () => ({
|
||||
browser: true,
|
||||
}));
|
||||
|
||||
// Mock auth store
|
||||
vi.mock('$lib/stores/auth.svelte', () => ({
|
||||
authStore: {
|
||||
getAccessToken: vi.fn().mockResolvedValue('test-token'),
|
||||
},
|
||||
}));
|
||||
|
||||
import { presiService, type PresiDeck } from './presi';
|
||||
|
||||
const mockDeck = (overrides: Partial<PresiDeck> = {}): PresiDeck => ({
|
||||
id: 'd-1',
|
||||
userId: 'u-1',
|
||||
title: 'Test Deck',
|
||||
isPublic: false,
|
||||
createdAt: '2026-01-01',
|
||||
updatedAt: '2026-03-01',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('presiService', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getDecks', () => {
|
||||
it('should fetch decks', async () => {
|
||||
const decks = [mockDeck()];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(decks),
|
||||
});
|
||||
|
||||
const result = await presiService.getDecks();
|
||||
|
||||
expect(result.data).toEqual(decks);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/decks'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecentDecks', () => {
|
||||
it('should sort by updatedAt and limit', async () => {
|
||||
const decks = [
|
||||
mockDeck({ id: 'd-1', updatedAt: '2026-01-01' }),
|
||||
mockDeck({ id: 'd-2', updatedAt: '2026-03-01' }),
|
||||
mockDeck({ id: 'd-3', updatedAt: '2026-02-01' }),
|
||||
];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(decks),
|
||||
});
|
||||
|
||||
const result = await presiService.getRecentDecks(2);
|
||||
|
||||
expect(result.data).toHaveLength(2);
|
||||
expect(result.data![0].id).toBe('d-2');
|
||||
expect(result.data![1].id).toBe('d-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeckCount', () => {
|
||||
it('should count total and public decks', async () => {
|
||||
const decks = [
|
||||
mockDeck({ isPublic: true }),
|
||||
mockDeck({ id: 'd-2', isPublic: false }),
|
||||
mockDeck({ id: 'd-3', isPublic: true }),
|
||||
];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(decks),
|
||||
});
|
||||
|
||||
const result = await presiService.getDeckCount();
|
||||
|
||||
expect(result.data).toEqual({ total: 3, public: 2 });
|
||||
});
|
||||
|
||||
it('should return error on failure', async () => {
|
||||
global.fetch = vi.fn().mockRejectedValue(new Error('Failed to fetch'));
|
||||
|
||||
const result = await presiService.getDeckCount();
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
101
apps/manacore/apps/web/src/lib/api/services/presi.ts
Normal file
101
apps/manacore/apps/web/src/lib/api/services/presi.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* Presi API Service
|
||||
*
|
||||
* Fetches presentation decks from the Presi backend for dashboard widgets.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createApiClient, type ApiResult } from '../base-client';
|
||||
|
||||
// Get Presi API URL dynamically at runtime
|
||||
function getPresiApiUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_PRESI_API_URL__?: string })
|
||||
.__PUBLIC_PRESI_API_URL__;
|
||||
if (injectedUrl) {
|
||||
return `${injectedUrl}/api`;
|
||||
}
|
||||
}
|
||||
return 'http://localhost:3008/api';
|
||||
}
|
||||
|
||||
// Lazy-initialized client
|
||||
let _client: ReturnType<typeof createApiClient> | null = null;
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
_client = createApiClient(getPresiApiUrl());
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deck entity from Presi backend
|
||||
*/
|
||||
export interface PresiDeck {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
themeId?: string;
|
||||
isPublic: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presi statistics
|
||||
*/
|
||||
export interface PresiStats {
|
||||
totalDecks: number;
|
||||
publicDecks: number;
|
||||
recentDecks: PresiDeck[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Presi service for dashboard widgets
|
||||
*/
|
||||
export const presiService = {
|
||||
/**
|
||||
* Get user's decks
|
||||
*/
|
||||
async getDecks(): Promise<ApiResult<PresiDeck[]>> {
|
||||
return getClient().get<PresiDeck[]>('/decks');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get recent decks
|
||||
*/
|
||||
async getRecentDecks(limit = 5): Promise<ApiResult<PresiDeck[]>> {
|
||||
const result = await this.getDecks();
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const sorted = result.data
|
||||
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
||||
.slice(0, limit);
|
||||
|
||||
return { data: sorted, error: null };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get deck count
|
||||
*/
|
||||
async getDeckCount(): Promise<ApiResult<{ total: number; public: number }>> {
|
||||
const result = await this.getDecks();
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
total: result.data.length,
|
||||
public: result.data.filter((d) => d.isPublic).length,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -101,6 +101,31 @@
|
|||
"recent": "Kürzlich",
|
||||
"empty": "Keine Dateien",
|
||||
"open": "Storage öffnen"
|
||||
},
|
||||
"mukke": {
|
||||
"title": "Musik",
|
||||
"description": "Deine Musikbibliothek",
|
||||
"songs": "Songs",
|
||||
"playlists": "Playlists",
|
||||
"favorites": "Favoriten",
|
||||
"empty": "Keine Songs",
|
||||
"open": "Mukke öffnen"
|
||||
},
|
||||
"presi": {
|
||||
"title": "Präsentationen",
|
||||
"description": "Deine Slide Decks",
|
||||
"decks": "Decks",
|
||||
"empty": "Keine Präsentationen",
|
||||
"create": "Deck erstellen",
|
||||
"open": "Presi öffnen"
|
||||
},
|
||||
"context": {
|
||||
"title": "Context",
|
||||
"description": "Deine Dokumente & Spaces",
|
||||
"spaces": "Spaces",
|
||||
"documents": "Dokumente",
|
||||
"empty": "Keine Dokumente",
|
||||
"open": "Context öffnen"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -101,6 +101,31 @@
|
|||
"recent": "Recent",
|
||||
"empty": "No files",
|
||||
"open": "Open Storage"
|
||||
},
|
||||
"mukke": {
|
||||
"title": "Music",
|
||||
"description": "Your music library",
|
||||
"songs": "Songs",
|
||||
"playlists": "Playlists",
|
||||
"favorites": "Favorites",
|
||||
"empty": "No songs",
|
||||
"open": "Open Mukke"
|
||||
},
|
||||
"presi": {
|
||||
"title": "Presentations",
|
||||
"description": "Your slide decks",
|
||||
"decks": "Decks",
|
||||
"empty": "No presentations",
|
||||
"create": "Create deck",
|
||||
"open": "Open Presi"
|
||||
},
|
||||
"context": {
|
||||
"title": "Context",
|
||||
"description": "Your documents & spaces",
|
||||
"spaces": "Spaces",
|
||||
"documents": "Documents",
|
||||
"empty": "No documents",
|
||||
"open": "Open Context"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -213,6 +213,9 @@ export const dashboardStore = {
|
|||
'chat-recent',
|
||||
'contacts-favorites',
|
||||
'zitare-quote',
|
||||
'mukke-library',
|
||||
'presi-decks',
|
||||
'context-docs',
|
||||
] as WidgetType[]
|
||||
).filter((type) => {
|
||||
const meta = getWidgetMeta(type);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import {
|
|||
} from './dashboard';
|
||||
|
||||
describe('WIDGET_REGISTRY', () => {
|
||||
it('should contain 13 widget definitions', () => {
|
||||
expect(WIDGET_REGISTRY).toHaveLength(13);
|
||||
it('should contain 16 widget definitions', () => {
|
||||
expect(WIDGET_REGISTRY).toHaveLength(16);
|
||||
});
|
||||
|
||||
it('should have unique types for all widgets', () => {
|
||||
|
|
@ -47,6 +47,9 @@ describe('WIDGET_REGISTRY', () => {
|
|||
'manadeck',
|
||||
'clock',
|
||||
'storage',
|
||||
'mukke',
|
||||
'presi',
|
||||
'context',
|
||||
'mana-core-auth',
|
||||
undefined,
|
||||
];
|
||||
|
|
@ -70,6 +73,9 @@ describe('WIDGET_REGISTRY', () => {
|
|||
expect(types).toContain('manadeck-progress');
|
||||
expect(types).toContain('clock-timers');
|
||||
expect(types).toContain('storage-usage');
|
||||
expect(types).toContain('mukke-library');
|
||||
expect(types).toContain('presi-decks');
|
||||
expect(types).toContain('context-docs');
|
||||
});
|
||||
|
||||
it('should have i18n-style name keys', () => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ export type WidgetType =
|
|||
| 'picture-recent' // Picture API: recent generations
|
||||
| 'manadeck-progress' // ManaDeck API: learning progress
|
||||
| 'clock-timers' // Clock: active timers and alarms
|
||||
| 'storage-usage'; // Storage: file storage stats
|
||||
| 'storage-usage' // Storage: file storage stats
|
||||
| 'mukke-library' // Mukke: music library stats
|
||||
| 'presi-decks' // Presi: recent presentations
|
||||
| 'context-docs'; // Context: recent documents & spaces
|
||||
|
||||
/**
|
||||
* Widget size - maps to CSS Grid columns
|
||||
|
|
@ -115,6 +118,9 @@ export interface WidgetMeta {
|
|||
| 'manadeck'
|
||||
| 'clock'
|
||||
| 'storage'
|
||||
| 'mukke'
|
||||
| 'presi'
|
||||
| 'context'
|
||||
| 'mana-core-auth';
|
||||
}
|
||||
|
||||
|
|
@ -238,6 +244,33 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [
|
|||
allowMultiple: false,
|
||||
requiredBackend: 'storage',
|
||||
},
|
||||
{
|
||||
type: 'mukke-library',
|
||||
nameKey: 'dashboard.widgets.mukke.title',
|
||||
descriptionKey: 'dashboard.widgets.mukke.description',
|
||||
icon: '🎵',
|
||||
defaultSize: 'medium',
|
||||
allowMultiple: false,
|
||||
requiredBackend: 'mukke',
|
||||
},
|
||||
{
|
||||
type: 'presi-decks',
|
||||
nameKey: 'dashboard.widgets.presi.title',
|
||||
descriptionKey: 'dashboard.widgets.presi.description',
|
||||
icon: '📊',
|
||||
defaultSize: 'medium',
|
||||
allowMultiple: false,
|
||||
requiredBackend: 'presi',
|
||||
},
|
||||
{
|
||||
type: 'context-docs',
|
||||
nameKey: 'dashboard.widgets.context.title',
|
||||
descriptionKey: 'dashboard.widgets.context.description',
|
||||
icon: '📝',
|
||||
defaultSize: 'medium',
|
||||
allowMultiple: false,
|
||||
requiredBackend: 'context',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue