feat: integrate shared PageHeader and ProfilePage across all web apps

- Add backHref prop to PageHeader component for back navigation
- Integrate PageHeader in Chat app (archive, documents, spaces, templates)
- Integrate PageHeader in Picture app (board, generate, profile, tags, upload)
- Integrate PageHeader in Manacore app (dashboard, organizations, teams)
- Integrate PageHeader in Presi app (home, profile)
- Integrate PageHeader in Zitare app (authors, lists)
- Update Picture, Manadeck, Presi profiles to use shared ProfilePage
- Create new profile pages for Manacore and Zitare using shared ProfilePage
- Add profile navigation links to Manacore and Zitare
- Add Mana subscription pages to Presi and Zitare
- Fix shared-profile-ui tsconfig.json (remove invalid extends)
- Add @manacore/shared-profile-ui dependency to all web apps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-30 00:06:29 +01:00
parent c85cd4556c
commit 9432a73a1f
62 changed files with 1803 additions and 1152 deletions

View file

@ -11,7 +11,7 @@ until pg_isready -h ${DB_HOST:-postgres} -p ${DB_PORT:-5432} -U ${DB_USER:-chat}
done
echo "PostgreSQL is up!"
cd /app/chat/backend
cd /app/apps/chat/apps/backend
# Run schema push (for development) or migrations (for production)
if [ "$NODE_ENV" = "production" ] && [ -d "src/db/migrations/meta" ]; then

View file

@ -3,6 +3,7 @@
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { PageHeader } from '@manacore/shared-ui';
import type { Conversation } from '@chat/types';
let conversations = $state<Conversation[]>([]);
@ -55,13 +56,7 @@
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-4xl mx-auto px-4">
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-foreground">Archiv</h1>
<p class="text-sm text-muted-foreground mt-1">
Deine archivierten Konversationen.
</p>
</div>
<PageHeader title="Archiv" description="Deine archivierten Konversationen." size="lg" />
<!-- Loading State -->
{#if isLoading}

View file

@ -3,6 +3,7 @@
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { documentService } from '$lib/services/document';
import { PageHeader } from '@manacore/shared-ui';
import type { DocumentWithConversation } from '@chat/types';
let documents = $state<DocumentWithConversation[]>([]);
@ -79,30 +80,29 @@
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-6xl mx-auto px-4">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Dokumente</h1>
<p class="text-sm text-muted-foreground mt-1">
Alle Dokumente aus deinen Konversationen im Dokumentmodus.
</p>
</div>
<button
onclick={loadDocuments}
class="p-2 text-muted-foreground hover:text-foreground
hover:bg-muted rounded-lg transition-colors"
aria-label="Aktualisieren"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
</button>
</div>
<PageHeader
title="Dokumente"
description="Alle Dokumente aus deinen Konversationen im Dokumentmodus."
size="lg"
>
{#snippet actions()}
<button
onclick={loadDocuments}
class="p-2 text-muted-foreground hover:text-foreground
hover:bg-muted rounded-lg transition-colors"
aria-label="Aktualisieren"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
</button>
{/snippet}
</PageHeader>
<!-- Loading State -->
{#if isLoading}

View file

@ -4,6 +4,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { spacesStore } from '$lib/stores/spaces.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { PageHeader } from '@manacore/shared-ui';
import SpaceCard from '$lib/components/spaces/SpaceCard.svelte';
import SpaceForm from '$lib/components/spaces/SpaceForm.svelte';
import type { Space } from '@chat/types';
@ -88,30 +89,29 @@
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-4xl mx-auto px-4">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Spaces</h1>
<p class="text-sm text-muted-foreground mt-1">
Organisiere deine Konversationen in kollaborativen Arbeitsbereichen.
</p>
</div>
<button
onclick={handleCreateNew}
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Neuen Space erstellen
</button>
</div>
<PageHeader
title="Spaces"
description="Organisiere deine Konversationen in kollaborativen Arbeitsbereichen."
size="lg"
>
{#snippet actions()}
<button
onclick={handleCreateNew}
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Neuen Space erstellen
</button>
{/snippet}
</PageHeader>
<!-- Loading State -->
{#if spacesStore.isLoading}

View file

@ -7,6 +7,7 @@
import { spaceService } from '$lib/services/space';
import { conversationService } from '$lib/services/conversation';
import { chatService } from '$lib/services/chat';
import { PageHeader } from '@manacore/shared-ui';
import type { Space, Conversation, AIModel } from '@chat/types';
const spaceId = $derived($page.params.id ?? '');
@ -97,29 +98,12 @@
{:else if space}
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-4xl mx-auto px-4">
<!-- Header -->
<div class="mb-6">
<div class="flex items-center gap-2 mb-2">
<a
href="/spaces"
class="p-1 text-muted-foreground hover:text-foreground rounded-lg hover:bg-muted transition-colors"
aria-label="Zurück zu Spaces"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
</a>
<h1 class="text-2xl font-bold text-foreground">{space.name}</h1>
</div>
{#if space.description}
<p class="text-sm text-muted-foreground">{space.description}</p>
{/if}
</div>
<PageHeader
title={space.name}
description={space.description}
backHref="/spaces"
size="lg"
/>
<!-- New Chat Section -->
<div

View file

@ -5,6 +5,7 @@
import { templatesStore } from '$lib/stores/templates.svelte';
import { conversationService } from '$lib/services/conversation';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { PageHeader } from '@manacore/shared-ui';
import TemplateCard from '$lib/components/templates/TemplateCard.svelte';
import TemplateForm from '$lib/components/templates/TemplateForm.svelte';
import type { Template } from '@chat/types';
@ -106,31 +107,29 @@
<div class="min-h-[calc(100vh-4rem)] bg-background py-8">
<div class="max-w-4xl mx-auto px-4">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Vorlagen</h1>
<p class="text-sm text-muted-foreground mt-1">
Erstelle Vorlagen mit benutzerdefinierten System-Prompts für verschiedene
KI-Verhaltensweisen.
</p>
</div>
<button
onclick={handleCreateNew}
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Neue Vorlage
</button>
</div>
<PageHeader
title="Vorlagen"
description="Erstelle Vorlagen mit benutzerdefinierten System-Prompts für verschiedene KI-Verhaltensweisen."
size="lg"
>
{#snippet actions()}
<button
onclick={handleCreateNew}
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Neue Vorlage
</button>
{/snippet}
</PageHeader>
<!-- Loading State -->
{#if templatesStore.isLoading}

View file

@ -41,9 +41,12 @@
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",
"@manacore/shared-feedback-ui": "workspace:*",
"@manacore/shared-config": "workspace:*",
"@manacore/shared-i18n": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-subscription-types": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-supabase": "workspace:*",

View file

@ -1,12 +1,15 @@
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
import { createServerClient } from '@supabase/ssr';
import { type Handle, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import type { Handle } from '@sveltejs/kit';
const supabase: Handle = async ({ event, resolve }) => {
/**
* Creates a Supabase client specific to this server request.
*/
/**
* Server hooks for ManaCore web app
*
* Note: Authentication is handled client-side via Mana Core Auth.
* Supabase is only used for database operations (not auth).
*/
export const handle: Handle = async ({ event, resolve }) => {
// Create Supabase client for database operations only
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
getAll: () => event.cookies.getAll(),
@ -18,70 +21,9 @@ const supabase: Handle = async ({ event, resolve }) => {
},
}) as any;
/**
* Unlike `supabase.auth.getSession()`, which returns the session _without_
* validating the JWT, this function also calls `getUser()` to validate the
* JWT before returning the session.
*/
event.locals.safeGetSession = async () => {
const {
data: { session },
} = await event.locals.supabase.auth.getSession();
if (!session) {
return { session: null, user: null };
}
const {
data: { user },
error,
} = await event.locals.supabase.auth.getUser();
if (error) {
// JWT validation has failed
return { session: null, user: null };
}
return { session, user };
};
return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range' || name === 'x-supabase-api-version';
},
});
};
const authGuard: Handle = async ({ event, resolve }) => {
const { session, user } = await event.locals.safeGetSession();
event.locals.session = session;
event.locals.user = user;
// Log user data
if (user) {
console.log('=== AUTH GUARD ===');
console.log('Path:', event.url.pathname);
console.log('User ID:', user.id);
console.log('User Email:', user.email);
console.log('User metadata:', JSON.stringify(user.user_metadata, null, 2));
console.log('Session expires at:', session?.expires_at);
console.log('==================\n');
}
// Protect (app) routes - redirect to login if not authenticated
if (!event.locals.session && event.url.pathname.startsWith('/(app)')) {
redirect(303, '/login');
}
// Redirect to dashboard if already logged in and trying to access auth pages
if (
event.locals.session &&
(event.url.pathname === '/login' || event.url.pathname === '/register')
) {
redirect(303, '/dashboard');
}
return resolve(event);
};
export const handle: Handle = sequence(supabase, authGuard);

View file

@ -0,0 +1,14 @@
/**
* Feedback Service Instance for ManaCore Web App
*/
import { createFeedbackService } from '@manacore/shared-feedback-service';
import { authStore } from '$lib/stores/authStore.svelte';
const MANA_AUTH_URL = 'http://localhost:3001'; // TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env
export const feedbackService = createFeedbackService({
apiUrl: MANA_AUTH_URL,
appId: 'manacore',
getAuthToken: async () => authStore.getAccessToken(),
});

View file

@ -1,82 +1,186 @@
import { createBrowserClient } from '@supabase/ssr';
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
/**
* Auth Store - Manages authentication state using Svelte 5 runes
* Uses Mana Core Auth
*/
// Create browser Supabase client
function getSupabaseClient() {
return createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
import { browser } from '$app/environment';
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
// Initialize Mana Core Auth only on the client side
// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available
const MANA_AUTH_URL = 'http://localhost:3001';
// Lazy initialization to avoid SSR issues with localStorage
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
function getAuthService() {
if (!browser) return null;
if (!_authService) {
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
_authService = auth.authService;
_tokenManager = auth.tokenManager;
}
return _authService;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
let initialized = $state(false);
export const authStore = {
// Getters
get user() {
return user;
},
get loading() {
return loading;
},
get isAuthenticated() {
return !!user;
},
get initialized() {
return initialized;
},
/**
* Initialize auth state from stored tokens
*/
async initialize() {
if (initialized) return;
const authService = getAuthService();
if (!authService) {
initialized = true;
loading = false;
return;
}
loading = true;
try {
const authenticated = await authService.isAuthenticated();
if (authenticated) {
const userData = await authService.getUserFromToken();
user = userData;
}
initialized = true;
} catch (error) {
console.error('Failed to initialize auth:', error);
user = null;
} finally {
loading = false;
}
},
/**
* Sign in with email and password
*/
async signIn(email: string, password: string) {
const supabase = getSupabaseClient();
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return {
success: false,
error: error.message,
};
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
return { success: true };
try {
const result = await authService.signIn(email, password);
if (!result.success) {
return { success: false, error: result.error || 'Login failed' };
}
// Get user data from token
const userData = await authService.getUserFromToken();
user = userData;
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Sign up with email and password
*/
async signUp(email: string, password: string) {
const supabase = getSupabaseClient();
const { data, error } = await supabase.auth.signUp({
email,
password,
});
if (error) {
return {
success: false,
error: error.message,
};
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
// Check if email confirmation is required
const needsVerification = !data.session;
try {
const result = await authService.signUp(email, password);
return {
success: true,
needsVerification,
};
},
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };
}
/**
* Send password reset email
*/
async forgotPassword(email: string) {
const supabase = getSupabaseClient();
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset-password`,
});
// Mana Core Auth requires separate login after signup
if (result.needsVerification) {
return { success: true, needsVerification: true };
}
if (error) {
return {
success: false,
error: error.message,
};
// Auto sign in after successful signup
const signInResult = await this.signIn(email, password);
return { ...signInResult, needsVerification: false };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage, needsVerification: false };
}
return { success: true };
},
/**
* Sign out
*/
async signOut() {
const supabase = getSupabaseClient();
await supabase.auth.signOut();
const authService = getAuthService();
if (!authService) {
user = null;
return;
}
try {
await authService.signOut();
user = null;
} catch (error) {
console.error('Sign out error:', error);
// Clear user even if sign out fails
user = null;
}
},
/**
* Send password reset email
*/
async forgotPassword(email: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.forgotPassword(email);
if (!result.success) {
return { success: false, error: result.error || 'Password reset failed' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Get access token for API calls
*/
async getAccessToken() {
const authService = getAuthService();
if (!authService) {
return null;
}
return await authService.getAppToken();
},
};

View file

@ -6,12 +6,13 @@
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem } from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/authStore.svelte';
import {
isSidebarMode as sidebarModeStore,
isNavCollapsed as collapsedStore,
} from '$lib/stores/navigation';
let { data, children }: { data: any; children: Snippet } = $props();
let { children }: { children: Snippet } = $props();
let loading = $state(true);
let isSidebarMode = $state(false);
@ -25,6 +26,7 @@
{ href: '/dashboard', label: 'Dashboard', icon: 'home' },
{ href: '/organizations', label: 'Organizations', icon: 'building' },
{ href: '/teams', label: 'Teams', icon: 'users' },
{ href: '/profile', label: 'Profil', icon: 'user' },
{ href: '/mana', label: 'Mana', icon: 'mana' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
];
@ -71,12 +73,13 @@
}
async function handleSignOut() {
await data.supabase.auth.signOut();
await authStore.signOut();
goto('/login');
}
$effect(() => {
if (!data.session) {
// Redirect to login if not authenticated (after initialization)
if (authStore.initialized && !authStore.isAuthenticated) {
goto('/login');
}
});
@ -102,7 +105,7 @@
<svelte:window onkeydown={handleKeydown} />
{#if loading}
{#if loading || authStore.loading}
<div class="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
<div class="text-center">
<div
@ -111,7 +114,7 @@
<p class="text-gray-500 dark:text-gray-400">Loading...</p>
</div>
</div>
{:else}
{:else if authStore.isAuthenticated}
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- Pill Navigation -->
<PillNavigation

View file

@ -1,46 +1,17 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals: { supabase, session } }) => {
if (!session) {
throw redirect(307, '/login');
}
console.log('=== DASHBOARD LOAD ===');
console.log('Session user ID:', session.user.id);
console.log('Session user email:', session.user.email);
// Fetch user profile
const { data: profile, error: profileError } = await supabase
.from('users')
.select('*')
.eq('auth_id', session.user.id)
.single();
console.log('Profile query error:', profileError);
console.log('Profile data:', JSON.stringify(profile, null, 2));
// Count organizations
const { count: organizationCount } = await supabase
.from('user_roles')
.select('organization_id', { count: 'exact', head: true })
.eq('user_id', session.user.id)
.not('organization_id', 'is', null);
console.log('Organization count:', organizationCount);
// Count teams
const { count: teamCount } = await supabase
.from('team_members')
.select('team_id', { count: 'exact', head: true })
.eq('user_id', session.user.id);
console.log('Team count:', teamCount);
console.log('======================\n');
/**
* Dashboard page server load
*
* Note: Auth is now handled client-side via Mana Core Auth.
* Data fetching will need to be done client-side with the auth token.
*/
export const load: PageServerLoad = async () => {
// Return empty data - auth is handled client-side
// TODO: Implement client-side data fetching with Mana Core Auth token
return {
profile,
organizationCount: organizationCount || 0,
teamCount: teamCount || 0,
profile: null,
organizationCount: 0,
teamCount: 0,
};
};

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Card } from '@manacore/shared-ui';
import { Card, PageHeader } from '@manacore/shared-ui';
let { data } = $props();
@ -26,12 +26,11 @@
</script>
<div>
<div class="mb-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Welcome back, {data.profile?.first_name || data.session?.user?.email}
</p>
</div>
<PageHeader
title="Dashboard"
description="Welcome back, {data.profile?.first_name || data.session?.user?.email}"
size="lg"
/>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{#each stats as stat}

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { FeedbackPage } from '@manacore/shared-feedback-ui';
import { feedbackService } from '$lib/api/feedback';
import { authStore } from '$lib/stores/authStore.svelte';
</script>
<FeedbackPage
{feedbackService}
appName="ManaCore"
currentUserId={authStore.user?.id}
/>

View file

@ -1,51 +1,15 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals: { supabase, session } }) => {
if (!session) {
throw redirect(307, '/login');
}
// Get all organizations where user has a role
const { data: userRoles } = await supabase
.from('user_roles')
.select('organization_id, role_id, roles(name)')
.eq('user_id', session.user.id)
.not('organization_id', 'is', null);
if (!userRoles || userRoles.length === 0) {
return { organizations: [] };
}
const orgIds = userRoles.map((ur) => ur.organization_id);
// Get organization details
const { data: organizations } = await supabase.from('organizations').select('*').in('id', orgIds);
if (!organizations) {
return { organizations: [] };
}
// Count teams for each organization
const orgsWithStats = await Promise.all(
organizations.map(async (org) => {
const { count: teamCount } = await supabase
.from('teams')
.select('id', { count: 'exact', head: true })
.eq('organization_id', org.id);
const userRole = userRoles.find((ur) => ur.organization_id === org.id);
const roleName = userRole?.roles as any;
return {
...org,
team_count: teamCount || 0,
user_role: roleName?.name || 'member',
};
})
);
/**
* Organizations page server load
*
* Note: Auth is now handled client-side via Mana Core Auth.
* Data fetching will need to be done client-side with the auth token.
*/
export const load: PageServerLoad = async () => {
// Return empty data - auth is handled client-side
// TODO: Implement client-side data fetching with Mana Core Auth token
return {
organizations: orgsWithStats,
organizations: [],
};
};

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Card, Button } from '@manacore/shared-ui';
import { Card, Button, PageHeader } from '@manacore/shared-ui';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
@ -23,20 +23,20 @@
</script>
<div>
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Organizations</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage your organizations and allocate credits
</p>
</div>
<Button variant="primary">
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Create Organization
</Button>
</div>
<PageHeader
title="Organizations"
description="Manage your organizations and allocate credits"
size="lg"
>
{#snippet actions()}
<Button variant="primary">
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Create Organization
</Button>
{/snippet}
</PageHeader>
{#if data.organizations && data.organizations.length > 0}
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { ProfilePage } from '@manacore/shared-profile-ui';
import type { UserProfile, ProfileActions } from '@manacore/shared-profile-ui';
import { authStore } from '$lib/stores/authStore.svelte';
import { goto } from '$app/navigation';
// Map auth store user to UserProfile
let userProfile = $derived<UserProfile>({
id: authStore.user?.id || '',
email: authStore.user?.email || '',
role: authStore.user?.role,
});
// Profile actions
const actions: ProfileActions = {
onLogout: async () => {
await authStore.signOut();
goto('/login');
},
onDeleteAccount: () => {
alert('Konto löschen ist noch nicht implementiert.');
},
};
</script>
<ProfilePage
user={userProfile}
appName="ManaCore"
{actions}
pageTitle="Profil"
accountInfoTitle="Konto-Informationen"
actionsTitle="Aktionen"
emailLabel="E-Mail"
nameLabel="Name"
memberSinceLabel="Mitglied seit"
lastLoginLabel="Letzter Login"
roleLabel="Rolle"
editProfileLabel="Profil bearbeiten"
changePasswordLabel="Passwort ändern"
logoutLabel="Abmelden"
deleteAccountLabel="Konto löschen"
deleteAccountWarning="Diese Aktion kann nicht rückgängig gemacht werden."
/>

View file

@ -1,50 +1,15 @@
import { redirect, fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals: { supabase, session } }) => {
if (!session) {
throw redirect(307, '/login');
}
const { data: profile } = await supabase
.from('users')
.select('*')
.eq('auth_id', session.user.id)
.single();
import type { PageServerLoad } from './$types';
/**
* Settings page server load
*
* Note: Auth is now handled client-side via Mana Core Auth.
* Data fetching will need to be done client-side with the auth token.
*/
export const load: PageServerLoad = async () => {
// Return empty data - auth is handled client-side
// TODO: Implement client-side data fetching with Mana Core Auth token
return {
profile,
profile: null,
};
};
export const actions: Actions = {
updateProfile: async ({ request, locals: { supabase, session } }) => {
if (!session) {
return fail(401, { error: 'Unauthorized' });
}
const formData = await request.formData();
const firstName = formData.get('firstName') as string;
const lastName = formData.get('lastName') as string;
const { error } = await supabase
.from('users')
.update({
first_name: firstName,
last_name: lastName,
updated_at: new Date().toISOString(),
})
.eq('auth_id', session.user.id);
if (error) {
console.error('Profile update error:', error);
return fail(500, {
error: 'Failed to update profile',
});
}
return {
success: true,
};
},
};

View file

@ -1,59 +1,15 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals: { supabase, session } }) => {
if (!session) {
throw redirect(307, '/login');
}
// Get all teams where user is a member
const { data: teamMembers } = await supabase
.from('team_members')
.select('team_id, allocated_credits, used_credits')
.eq('user_id', session.user.id);
if (!teamMembers || teamMembers.length === 0) {
return { teams: [] };
}
const teamIds = teamMembers.map((tm) => tm.team_id);
// Get team details with organization info
const { data: teams } = await supabase
.from('teams')
.select('*, organization:organizations(*)')
.in('id', teamIds);
if (!teams) {
return { teams: [] };
}
// Check if user is team admin and count members
const teamsWithStats = await Promise.all(
teams.map(async (team) => {
const { count: memberCount } = await supabase
.from('team_members')
.select('user_id', { count: 'exact', head: true })
.eq('team_id', team.id);
const { data: userRole } = await supabase
.from('user_roles')
.select('roles(name)')
.eq('user_id', session.user.id)
.eq('team_id', team.id)
.single();
const roleName = userRole?.roles as any;
return {
...team,
member_count: memberCount || 0,
user_role: roleName?.name || 'member',
};
})
);
/**
* Teams page server load
*
* Note: Auth is now handled client-side via Mana Core Auth.
* Data fetching will need to be done client-side with the auth token.
*/
export const load: PageServerLoad = async () => {
// Return empty data - auth is handled client-side
// TODO: Implement client-side data fetching with Mana Core Auth token
return {
teams: teamsWithStats,
teams: [],
};
};

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Card, Button } from '@manacore/shared-ui';
import { Card, Button, PageHeader } from '@manacore/shared-ui';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
@ -19,20 +19,20 @@
</script>
<div>
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Teams</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage your teams and collaborate with members
</p>
</div>
<Button variant="primary">
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Create Team
</Button>
</div>
<PageHeader
title="Teams"
description="Manage your teams and collaborate with members"
size="lg"
>
{#snippet actions()}
<Button variant="primary">
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Create Team
</Button>
{/snippet}
</PageHeader>
{#if data.teams && data.teams.length > 0}
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">

View file

@ -1,8 +1,7 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals: { session }, cookies }) => {
export const load: LayoutServerLoad = async ({ cookies }) => {
return {
session,
cookies: cookies.getAll(),
};
};

View file

@ -1,29 +1,20 @@
<script lang="ts">
import '../app.css';
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/authStore.svelte';
let { data, children } = $props();
let { children } = $props();
onMount(() => {
onMount(async () => {
// Initialize theme
const cleanupTheme = theme.initialize();
// Setup auth state change listener
const {
data: { subscription },
} = data.supabase.auth.onAuthStateChange(async (event, session) => {
if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
invalidate('supabase:auth');
} else if (event === 'SIGNED_OUT') {
invalidate('supabase:auth');
}
});
// Initialize auth
await authStore.initialize();
return () => {
cleanupTheme();
subscription.unsubscribe();
};
});
</script>

View file

@ -11,4 +11,34 @@ export default defineConfig({
port: 4173,
strictPort: true,
},
ssr: {
noExternal: [
'@manacore/shared-icons',
'@manacore/shared-ui',
'@manacore/shared-tailwind',
'@manacore/shared-theme',
'@manacore/shared-theme-ui',
'@manacore/shared-feedback-ui',
'@manacore/shared-feedback-service',
'@manacore/shared-feedback-types',
'@manacore/shared-auth',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
],
},
optimizeDeps: {
exclude: [
'@manacore/shared-icons',
'@manacore/shared-ui',
'@manacore/shared-tailwind',
'@manacore/shared-theme',
'@manacore/shared-theme-ui',
'@manacore/shared-feedback-ui',
'@manacore/shared-feedback-service',
'@manacore/shared-feedback-types',
'@manacore/shared-auth',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
],
},
});

View file

@ -29,9 +29,12 @@
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",
"@manacore/shared-feedback-ui": "workspace:*",
"@manacore/shared-config": "workspace:*",
"@manacore/shared-i18n": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-subscription-types": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",

View file

@ -0,0 +1,15 @@
/**
* Feedback Service Instance for ManaDeck Web App
*/
import { createFeedbackService } from '@manacore/shared-feedback-service';
import { authService } from '$lib/auth';
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
export const feedbackService = createFeedbackService({
apiUrl: MANA_AUTH_URL,
appId: 'manadeck',
getAuthToken: async () => authService.getAppToken(),
});

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { FeedbackPage } from '@manacore/shared-feedback-ui';
import { feedbackService } from '$lib/api/feedback';
import { authStore } from '$lib/stores/authStore.svelte';
</script>
<FeedbackPage
{feedbackService}
appName="ManaDeck"
currentUserId={authStore.user?.id}
/>

View file

@ -1,88 +1,43 @@
<script lang="ts">
import { ProfilePage } from '@manacore/shared-profile-ui';
import type { UserProfile, ProfileActions } from '@manacore/shared-profile-ui';
import { authStore } from '$lib/stores/authStore.svelte';
import { Card, Button } from '@manacore/shared-ui';
import { goto } from '$app/navigation';
let credits = $state<number | null>(null);
let loadingCredits = $state(false);
// Map auth store user to UserProfile
let userProfile = $derived<UserProfile>({
id: authStore.user?.id || '',
email: authStore.user?.email || '',
role: authStore.user?.role,
});
async function loadCredits() {
loadingCredits = true;
try {
const { authService } = await import('$lib/auth');
const balance = await authService.getUserCredits();
credits = balance?.credits ?? null;
} catch (error) {
console.error('Failed to load credits:', error);
} finally {
loadingCredits = false;
}
}
// Profile actions
const actions: ProfileActions = {
onLogout: async () => {
await authStore.signOut();
goto('/login');
},
onDeleteAccount: () => {
alert('Konto löschen ist noch nicht implementiert.');
},
};
</script>
<svelte:head>
<title>Profile - Manadeck</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-3xl font-bold">Profile</h1>
<p class="text-muted-foreground mt-1">Manage your account and settings</p>
</div>
<!-- User Info -->
<Card>
<div class="space-y-4">
<div class="flex items-center space-x-4">
<div
class="h-16 w-16 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-2xl font-bold"
>
{authStore.user?.email?.[0].toUpperCase() || 'U'}
</div>
<div>
<h2 class="text-xl font-semibold">{authStore.user?.email || 'User'}</h2>
<p class="text-sm text-muted-foreground">
Member since {new Date().toLocaleDateString()}
</p>
</div>
</div>
</div>
</Card>
<!-- Credits -->
<Card>
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold mb-1">Mana Credits</h3>
<p class="text-sm text-muted-foreground">Use credits for AI features</p>
</div>
<div class="text-right">
{#if loadingCredits}
<div class="text-2xl font-bold">...</div>
{:else if credits !== null}
<div class="text-2xl font-bold">{credits}</div>
{:else}
<Button size="sm" onclick={loadCredits}>Load Balance</Button>
{/if}
</div>
</div>
</Card>
<!-- Settings -->
<Card>
<h3 class="text-lg font-semibold mb-4">Settings</h3>
<div class="space-y-3 text-sm text-muted-foreground">
<div class="flex items-center justify-between py-2 border-b border-border">
<span>Theme</span>
<span>System</span>
</div>
<div class="flex items-center justify-between py-2 border-b border-border">
<span>Notifications</span>
<span>Enabled</span>
</div>
<div class="flex items-center justify-between py-2">
<span>Study Reminders</span>
<span>Daily</span>
</div>
</div>
</Card>
</div>
<ProfilePage
user={userProfile}
appName="Manadeck"
{actions}
pageTitle="Profil"
accountInfoTitle="Konto-Informationen"
actionsTitle="Aktionen"
emailLabel="E-Mail"
nameLabel="Name"
memberSinceLabel="Mitglied seit"
lastLoginLabel="Letzter Login"
roleLabel="Rolle"
editProfileLabel="Profil bearbeiten"
changePasswordLabel="Passwort ändern"
logoutLabel="Abmelden"
deleteAccountLabel="Konto löschen"
deleteAccountWarning="Diese Aktion kann nicht rückgängig gemacht werden."
/>

View file

@ -19,8 +19,11 @@
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",
"@manacore/shared-feedback-ui": "workspace:*",
"@manacore/shared-i18n": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-subscription-types": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",

View file

@ -0,0 +1,15 @@
/**
* Feedback Service Instance for Picture Web App
*/
import { createFeedbackService } from '@manacore/shared-feedback-service';
import { authStore } from '$lib/stores/auth.svelte';
import { env } from '$env/dynamic/public';
const MANA_AUTH_URL = env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
export const feedbackService = createFeedbackService({
apiUrl: MANA_AUTH_URL,
appId: 'picture',
getAuthToken: async () => authStore.getAccessToken(),
});

View file

@ -14,6 +14,7 @@
removeBoardFromList,
} from '$lib/stores/boards';
import { getBoards, deleteBoard, duplicateBoard } from '$lib/api/boards';
import { PageHeader } from '@manacore/shared-ui';
import Button from '$lib/components/ui/Button.svelte';
import Modal from '$lib/components/ui/Modal.svelte';
import { showToast } from '$lib/stores/toast';
@ -163,19 +164,18 @@
</svelte:head>
<div class="min-h-screen px-4 py-8">
<!-- Header -->
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Moodboards</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
Erstelle und organisiere deine Bilder auf einem Canvas
</p>
</div>
<Button onclick={() => showCreateBoardModal.set(true)}>
<Plus size={20} class="mr-2" />
Neues Board
</Button>
</div>
<PageHeader
title="Moodboards"
description="Erstelle und organisiere deine Bilder auf einem Canvas"
size="lg"
>
{#snippet actions()}
<Button onclick={() => showCreateBoardModal.set(true)}>
<Plus size={20} class="mr-2" />
Neues Board
</Button>
{/snippet}
</PageHeader>
<!-- Loading State -->
{#if $isLoadingBoards}

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { FeedbackPage } from '@manacore/shared-feedback-ui';
import { feedbackService } from '$lib/api/feedback';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<FeedbackPage
{feedbackService}
appName="Picture"
currentUserId={authStore.user?.id}
/>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { PageHeader } from '@manacore/shared-ui';
import GenerateForm from '$lib/components/generate/GenerateForm.svelte';
import { CheckCircle } from '@manacore/shared-icons';
</script>
@ -8,12 +9,11 @@
</svelte:head>
<div class="container mx-auto px-4 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Generate Image</h1>
<p class="mt-2 text-gray-600">
Create stunning AI-generated images from your text descriptions
</p>
</div>
<PageHeader
title="Generate Image"
description="Create stunning AI-generated images from your text descriptions"
size="lg"
/>
<div class="mx-auto max-w-3xl">
<GenerateForm />

View file

@ -1,183 +1,50 @@
<script lang="ts">
import { ProfilePage } from '@manacore/shared-profile-ui';
import type { UserProfile, ProfileActions } from '@manacore/shared-profile-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import Card from '$lib/components/ui/Card.svelte';
import Button from '$lib/components/ui/Button.svelte';
import ThemePicker from '$lib/components/settings/ThemePicker.svelte';
let isLoggingOut = $state(false);
// Map auth store user to UserProfile
let userProfile = $derived<UserProfile>({
id: authStore.user?.id || '',
email: authStore.user?.email || '',
role: authStore.user?.role,
});
async function handleLogout() {
isLoggingOut = true;
try {
// Profile actions
const actions: ProfileActions = {
onLogout: async () => {
await authStore.signOut();
goto('/');
} catch (error) {
console.error('Error logging out:', error);
alert('Failed to log out');
} finally {
isLoggingOut = false;
}
}
function formatDate(dateString: string | undefined) {
if (!dateString) return 'Unknown';
const date = new Date(dateString);
return new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric',
}).format(date);
}
},
onDeleteAccount: () => {
alert('Konto löschen ist noch nicht implementiert.');
},
};
</script>
<svelte:head>
<title>Profile - Picture</title>
</svelte:head>
<ProfilePage
user={userProfile}
appName="Picture"
{actions}
pageTitle="Profil"
accountInfoTitle="Konto-Informationen"
actionsTitle="Aktionen"
emailLabel="E-Mail"
nameLabel="Name"
memberSinceLabel="Mitglied seit"
lastLoginLabel="Letzter Login"
roleLabel="Rolle"
editProfileLabel="Profil bearbeiten"
changePasswordLabel="Passwort ändern"
logoutLabel="Abmelden"
deleteAccountLabel="Konto löschen"
deleteAccountWarning="Diese Aktion kann nicht rückgängig gemacht werden."
/>
<div class="container mx-auto px-4 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Profile</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">Manage your account settings</p>
</div>
<div class="mx-auto max-w-3xl space-y-6">
<!-- Account Information -->
<Card>
<div class="p-6">
<h2 class="mb-6 text-xl font-semibold text-gray-900 dark:text-gray-100">
Account Information
</h2>
<div class="space-y-4">
<!-- Email -->
<div
class="flex items-center justify-between border-b border-gray-200 pb-4 dark:border-gray-700"
>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Email</h3>
<p class="mt-1 text-gray-900 dark:text-gray-100">{authStore.user?.email || 'Not available'}</p>
</div>
{#if authStore.user?.email}
<span class="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800">
Verified
</span>
{:else}
<span
class="rounded-full bg-yellow-100 px-3 py-1 text-xs font-medium text-yellow-800"
>
Not verified
</span>
{/if}
</div>
<!-- User ID -->
<div class="flex items-center justify-between border-b border-gray-200 pb-4">
<div>
<h3 class="text-sm font-medium text-gray-500">User ID</h3>
<p class="mt-1 font-mono text-sm text-gray-900">{authStore.user?.id || 'Not available'}</p>
</div>
</div>
<!-- Created At -->
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-gray-500">Member Since</h3>
<p class="mt-1 text-gray-900">-</p>
</div>
</div>
</div>
</div>
</Card>
<!-- Theme Settings -->
<div>
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-gray-100">Appearance</h2>
<ThemePicker />
</div>
<!-- Settings -->
<Card>
<div class="p-6">
<h2 class="mb-6 text-xl font-semibold text-gray-900 dark:text-gray-100">Settings</h2>
<div class="space-y-4">
<!-- Language -->
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">Language</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Select your preferred language
</p>
</div>
<select
class="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
>
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
</div>
</div>
</Card>
<!-- Statistics -->
<Card>
<div class="p-6">
<h2 class="mb-6 text-xl font-semibold text-gray-900">Statistics</h2>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3">
<div class="rounded-lg bg-blue-50 p-4">
<p class="text-sm font-medium text-blue-600">Total Images</p>
<p class="mt-2 text-2xl font-bold text-blue-900">-</p>
</div>
<div class="rounded-lg bg-green-50 p-4">
<p class="text-sm font-medium text-green-600">Generated</p>
<p class="mt-2 text-2xl font-bold text-green-900">-</p>
</div>
<div class="rounded-lg bg-purple-50 p-4">
<p class="text-sm font-medium text-purple-600">Archived</p>
<p class="mt-2 text-2xl font-bold text-purple-900">-</p>
</div>
</div>
<p class="mt-4 text-sm text-gray-500">Statistics coming soon...</p>
</div>
</Card>
<!-- Danger Zone -->
<Card>
<div class="p-6">
<h2 class="mb-6 text-xl font-semibold text-red-600">Danger Zone</h2>
<div class="space-y-4">
<div
class="flex items-center justify-between rounded-lg border border-red-200 bg-red-50 p-4"
>
<div>
<h3 class="font-medium text-red-900">Log Out</h3>
<p class="mt-1 text-sm text-red-700">Sign out of your account</p>
</div>
<Button variant="danger" onclick={handleLogout} loading={isLoggingOut}>
{isLoggingOut ? 'Logging out...' : 'Log Out'}
</Button>
</div>
<div
class="flex items-center justify-between rounded-lg border border-red-200 bg-red-50 p-4"
>
<div>
<h3 class="font-medium text-red-900">Delete Account</h3>
<p class="mt-1 text-sm text-red-700">Permanently delete your account and all data</p>
</div>
<Button
variant="danger"
onclick={() => alert('Account deletion is not yet implemented')}
>
Delete Account
</Button>
</div>
</div>
</div>
</Card>
</div>
<!-- Theme Settings (additional section) -->
<div class="mx-auto max-w-xl px-4 pb-8">
<h2 class="mb-4 text-lg font-semibold text-foreground">Erscheinungsbild</h2>
<ThemePicker />
</div>

View file

@ -3,6 +3,7 @@
import { tags, isLoadingTags } from '$lib/stores/tags';
import { getAllTags, createTag, updateTag, deleteTag, type Tag } from '$lib/api/tags';
import { showToast } from '$lib/stores/toast';
import { PageHeader } from '@manacore/shared-ui';
import { Plus, Tag as TagIcon, PencilSimple, Trash } from '@manacore/shared-icons';
let showCreateModal = $state(false);
@ -105,22 +106,21 @@
<div class="min-h-screen px-4 py-8">
<div class="mx-auto max-w-4xl">
<!-- Header -->
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Tag-Verwaltung</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
Verwalte deine Tags für eine bessere Organisation deiner Bilder
</p>
</div>
<button
onclick={() => (showCreateModal = true)}
class="flex items-center gap-2 rounded-2xl bg-blue-600/90 px-6 py-3 text-sm font-medium text-white backdrop-blur-xl transition-all hover:bg-blue-700/90 dark:bg-blue-500/90 dark:hover:bg-blue-600/90"
>
<Plus size={20} />
Neuer Tag
</button>
</div>
<PageHeader
title="Tag-Verwaltung"
description="Verwalte deine Tags für eine bessere Organisation deiner Bilder"
size="lg"
>
{#snippet actions()}
<button
onclick={() => (showCreateModal = true)}
class="flex items-center gap-2 rounded-2xl bg-blue-600/90 px-6 py-3 text-sm font-medium text-white backdrop-blur-xl transition-all hover:bg-blue-700/90 dark:bg-blue-500/90 dark:hover:bg-blue-600/90"
>
<Plus size={20} />
Neuer Tag
</button>
{/snippet}
</PageHeader>
<!-- Tags Grid -->
{#if $isLoadingTags}

View file

@ -3,6 +3,7 @@
import { goto } from '$app/navigation';
import { uploadMultipleImages, type UploadProgress } from '$lib/api/upload';
import { showToast } from '$lib/stores/toast';
import { PageHeader } from '@manacore/shared-ui';
import DropZone from '$lib/components/upload/DropZone.svelte';
import { images } from '$lib/stores/images';
import { Check, Image, CloudArrowUp, CheckCircle } from '@manacore/shared-icons';
@ -58,13 +59,11 @@
<div class="min-h-screen px-4 py-8">
<div class="mx-auto max-w-5xl">
<!-- Header -->
<div class="mb-8">
<h1 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">Bilder hochladen</h1>
<p class="text-gray-600 dark:text-gray-400">
Lade deine eigenen Bilder hoch und verwalte sie in deiner Galerie
</p>
</div>
<PageHeader
title="Bilder hochladen"
description="Lade deine eigenen Bilder hoch und verwalte sie in deiner Galerie"
size="lg"
/>
<!-- Upload Success Banner -->
{#if successCount > 0 && !uploading}

View file

@ -33,7 +33,11 @@
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",
"@manacore/shared-feedback-ui": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",

View file

@ -0,0 +1,15 @@
/**
* Feedback Service Instance for Presi Web App
*/
import { createFeedbackService } from '@manacore/shared-feedback-service';
import { auth } from '$lib/stores/auth.svelte';
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
export const feedbackService = createFeedbackService({
apiUrl: MANA_AUTH_URL,
appId: 'presi',
getAuthToken: async () => auth.getAccessToken(),
});

View file

@ -44,6 +44,7 @@
const navItems: PillNavItem[] = [
{ href: '/', label: 'Decks', icon: 'document' },
{ href: '/profile', label: 'Profil', icon: 'user' },
{ href: '/mana', label: 'Mana', icon: 'sparkles' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
];

View file

@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { decksStore } from '$lib/stores/decks.svelte';
import { PageHeader } from '@manacore/shared-ui';
import { Plus, Presentation, Trash2, MoreVertical, Clock, Layers } from 'lucide-svelte';
let showCreateModal = $state(false);
@ -60,19 +61,21 @@
</svelte:head>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex justify-between items-center mb-8">
<div>
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">My Presentations</h1>
<p class="text-slate-600 dark:text-slate-400 mt-1">Create and manage your slide decks</p>
</div>
<button
onclick={() => (showCreateModal = true)}
class="flex items-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
>
<Plus class="w-5 h-5" />
New Deck
</button>
</div>
<PageHeader
title="My Presentations"
description="Create and manage your slide decks"
size="lg"
>
{#snippet actions()}
<button
onclick={() => (showCreateModal = true)}
class="flex items-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
>
<Plus class="w-5 h-5" />
New Deck
</button>
{/snippet}
</PageHeader>
{#if decksStore.isLoading}
<div class="flex items-center justify-center py-16">

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { FeedbackPage } from '@manacore/shared-feedback-ui';
import { feedbackService } from '$lib/api/feedback';
import { auth } from '$lib/stores/auth.svelte';
</script>
<FeedbackPage
{feedbackService}
appName="Presi"
currentUserId={auth.user?.id}
/>

View file

@ -0,0 +1,39 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);
alert(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
}
function handleBuyPackage(packageId: string) {
console.log('Buy package:', packageId);
alert(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`);
}
</script>
<svelte:head>
<title>Mana - Presi</title>
</svelte:head>
<div class="mana-page">
<SubscriptionPage
appName="Presi"
onSubscribe={handleSubscribe}
onBuyPackage={handleBuyPackage}
currentPlanId="free"
pageTitle="Wähle dein Abo"
subscriptionsTitle="Abonnements"
packagesTitle="Einmal-Pakete"
yearlyDiscount="2 Monate gratis"
/>
</div>
<style>
.mana-page {
min-height: 100%;
width: 100%;
overflow-x: hidden;
background-color: hsl(var(--background));
}
</style>

View file

@ -1,23 +1,34 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ProfilePage } from '@manacore/shared-profile-ui';
import type { UserProfile, ProfileActions } from '@manacore/shared-profile-ui';
import { auth } from '$lib/stores/auth.svelte';
import { decksStore } from '$lib/stores/decks.svelte';
import { User, FolderOpen, Layers, Calendar, ArrowLeft } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { FolderOpen, Layers, Calendar } from 'lucide-svelte';
let totalSlides = $state(0);
let isLoading = $state(true);
// Map auth store user to UserProfile
let userProfile = $derived<UserProfile>({
id: auth.user?.id || '',
email: auth.user?.email || '',
role: auth.user?.role,
});
// Profile actions
const actions: ProfileActions = {
onLogout: async () => {
await auth.logout();
goto('/login');
},
onDeleteAccount: () => {
alert('Konto löschen ist noch nicht implementiert.');
},
};
onMount(async () => {
await decksStore.loadDecks();
// Calculate total slides from all decks
let slides = 0;
for (const deck of decksStore.decks) {
// Load each deck to get slide count
// Note: This is a simplified approach - in production you might want an API endpoint for stats
}
// For now, we show deck count - slide count would require loading all decks
isLoading = false;
});
@ -30,109 +41,90 @@
}
</script>
<svelte:head>
<title>Profile - Presi</title>
</svelte:head>
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center gap-4 mb-8">
<a href="/" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors">
<ArrowLeft class="w-5 h-5 text-slate-600 dark:text-slate-400" />
</a>
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">Profile</h1>
</div>
<ProfilePage
user={userProfile}
appName="Presi"
{actions}
pageTitle="Profil"
accountInfoTitle="Konto-Informationen"
actionsTitle="Aktionen"
emailLabel="E-Mail"
nameLabel="Name"
memberSinceLabel="Mitglied seit"
lastLoginLabel="Letzter Login"
roleLabel="Rolle"
editProfileLabel="Profil bearbeiten"
changePasswordLabel="Passwort ändern"
logoutLabel="Abmelden"
deleteAccountLabel="Konto löschen"
deleteAccountWarning="Diese Aktion kann nicht rückgängig gemacht werden."
/>
<!-- Stats Section -->
<div class="mx-auto max-w-xl px-4 pb-8">
{#if isLoading}
<div class="flex items-center justify-center py-16">
<div class="flex items-center justify-center py-8">
<div
class="animate-spin rounded-full h-10 w-10 border-4 border-primary-500 border-t-transparent"
class="animate-spin rounded-full h-8 w-8 border-4 border-primary border-t-transparent"
></div>
</div>
{:else}
<div class="space-y-6">
<!-- User Info Card -->
<!-- Stats Card -->
<section class="mb-6">
<h2 class="text-lg font-semibold text-foreground mb-4">Statistiken</h2>
<div
class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden"
class="p-5 rounded-2xl bg-white/85 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/10 shadow-sm"
>
<div class="p-8 text-center">
<div
class="mx-auto w-20 h-20 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center mb-4"
>
<User class="w-10 h-10 text-primary-600 dark:text-primary-400" />
<div class="grid grid-cols-2 gap-4">
<div class="text-center p-4 bg-black/5 dark:bg-white/5 rounded-xl">
<div class="flex justify-center mb-2">
<FolderOpen class="w-6 h-6 text-primary" />
</div>
<div class="text-2xl font-bold text-foreground">
{decksStore.decks.length}
</div>
<div class="text-sm text-muted-foreground">Präsentationen</div>
</div>
<h2 class="text-xl font-semibold text-slate-900 dark:text-white">
{auth.user?.email || 'User'}
</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1 font-mono">
ID: {auth.user?.id?.slice(0, 8)}...
</p>
</div>
</div>
<!-- Stats Card -->
<div
class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden"
>
<div class="p-4 border-b border-slate-200 dark:border-slate-700">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Statistics</h3>
</div>
<div class="p-6">
<div class="grid grid-cols-2 gap-6">
<!-- Total Decks -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/50 rounded-xl">
<div class="flex justify-center mb-2">
<FolderOpen class="w-8 h-8 text-primary-500" />
</div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">
{decksStore.decks.length}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400 mt-1">Total Decks</div>
</div>
<!-- Total Slides -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/50 rounded-xl">
<div class="flex justify-center mb-2">
<Layers class="w-8 h-8 text-primary-500" />
</div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">-</div>
<div class="text-sm text-slate-600 dark:text-slate-400 mt-1">Total Slides</div>
<div class="text-center p-4 bg-black/5 dark:bg-white/5 rounded-xl">
<div class="flex justify-center mb-2">
<Layers class="w-6 h-6 text-primary" />
</div>
<div class="text-2xl font-bold text-foreground">-</div>
<div class="text-sm text-muted-foreground">Folien</div>
</div>
</div>
</div>
</section>
<!-- Recent Activity -->
{#if decksStore.decks.length > 0}
<!-- Recent Presentations -->
{#if decksStore.decks.length > 0}
<section>
<h2 class="text-lg font-semibold text-foreground mb-4">Letzte Präsentationen</h2>
<div
class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden"
class="rounded-2xl bg-white/85 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/10 shadow-sm overflow-hidden"
>
<div class="p-4 border-b border-slate-200 dark:border-slate-700">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
Recent Presentations
</h3>
</div>
<div class="divide-y divide-slate-200 dark:divide-slate-700">
<div class="divide-y divide-black/10 dark:divide-white/10">
{#each decksStore.decks.slice(0, 5) as deck (deck.id)}
<a
href="/deck/{deck.id}"
class="flex items-center justify-between p-4 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
class="flex items-center justify-between p-4 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
>
<div class="flex items-center gap-3">
<div
class="w-10 h-10 bg-primary-100 dark:bg-primary-900/30 rounded-lg flex items-center justify-center"
class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center"
>
<FolderOpen class="w-5 h-5 text-primary-600 dark:text-primary-400" />
<FolderOpen class="w-5 h-5 text-primary" />
</div>
<div>
<h4 class="font-medium text-slate-900 dark:text-white">{deck.title}</h4>
<h4 class="font-medium text-foreground">{deck.title}</h4>
{#if deck.description}
<p class="text-sm text-slate-500 dark:text-slate-400 truncate max-w-xs">
<p class="text-sm text-muted-foreground truncate max-w-xs">
{deck.description}
</p>
{/if}
</div>
</div>
<div class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar class="w-4 h-4" />
{formatDate(deck.updatedAt)}
</div>
@ -140,7 +132,7 @@
{/each}
</div>
</div>
{/if}
</div>
</section>
{/if}
{/if}
</div>

View file

@ -27,7 +27,14 @@
"vite": "^6.0.0"
},
"dependencies": {
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",
"@manacore/shared-feedback-ui": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-subscription-ui": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",

View file

@ -0,0 +1,14 @@
/**
* Feedback Service Instance for Zitare Web App
*/
import { createFeedbackService } from '@manacore/shared-feedback-service';
import { authStore } from '$lib/stores/auth.svelte';
const MANA_AUTH_URL = 'http://localhost:3001'; // TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env
export const feedbackService = createFeedbackService({
apiUrl: MANA_AUTH_URL,
appId: 'zitare',
getAuthToken: async () => authStore.getAccessToken(),
});

View file

@ -0,0 +1,186 @@
/**
* Auth Store - Manages authentication state using Svelte 5 runes
* Uses Mana Core Auth
*/
import { browser } from '$app/environment';
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
// Initialize Mana Core Auth only on the client side
// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available
const MANA_AUTH_URL = 'http://localhost:3001';
// Lazy initialization to avoid SSR issues with localStorage
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
function getAuthService() {
if (!browser) return null;
if (!_authService) {
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
_authService = auth.authService;
_tokenManager = auth.tokenManager;
}
return _authService;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
let initialized = $state(false);
export const authStore = {
// Getters
get user() {
return user;
},
get loading() {
return loading;
},
get isAuthenticated() {
return !!user;
},
get initialized() {
return initialized;
},
/**
* Initialize auth state from stored tokens
*/
async initialize() {
if (initialized) return;
const authService = getAuthService();
if (!authService) {
initialized = true;
loading = false;
return;
}
loading = true;
try {
const authenticated = await authService.isAuthenticated();
if (authenticated) {
const userData = await authService.getUserFromToken();
user = userData;
}
initialized = true;
} catch (error) {
console.error('Failed to initialize auth:', error);
user = null;
} finally {
loading = false;
}
},
/**
* Sign in with email and password
*/
async signIn(email: string, password: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.signIn(email, password);
if (!result.success) {
return { success: false, error: result.error || 'Login failed' };
}
// Get user data from token
const userData = await authService.getUserFromToken();
user = userData;
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Sign up with email and password
*/
async signUp(email: string, password: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const result = await authService.signUp(email, password);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };
}
// Mana Core Auth requires separate login after signup
if (result.needsVerification) {
return { success: true, needsVerification: true };
}
// Auto sign in after successful signup
const signInResult = await this.signIn(email, password);
return { ...signInResult, needsVerification: false };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage, needsVerification: false };
}
},
/**
* Sign out
*/
async signOut() {
const authService = getAuthService();
if (!authService) {
user = null;
return;
}
try {
await authService.signOut();
user = null;
} catch (error) {
console.error('Sign out error:', error);
// Clear user even if sign out fails
user = null;
}
},
/**
* Send password reset email
*/
async resetPassword(email: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.forgotPassword(email);
if (!result.success) {
return { success: false, error: result.error || 'Password reset failed' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Get access token for API calls
*/
async getAccessToken() {
const authService = getAuthService();
if (!authService) {
return null;
}
return await authService.getAppToken();
},
};

View file

@ -0,0 +1,7 @@
<script lang="ts">
import '../../app.css';
let { children } = $props();
</script>
{@render children()}

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { ZitareLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
async function handleForgotPassword(email: string) {
return authStore.resetPassword(email);
}
</script>
<svelte:head>
<title>Passwort vergessen - Zitare</title>
</svelte:head>
<ForgotPasswordPage
appName="Zitare"
logo={ZitareLogo}
primaryColor="#f59e0b"
onForgotPassword={handleForgotPassword}
{goto}
loginPath="/login"
lightBackground="#fffbeb"
darkBackground="#1c1917"
/>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { LoginPage } from '@manacore/shared-auth-ui';
import { ZitareLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
</script>
<svelte:head>
<title>Anmelden - Zitare</title>
</svelte:head>
<LoginPage
appName="Zitare"
logo={ZitareLogo}
primaryColor="#f59e0b"
onSignIn={handleSignIn}
{goto}
successRedirect="/"
registerPath="/register"
forgotPasswordPath="/forgot-password"
lightBackground="#fffbeb"
darkBackground="#1c1917"
/>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { ZitareLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
</script>
<svelte:head>
<title>Registrieren - Zitare</title>
</svelte:head>
<RegisterPage
appName="Zitare"
logo={ZitareLogo}
primaryColor="#f59e0b"
onSignUp={handleSignUp}
{goto}
successRedirect="/"
loginPath="/login"
lightBackground="#fffbeb"
darkBackground="#1c1917"
/>

View file

@ -5,6 +5,7 @@
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import {
isSidebarMode as sidebarModeStore,
@ -53,6 +54,8 @@
{ href: '/authors', label: 'Autoren', icon: 'users' },
{ href: '/favorites', label: 'Favoriten', icon: 'heart' },
{ href: '/lists', label: 'Listen', icon: 'list' },
{ href: '/profile', label: 'Profil', icon: 'user' },
{ href: '/mana', label: 'Mana', icon: 'sparkles' },
];
// Navigation shortcuts (Ctrl+1-5)
@ -104,10 +107,18 @@
theme.toggleMode();
}
onMount(() => {
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
onMount(async () => {
// Initialize theme
theme.initialize();
// Initialize auth
await authStore.initialize();
// Initialize sidebar mode from localStorage
const savedSidebar = localStorage.getItem('zitare-nav-sidebar');
if (savedSidebar === 'true') {
@ -159,7 +170,8 @@
{themeVariantItems}
{currentThemeVariantLabel}
showLanguageSwitcher={false}
showLogout={false}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
primaryColor="#f59e0b"
/>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { authorsDE, quotesDE, type Author } from '@zitare/shared';
import { PageHeader } from '@manacore/shared-ui';
import AuthorCard from '$lib/components/AuthorCard.svelte';
// Get quote counts for each author
@ -97,29 +98,29 @@
<div class="authors-page">
<div class="header-container">
<div class="header-row">
<h2>Autoren</h2>
<button class="search-fab" onclick={toggleSearch} aria-label="Toggle search">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{#if isSearchOpen}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
{/if}
</svg>
</button>
</div>
<PageHeader title="Autoren" size="lg">
{#snippet actions()}
<button class="search-fab" onclick={toggleSearch} aria-label="Toggle search">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{#if isSearchOpen}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
{/if}
</svg>
</button>
{/snippet}
</PageHeader>
{#if isSearchOpen}
<div class="search-bar">

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { FeedbackPage } from '@manacore/shared-feedback-ui';
import { feedbackService } from '$lib/api/feedback';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<FeedbackPage
{feedbackService}
appName="Zitare"
currentUserId={authStore.user?.id}
/>

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { listsStore, type QuoteList } from '$lib/stores/lists';
import { quotesDE } from '@zitare/shared';
import { PageHeader } from '@manacore/shared-ui';
import { toast } from '$lib/stores/toast';
let lists = $state<QuoteList[]>([]);
@ -69,23 +70,24 @@
<div class="lists-page">
<div class="header-container">
<div class="header-row">
<div>
<h2>Meine Listen</h2>
<p class="subtitle">{lists.length} {lists.length === 1 ? 'Liste' : 'Listen'}</p>
</div>
<button class="create-fab" onclick={openCreateModal} aria-label="Neue Liste erstellen">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
</button>
</div>
<PageHeader
title="Meine Listen"
description="{lists.length} {lists.length === 1 ? 'Liste' : 'Listen'}"
size="lg"
>
{#snippet actions()}
<button class="create-fab" onclick={openCreateModal} aria-label="Neue Liste erstellen">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
</button>
{/snippet}
</PageHeader>
{#if lists.length > 3}
<div class="search-container">

View file

@ -0,0 +1,40 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { toast } from '$lib/stores/toast';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);
toast.info(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
}
function handleBuyPackage(packageId: string) {
console.log('Buy package:', packageId);
toast.info(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`);
}
</script>
<svelte:head>
<title>Mana - Zitare</title>
</svelte:head>
<div class="mana-page">
<SubscriptionPage
appName="Zitare"
onSubscribe={handleSubscribe}
onBuyPackage={handleBuyPackage}
currentPlanId="free"
pageTitle="Wähle dein Abo"
subscriptionsTitle="Abonnements"
packagesTitle="Einmal-Pakete"
yearlyDiscount="2 Monate gratis"
/>
</div>
<style>
.mana-page {
min-height: 100%;
width: 100%;
overflow-x: hidden;
background-color: rgb(var(--color-background));
}
</style>

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { ProfilePage } from '@manacore/shared-profile-ui';
import type { UserProfile, ProfileActions } from '@manacore/shared-profile-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
// Map auth store user to UserProfile
let userProfile = $derived<UserProfile>({
id: authStore.user?.id || '',
email: authStore.user?.email || '',
role: authStore.user?.role,
});
// Profile actions
const actions: ProfileActions = {
onLogout: async () => {
await authStore.signOut();
goto('/login');
},
onDeleteAccount: () => {
alert('Konto löschen ist noch nicht implementiert.');
},
};
</script>
<ProfilePage
user={userProfile}
appName="Zitare"
{actions}
pageTitle="Profil"
accountInfoTitle="Konto-Informationen"
actionsTitle="Aktionen"
emailLabel="E-Mail"
nameLabel="Name"
memberSinceLabel="Mitglied seit"
lastLoginLabel="Letzter Login"
roleLabel="Rolle"
editProfileLabel="Profil bearbeiten"
changePasswordLabel="Passwort ändern"
logoutLabel="Abmelden"
deleteAccountLabel="Konto löschen"
deleteAccountWarning="Diese Aktion kann nicht rückgängig gemacht werden."
/>

View file

@ -18,6 +18,13 @@ export default defineConfig({
'@manacore/shared-tailwind',
'@manacore/shared-theme',
'@manacore/shared-theme-ui',
'@manacore/shared-feedback-ui',
'@manacore/shared-feedback-service',
'@manacore/shared-feedback-types',
'@manacore/shared-auth',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
'@manacore/shared-subscription-ui',
],
},
optimizeDeps: {
@ -30,6 +37,13 @@ export default defineConfig({
'@manacore/shared-tailwind',
'@manacore/shared-theme',
'@manacore/shared-theme-ui',
'@manacore/shared-feedback-ui',
'@manacore/shared-feedback-service',
'@manacore/shared-feedback-types',
'@manacore/shared-auth',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
'@manacore/shared-subscription-ui',
],
},
});

View file

@ -0,0 +1,13 @@
<script lang="ts">
import AppLogo from '../AppLogo.svelte';
interface Props {
size?: number;
color?: string;
class?: string;
}
let { size = 55, color, class: className = '' }: Props = $props();
</script>
<AppLogo app="zitare" {size} {color} class={className} />

View file

@ -10,3 +10,4 @@ export { default as UloadLogo } from './UloadLogo.svelte';
export { default as ChatLogo } from './ChatLogo.svelte';
export { default as PresiLogo } from './PresiLogo.svelte';
export { default as NutriPhiLogo } from './NutriPhiLogo.svelte';
export { default as ZitareLogo } from './ZitareLogo.svelte';

View file

@ -1,12 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationDir": "dist",
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"types": ["svelte"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules"]
}

View file

@ -110,6 +110,24 @@
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3 min-w-0">
<!-- Back Button -->
{#if backHref}
<a
href={backHref}
class="page-header__back flex-shrink-0 p-1.5 -ml-1.5 rounded-lg text-theme-secondary hover:text-theme hover:bg-theme-secondary/10 transition-colors"
aria-label="Zurück"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
</a>
{/if}
<!-- Icon -->
{#if icon}
<div class="page-header__icon flex-shrink-0 text-theme-secondary">

762
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,12 @@
import { Module } from '@nestjs/common';
import { FeedbackController } from './feedback.controller';
import { FeedbackService } from './feedback.service';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { OptionalAuthGuard } from '../common/guards/optional-auth.guard';
@Module({
controllers: [FeedbackController],
providers: [FeedbackService],
providers: [FeedbackService, JwtAuthGuard, OptionalAuthGuard],
exports: [FeedbackService],
})
export class FeedbackModule {}