mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
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:
parent
c85cd4556c
commit
9432a73a1f
62 changed files with 1803 additions and 1152 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
14
apps/manacore/apps/web/src/lib/api/feedback.ts
Normal file
14
apps/manacore/apps/web/src/lib/api/feedback.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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: [],
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
43
apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte
Normal file
43
apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte
Normal 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."
|
||||
/>
|
||||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
15
apps/manadeck/apps/web/src/lib/api/feedback.ts
Normal file
15
apps/manadeck/apps/web/src/lib/api/feedback.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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."
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
15
apps/picture/apps/web/src/lib/api/feedback.ts
Normal file
15
apps/picture/apps/web/src/lib/api/feedback.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
11
apps/picture/apps/web/src/routes/app/feedback/+page.svelte
Normal file
11
apps/picture/apps/web/src/routes/app/feedback/+page.svelte
Normal 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}
|
||||
/>
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
15
apps/presi/apps/web/src/lib/api/feedback.ts
Normal file
15
apps/presi/apps/web/src/lib/api/feedback.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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' },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
11
apps/presi/apps/web/src/routes/feedback/+page.svelte
Normal file
11
apps/presi/apps/web/src/routes/feedback/+page.svelte
Normal 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}
|
||||
/>
|
||||
39
apps/presi/apps/web/src/routes/mana/+page.svelte
Normal file
39
apps/presi/apps/web/src/routes/mana/+page.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
14
apps/zitare/apps/web/src/lib/api/feedback.ts
Normal file
14
apps/zitare/apps/web/src/lib/api/feedback.ts
Normal 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(),
|
||||
});
|
||||
186
apps/zitare/apps/web/src/lib/stores/auth.svelte.ts
Normal file
186
apps/zitare/apps/web/src/lib/stores/auth.svelte.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
7
apps/zitare/apps/web/src/routes/(auth)/+layout.svelte
Normal file
7
apps/zitare/apps/web/src/routes/(auth)/+layout.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import '../../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
|
|
@ -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"
|
||||
/>
|
||||
27
apps/zitare/apps/web/src/routes/(auth)/login/+page.svelte
Normal file
27
apps/zitare/apps/web/src/routes/(auth)/login/+page.svelte
Normal 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"
|
||||
/>
|
||||
26
apps/zitare/apps/web/src/routes/(auth)/register/+page.svelte
Normal file
26
apps/zitare/apps/web/src/routes/(auth)/register/+page.svelte
Normal 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"
|
||||
/>
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
11
apps/zitare/apps/web/src/routes/feedback/+page.svelte
Normal file
11
apps/zitare/apps/web/src/routes/feedback/+page.svelte
Normal 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}
|
||||
/>
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
40
apps/zitare/apps/web/src/routes/mana/+page.svelte
Normal file
40
apps/zitare/apps/web/src/routes/mana/+page.svelte
Normal 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>
|
||||
43
apps/zitare/apps/web/src/routes/profile/+page.svelte
Normal file
43
apps/zitare/apps/web/src/routes/profile/+page.svelte
Normal 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."
|
||||
/>
|
||||
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
13
packages/shared-branding/src/logos/ZitareLogo.svelte
Normal file
13
packages/shared-branding/src/logos/ZitareLogo.svelte
Normal 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} />
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
762
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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 {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue