mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
Commit Message feat: implement comprehensive shared packages architecture for monorepo SUMMARY: Introduce 10 shared packages to unify common code across all 4 web apps, reducing ~3,000 lines of duplicated code and establishing consistent patterns for authentication, UI components, theming, and utilities. NEW SHARED PACKAGES: - @manacore/shared-auth: Unified auth logic (token management, JWT utils, fetch interceptor, storage/device/network adapters) - @manacore/shared-auth-ui: Reusable auth UI (LoginPage, RegisterPage, OAuth buttons for Google/Apple) - @manacore/shared-tailwind: Unified Tailwind config with 4 themes (lume, nature, stone, ocean) and light/dark mode support - @manacore/shared-icons: Phosphor-based icon library (40+ icons) - @manacore/shared-ui: Atomic design system (Text, Button, Badge, Toggle, Input, Modal) - @manacore/shared-i18n: Unified i18n setup with locale detection - @manacore/shared-config: Environment validation with Zod - @manacore/shared-subscriptio n-types: Subscription type definitions - @manacore/shared-subscriptio n-ui: Subscription UI components (planned) EXTENDED PACKAGES: - @manacore/shared-types: Added auth.ts, theme.ts, ui.ts, common.ts - @manacore/shared-utils: Added format.ts, validation.ts APP MIGRATIONS: - memoro/web: Migrated login (549→46 LOC), tailwind (165→12 LOC), removed 15+ duplicate components - manacore/web: Migrated to client-side auth with shared-auth, added new components (Icon, ThemeToggle, Logo) - manadeck/web: Replaced local authService/tokenManager with shared-auth, migrated auth pages - maerchenzauber/web: Added auth setup, stores, components, routes DELETED FILES (migrated to shared packages): - OAuth buttons (Google/Apple) from memoro, manacore, manadeck - Local authService, tokenManager, deviceManager, jwt utils - Duplicate Modal, Toggle, Text components - iconPaths and ManaIcon components - Subscription-related components (CostCard, PackageCard, etc.) BENEFITS: - 92% reduction in login page code - 93% reduction in tailwind config code - Consistent theming across all apps - Single source of truth for auth logic - Easier maintenance and updates BREAKING CHANGES: - Icon imports now from @manacore/shared-icons - Modal imports from @manacore/shared-ui - OAuth config via setGoogleCl ientId()/setAppleConfig()
This commit is contained in:
parent
725db638ea
commit
ef70a1af0b
198 changed files with 11113 additions and 3656 deletions
|
|
@ -36,6 +36,18 @@
|
|||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-config": "workspace:*",
|
||||
"@manacore/shared-i18n": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-supabase": "workspace:*",
|
||||
"@manacore/shared-subscription-types": "workspace:*",
|
||||
"@manacore/shared-subscription-ui": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.81.1"
|
||||
},
|
||||
|
|
|
|||
264
manacore/apps/web/src/lib/components/AppSlider.svelte
Normal file
264
manacore/apps/web/src/lib/components/AppSlider.svelte
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
<script lang="ts">
|
||||
interface App {
|
||||
name: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
comingSoon?: boolean;
|
||||
status: 'published' | 'beta' | 'development' | 'planning';
|
||||
}
|
||||
|
||||
let selectedApp = $state<number | null>(null);
|
||||
let hoveredApp = $state<number | null>(null);
|
||||
let cardRotations = $state<{ [key: number]: { rotateX: number; rotateY: number } }>({});
|
||||
let modalScrollContainer = $state<HTMLDivElement | null>(null);
|
||||
|
||||
const apps: App[] = [
|
||||
{
|
||||
name: 'Memoro',
|
||||
description: 'AI Voice Memos',
|
||||
longDescription: 'Transform your voice recordings into organized, searchable notes with AI-powered transcription and insights.',
|
||||
icon: '/images/app-icons/memoro-logo-gradient.png',
|
||||
color: '#f8d62b',
|
||||
comingSoon: false,
|
||||
status: 'published'
|
||||
},
|
||||
{
|
||||
name: 'Märchenzauber',
|
||||
description: 'AI Story Creator',
|
||||
longDescription: 'Create magical personalized stories for children with AI-generated illustrations and consistent characters.',
|
||||
icon: '/images/app-icons/maerchenzauber-logo-gradient.png',
|
||||
color: '#FF6B9D',
|
||||
comingSoon: true,
|
||||
status: 'beta'
|
||||
},
|
||||
{
|
||||
name: 'Moodlit',
|
||||
description: 'AI Mood Tracker',
|
||||
longDescription: 'Track your emotional well-being with AI-powered insights and personalized recommendations.',
|
||||
icon: '/images/app-icons/moodlit-logo-gradient.png',
|
||||
color: '#9C27B0',
|
||||
comingSoon: true,
|
||||
status: 'beta'
|
||||
},
|
||||
{
|
||||
name: 'Manacore',
|
||||
description: 'Central Hub',
|
||||
longDescription: 'Your central hub for managing all Mana applications, subscriptions, and account settings.',
|
||||
icon: '/images/app-icons/manacore-logo-gradient.png',
|
||||
color: '#6366f1',
|
||||
comingSoon: false,
|
||||
status: 'development'
|
||||
}
|
||||
];
|
||||
|
||||
function getStatusColor(status: App['status']) {
|
||||
const colors = {
|
||||
published: '#4CAF50',
|
||||
beta: '#FFD700',
|
||||
development: '#FF9800',
|
||||
planning: '#F44336'
|
||||
};
|
||||
return colors[status];
|
||||
}
|
||||
|
||||
function getStatusLabel(status: App['status']) {
|
||||
const labels = {
|
||||
published: 'Live',
|
||||
beta: 'Beta',
|
||||
development: 'In Development',
|
||||
planning: 'Planned'
|
||||
};
|
||||
return labels[status];
|
||||
}
|
||||
|
||||
function openModal(index: number) {
|
||||
selectedApp = index;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
selectedApp = null;
|
||||
}
|
||||
|
||||
function handleCardMouseMove(e: MouseEvent, index: number, cardElement: HTMLElement) {
|
||||
const rect = cardElement.getBoundingClientRect();
|
||||
const cardCenterX = rect.left + rect.width / 2;
|
||||
const cardCenterY = rect.top + rect.height / 2;
|
||||
|
||||
const mouseXRelative = e.clientX - cardCenterX;
|
||||
const mouseYRelative = e.clientY - cardCenterY;
|
||||
|
||||
const maxRotation = 3;
|
||||
const rotateY = (mouseXRelative / (rect.width / 2)) * maxRotation;
|
||||
const rotateX = -(mouseYRelative / (rect.height / 2)) * maxRotation;
|
||||
|
||||
cardRotations[index] = { rotateX, rotateY };
|
||||
}
|
||||
|
||||
function handleCardMouseLeave(index: number) {
|
||||
cardRotations[index] = { rotateX: 0, rotateY: 0 };
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (selectedApp !== null && modalScrollContainer) {
|
||||
const appIndex = selectedApp;
|
||||
setTimeout(() => {
|
||||
const cardWidth = 360 + 24;
|
||||
const scrollPosition = appIndex * cardWidth;
|
||||
modalScrollContainer?.scrollTo({
|
||||
left: scrollPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<h3 class="mb-4 text-center text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Part of the Mana Ecosystem
|
||||
</h3>
|
||||
|
||||
<div class="relative">
|
||||
<div class="flex gap-4 justify-center overflow-x-auto pb-6 scrollbar-hide snap-x snap-mandatory scroll-smooth px-4 py-4" style="perspective: 1000px;">
|
||||
{#each apps as app, index}
|
||||
<button
|
||||
class="group relative flex-shrink-0 rounded-2xl p-5 cursor-pointer snap-center border transition-all"
|
||||
class:bg-gray-100={hoveredApp !== index}
|
||||
class:dark:bg-gray-800={hoveredApp !== index}
|
||||
class:bg-gray-200={hoveredApp === index}
|
||||
class:dark:bg-gray-700={hoveredApp === index}
|
||||
style="width: 160px; border-color: rgba(0, 0, 0, 0.1); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg); transform-style: preserve-3d; transition: transform 0.1s ease-out, background-color 0.2s ease-out;"
|
||||
onmouseenter={() => hoveredApp = index}
|
||||
onmousemove={(e) => handleCardMouseMove(e, index, e.currentTarget)}
|
||||
onmouseleave={() => { handleCardMouseLeave(index); hoveredApp = null; }}
|
||||
onclick={() => openModal(index)}
|
||||
>
|
||||
<div
|
||||
class="absolute top-3 right-3 w-3 h-3 rounded-full status-indicator"
|
||||
style="background-color: {getStatusColor(app.status)}; box-shadow: 0 0 8px {getStatusColor(app.status)};"
|
||||
></div>
|
||||
|
||||
<div class="mb-2 flex h-20 w-20 mx-auto items-center justify-center rounded-xl transition-transform group-hover:scale-110">
|
||||
<img src={app.icon} alt={app.name} class="w-16 h-16 object-contain" />
|
||||
</div>
|
||||
|
||||
<h4 class="text-base font-semibold text-center text-gray-900 dark:text-white">
|
||||
{app.name}
|
||||
</h4>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedApp !== null}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background-color: rgba(0, 0, 0, 0.85);"
|
||||
onclick={closeModal}
|
||||
onkeydown={(e) => e.key === 'Escape' && closeModal()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<button
|
||||
onclick={closeModal}
|
||||
class="absolute top-6 right-6 rounded-full p-2 transition-all hover:bg-white/10 z-10"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
bind:this={modalScrollContainer}
|
||||
class="absolute inset-0 flex items-center overflow-x-auto scrollbar-hide snap-x snap-mandatory scroll-smooth"
|
||||
>
|
||||
<div class="flex gap-6 px-8 py-8 mx-auto" style="perspective: 1000px;">
|
||||
{#each apps as app, index}
|
||||
<div
|
||||
class="flex-shrink-0 rounded-3xl p-8 snap-center shadow-2xl relative"
|
||||
class:bg-gray-100={hoveredApp !== index}
|
||||
class:dark:bg-gray-800={hoveredApp !== index}
|
||||
class:bg-gray-200={hoveredApp === index}
|
||||
class:dark:bg-gray-700={hoveredApp === index}
|
||||
style="min-width: 360px; max-width: 360px; border: 3px solid {app.color}40; transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg); transform-style: preserve-3d; transition: transform 0.1s ease-out, background-color 0.2s ease-out;"
|
||||
onclick={(e) => { e.stopPropagation(); selectedApp = index; }}
|
||||
onmouseenter={() => hoveredApp = index}
|
||||
onmousemove={(e) => handleCardMouseMove(e, index, e.currentTarget)}
|
||||
onmouseleave={() => { handleCardMouseLeave(index); hoveredApp = null; }}
|
||||
onkeydown={() => {}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="absolute top-4 right-4 flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-300">
|
||||
{getStatusLabel(app.status)}
|
||||
</span>
|
||||
<div
|
||||
class="w-4 h-4 rounded-full status-indicator"
|
||||
style="background-color: {getStatusColor(app.status)}; box-shadow: 0 0 12px {getStatusColor(app.status)};"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<img src={app.icon} alt={app.name} class="w-28 h-28 object-contain mb-3 mx-auto" />
|
||||
|
||||
<h3 class="text-2xl font-bold mb-2 text-center text-gray-900 dark:text-white">
|
||||
{app.name}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm mb-4 text-center font-medium" style="color: {app.color};">
|
||||
{app.description}
|
||||
</p>
|
||||
|
||||
<p class="text-sm leading-relaxed mb-6 text-center text-gray-600 dark:text-gray-300">
|
||||
{app.longDescription}
|
||||
</p>
|
||||
|
||||
<div class="text-center">
|
||||
{#if app.comingSoon}
|
||||
<div class="inline-block rounded-full px-5 py-2.5 text-sm font-medium bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400">
|
||||
Coming Soon
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="rounded-xl px-8 py-3 text-sm font-semibold transition-all hover:opacity-80 border-2 text-white"
|
||||
style="background-color: {app.color}60; border-color: {app.color};"
|
||||
>
|
||||
Open App
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
manacore/apps/web/src/lib/components/Icon.svelte
Normal file
34
manacore/apps/web/src/lib/components/Icon.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Icon Component - Re-exports from @manacore/shared-icons
|
||||
* This wrapper ensures backward compatibility with existing imports
|
||||
*/
|
||||
import { iconPaths } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
name: keyof typeof iconPaths;
|
||||
size?: number;
|
||||
class?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { name, size = 24, class: className = '', color }: Props = $props();
|
||||
|
||||
const path = $derived(iconPaths[name]);
|
||||
</script>
|
||||
|
||||
{#if path}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={color || 'currentColor'}
|
||||
viewBox="0 0 256 256"
|
||||
class={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{@html path}
|
||||
</svg>
|
||||
{:else}
|
||||
<span class="text-red-500" title="Icon '{name}' not found">⚠</span>
|
||||
{/if}
|
||||
29
manacore/apps/web/src/lib/components/ManaCoreLogo.svelte
Normal file
29
manacore/apps/web/src/lib/components/ManaCoreLogo.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { size = 55, color = '#6366f1' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<!-- M letter for ManaCore -->
|
||||
<text
|
||||
x="50%"
|
||||
y="55%"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="middle"
|
||||
font-size="16"
|
||||
font-weight="bold"
|
||||
fill={color}
|
||||
>
|
||||
M
|
||||
</text>
|
||||
</svg>
|
||||
40
manacore/apps/web/src/lib/components/ThemeToggle.svelte
Normal file
40
manacore/apps/web/src/lib/components/ThemeToggle.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { theme } from '$lib/stores/theme';
|
||||
|
||||
let currentTheme = $derived($theme);
|
||||
|
||||
function toggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="rounded-lg p-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
aria-label="Toggle theme"
|
||||
title={currentTheme.effectiveMode === 'light'
|
||||
? 'Switch to dark mode'
|
||||
: 'Switch to light mode'}
|
||||
>
|
||||
{#if currentTheme.effectiveMode === 'light'}
|
||||
<!-- Moon Icon (Dark Mode) -->
|
||||
<svg class="h-5 w-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Sun Icon (Light Mode) -->
|
||||
<svg class="h-5 w-5 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
class?: string;
|
||||
autocomplete?: 'email' | 'current-password' | 'new-password' | 'username' | 'off' | string;
|
||||
oninput?: (event: Event) => void;
|
||||
minlength?: number;
|
||||
maxlength?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -22,7 +24,9 @@
|
|||
disabled = false,
|
||||
class: className = '',
|
||||
autocomplete,
|
||||
oninput
|
||||
oninput,
|
||||
minlength,
|
||||
maxlength
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
|
|
@ -33,6 +37,8 @@
|
|||
{placeholder}
|
||||
{required}
|
||||
{disabled}
|
||||
{minlength}
|
||||
{maxlength}
|
||||
autocomplete={autocomplete as any}
|
||||
bind:value
|
||||
oninput={oninput}
|
||||
|
|
|
|||
82
manacore/apps/web/src/lib/stores/authStore.svelte.ts
Normal file
82
manacore/apps/web/src/lib/stores/authStore.svelte.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { createBrowserClient } from '@supabase/ssr';
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
|
||||
// Create browser Supabase client
|
||||
function getSupabaseClient() {
|
||||
return createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
|
||||
}
|
||||
|
||||
export const authStore = {
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
|
||||
// Check if email confirmation is required
|
||||
const needsVerification = !data.session;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
needsVerification
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Send password reset email
|
||||
*/
|
||||
async forgotPassword(email: string) {
|
||||
const supabase = getSupabaseClient();
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/reset-password`
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
async signOut() {
|
||||
const supabase = getSupabaseClient();
|
||||
await supabase.auth.signOut();
|
||||
}
|
||||
};
|
||||
79
manacore/apps/web/src/lib/stores/theme.ts
Normal file
79
manacore/apps/web/src/lib/stores/theme.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { writable, derived } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
interface ThemeState {
|
||||
mode: ThemeMode;
|
||||
effectiveMode: 'light' | 'dark';
|
||||
}
|
||||
|
||||
function createThemeStore() {
|
||||
const getInitialMode = (): ThemeMode => {
|
||||
if (browser) {
|
||||
const stored = localStorage.getItem('theme-mode');
|
||||
if (stored === 'light' || stored === 'dark' || stored === 'system') {
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
return 'system';
|
||||
};
|
||||
|
||||
const getSystemPreference = (): 'light' | 'dark' => {
|
||||
if (browser && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
};
|
||||
|
||||
const mode = writable<ThemeMode>(getInitialMode());
|
||||
|
||||
const effectiveMode = derived(mode, ($mode) => {
|
||||
if ($mode === 'system') {
|
||||
return getSystemPreference();
|
||||
}
|
||||
return $mode;
|
||||
});
|
||||
|
||||
const state = derived([mode, effectiveMode], ([$mode, $effectiveMode]) => ({
|
||||
mode: $mode,
|
||||
effectiveMode: $effectiveMode
|
||||
}));
|
||||
|
||||
// Apply theme to document
|
||||
if (browser) {
|
||||
effectiveMode.subscribe((effective) => {
|
||||
if (effective === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for system preference changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
mode.update((m) => m); // Trigger re-evaluation
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: state.subscribe,
|
||||
setMode: (newMode: ThemeMode) => {
|
||||
mode.set(newMode);
|
||||
if (browser) {
|
||||
localStorage.setItem('theme-mode', newMode);
|
||||
}
|
||||
},
|
||||
toggleMode: () => {
|
||||
mode.update((current) => {
|
||||
const newMode = current === 'light' ? 'dark' : current === 'dark' ? 'system' : 'light';
|
||||
if (browser) {
|
||||
localStorage.setItem('theme-mode', newMode);
|
||||
}
|
||||
return newMode;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const theme = createThemeStore();
|
||||
|
|
@ -11,8 +11,4 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 dark:bg-gray-900 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{@render children()}
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import { redirect, fail } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals: { supabase } }) => {
|
||||
const formData = await request.formData();
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
|
||||
if (!email || !password) {
|
||||
return fail(400, {
|
||||
error: 'Email and password are required',
|
||||
email
|
||||
});
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Login error:', error);
|
||||
return fail(400, {
|
||||
error: error.message,
|
||||
email
|
||||
});
|
||||
}
|
||||
|
||||
throw redirect(303, '/dashboard');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,86 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Input from '$lib/components/ui/Input.svelte';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import ManaCoreLogo from '$lib/components/ManaCoreLogo.svelte';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
|
||||
let { form } = $props();
|
||||
let loading = $state(false);
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
return authStore.signIn(email, password);
|
||||
}
|
||||
|
||||
async function handleForgotPassword(email: string) {
|
||||
return authStore.forgotPassword(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<h2 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">ManaCore</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<Card class="mt-8">
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}}
|
||||
>
|
||||
{#if form?.error}
|
||||
<div class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Email address
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
autocomplete="email"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
value={form?.email ?? ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<label for="password" class="block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Password
|
||||
</label>
|
||||
<a href="/forgot-password" class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" {loading} class="w-full">
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Don't have an account?
|
||||
<a href="/register" class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400">
|
||||
Sign up
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<LoginPage
|
||||
appName="ManaCore"
|
||||
logo={ManaCoreLogo}
|
||||
primaryColor="#6366f1"
|
||||
onSignIn={handleSignIn}
|
||||
onForgotPassword={handleForgotPassword}
|
||||
goto={goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect="/dashboard"
|
||||
registerPath="/register"
|
||||
lightBackground="#f3f4f6"
|
||||
darkBackground="#121212"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals: { supabase } }) => {
|
||||
const formData = await request.formData();
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirmPassword') as string;
|
||||
|
||||
if (!email || !password || !confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'All fields are required',
|
||||
email
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return fail(400, {
|
||||
error: 'Passwords do not match',
|
||||
email
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return fail(400, {
|
||||
error: 'Password must be at least 8 characters',
|
||||
email
|
||||
});
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${new URL('/auth/callback', request.url).toString()}`
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Registration error:', error);
|
||||
return fail(400, {
|
||||
error: error.message,
|
||||
email
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -1,101 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Input from '$lib/components/ui/Input.svelte';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import ManaCoreLogo from '$lib/components/ManaCoreLogo.svelte';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
|
||||
let { form } = $props();
|
||||
let loading = $state(false);
|
||||
async function handleSignUp(email: string, password: string) {
|
||||
return authStore.signUp(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<h2 class="mb-2 text-3xl font-bold text-gray-900 dark:text-white">Create Account</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">Sign up for ManaCore</p>
|
||||
</div>
|
||||
|
||||
<Card class="mt-8">
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}}
|
||||
>
|
||||
{#if form?.error}
|
||||
<div class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if form?.success}
|
||||
<div class="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||
Account created! Please check your email to verify your account.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Email address
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
autocomplete="email"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
value={form?.email ?? ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Confirm Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
id="confirmPassword"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button type="submit" {loading} class="w-full">
|
||||
{loading ? 'Creating account...' : 'Sign up'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Already have an account?
|
||||
<a href="/login" class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400">
|
||||
Sign in
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<RegisterPage
|
||||
appName="ManaCore"
|
||||
logo={ManaCoreLogo}
|
||||
primaryColor="#6366f1"
|
||||
onSignUp={handleSignUp}
|
||||
goto={goto}
|
||||
successRedirect="/dashboard"
|
||||
loginPath="/login"
|
||||
lightBackground="#f3f4f6"
|
||||
darkBackground="#121212"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -136,12 +136,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if form?.success}
|
||||
<div class="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||
Password updated successfully! Redirecting to dashboard...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="password" class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
|
|
@ -154,7 +148,7 @@
|
|||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength="6"
|
||||
minlength={6}
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Must be at least 6 characters
|
||||
|
|
@ -172,7 +166,7 @@
|
|||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength="6"
|
||||
minlength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -141,12 +141,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if form?.success}
|
||||
<div class="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||
Password updated successfully! Redirecting to dashboard...
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="password" class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
|
|
@ -159,7 +153,7 @@
|
|||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength="6"
|
||||
minlength={6}
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Must be at least 6 characters
|
||||
|
|
@ -177,7 +171,7 @@
|
|||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
minlength="6"
|
||||
minlength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 263 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 431 KiB |
|
|
@ -1,9 +1,17 @@
|
|||
import preset from '@manacore/shared-tailwind/preset';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
presets: [preset],
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
'../../packages/shared-ui/src/**/*.{html,js,svelte,ts}',
|
||||
'../../packages/shared-auth-ui/src/**/*.{html,js,svelte,ts}'
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// ManaCore specific primary blue
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
|
|
@ -19,6 +27,5 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue