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
|
|
@ -16,7 +16,6 @@
|
|||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.43.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"svelte": "^5.39.5",
|
||||
|
|
@ -26,6 +25,18 @@
|
|||
"vite": "^7.1.7"
|
||||
},
|
||||
"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:*",
|
||||
"@phosphor-icons/core": "^2.1.1",
|
||||
"@supabase/supabase-js": "^2.81.1",
|
||||
"date-fns": "^4.1.0",
|
||||
|
|
|
|||
|
|
@ -1,408 +1,7 @@
|
|||
@import '@manacore/shared-tailwind/theme.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--font-body: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
--font-mono: 'Fira Mono', monospace;
|
||||
}
|
||||
|
||||
/* Default Theme: Lume Light */
|
||||
:root {
|
||||
--color-primary: #f8d62b;
|
||||
--color-primary-button: #f8d62b;
|
||||
--color-primary-button-text: #000000;
|
||||
--color-secondary: #d4b200;
|
||||
--color-secondary-button: #ffe9a3;
|
||||
--color-content-bg: #ffffff;
|
||||
--color-content-bg-hover: #f5f5f5;
|
||||
--color-content-page-bg: #ffffff;
|
||||
--color-menu-bg: #dddddd;
|
||||
--color-menu-bg-hover: #cccccc;
|
||||
--color-panel-bg: #e8e8e8;
|
||||
--color-page-bg: #dddddd;
|
||||
--color-text: #2c2c2c;
|
||||
--color-border-light: #f2f2f2;
|
||||
--color-border: #999999;
|
||||
--color-border-strong: #cccccc;
|
||||
--color-error: #e74c3c;
|
||||
}
|
||||
|
||||
/* Lume Dark */
|
||||
:root.dark {
|
||||
--color-primary: #f8d62b;
|
||||
--color-primary-button: #7c6b16;
|
||||
--color-primary-button-text: #ffffff;
|
||||
--color-secondary: #d4b200;
|
||||
--color-secondary-button: #1e1e1e;
|
||||
--color-content-bg: #1e1e1e;
|
||||
--color-content-bg-hover: #333333;
|
||||
--color-content-page-bg: #121212;
|
||||
--color-menu-bg: #101010;
|
||||
--color-menu-bg-hover: #333333;
|
||||
--color-panel-bg: #1a1a1a;
|
||||
--color-page-bg: #101010;
|
||||
--color-text: #ffffff;
|
||||
--color-border-light: #333333;
|
||||
--color-border: #424242;
|
||||
--color-border-strong: #616161;
|
||||
--color-error: #e74c3c;
|
||||
}
|
||||
|
||||
/* Nature Light */
|
||||
:root[data-theme='nature'] {
|
||||
--color-primary: #4caf50;
|
||||
--color-primary-button: #a08500;
|
||||
--color-primary-button-text: #ffffff;
|
||||
--color-secondary: #81c784;
|
||||
--color-secondary-button: #f1f8e9;
|
||||
--color-content-bg: #f1f8e9;
|
||||
--color-content-bg-hover: #e8f5e9;
|
||||
--color-content-page-bg: #ffffff;
|
||||
--color-menu-bg: #e8f5e9;
|
||||
--color-menu-bg-hover: #c8e6c9;
|
||||
--color-panel-bg: #eff8f0;
|
||||
--color-page-bg: #fbfdf8;
|
||||
--color-text: #1b5e20;
|
||||
--color-border-light: #e8f5e9;
|
||||
--color-border: #c8e6c9;
|
||||
--color-border-strong: #a5d6a7;
|
||||
--color-error: #e57373;
|
||||
}
|
||||
|
||||
/* Nature Dark */
|
||||
:root[data-theme='nature'].dark {
|
||||
--color-primary: #4caf50;
|
||||
--color-primary-button: #ff9500;
|
||||
--color-primary-button-text: #000000;
|
||||
--color-secondary: #81c784;
|
||||
--color-secondary-button: #1e1e1e;
|
||||
--color-content-bg: #1e1e1e;
|
||||
--color-content-bg-hover: #2e7d32;
|
||||
--color-content-page-bg: #121212;
|
||||
--color-menu-bg: #252525;
|
||||
--color-menu-bg-hover: #2e7d32;
|
||||
--color-panel-bg: #2a2a2a;
|
||||
--color-page-bg: #121212;
|
||||
--color-text: #ffffff;
|
||||
--color-border-light: #1b5e20;
|
||||
--color-border: #2e7d32;
|
||||
--color-border-strong: #388e3c;
|
||||
--color-error: #cf6679;
|
||||
}
|
||||
|
||||
/* Stone Light */
|
||||
:root[data-theme='stone'] {
|
||||
--color-primary: #607d8b;
|
||||
--color-primary-button: #ff9500;
|
||||
--color-primary-button-text: #000000;
|
||||
--color-secondary: #90a4ae;
|
||||
--color-secondary-button: #eceff1;
|
||||
--color-content-bg: #eceff1;
|
||||
--color-content-bg-hover: #e0e6ea;
|
||||
--color-content-page-bg: #ffffff;
|
||||
--color-menu-bg: #e0e6ea;
|
||||
--color-menu-bg-hover: #cfd8dc;
|
||||
--color-panel-bg: #e8edf0;
|
||||
--color-page-bg: #f5f7f9;
|
||||
--color-text: #263238;
|
||||
--color-border-light: #eceff1;
|
||||
--color-border: #cfd8dc;
|
||||
--color-border-strong: #b0bec5;
|
||||
--color-error: #ef5350;
|
||||
}
|
||||
|
||||
/* Stone Dark */
|
||||
:root[data-theme='stone'].dark {
|
||||
--color-primary: #78909c;
|
||||
--color-primary-button: #ff9500;
|
||||
--color-primary-button-text: #000000;
|
||||
--color-secondary: #90a4ae;
|
||||
--color-secondary-button: #1e1e1e;
|
||||
--color-content-bg: #1e1e1e;
|
||||
--color-content-bg-hover: #37474f;
|
||||
--color-content-page-bg: #121212;
|
||||
--color-menu-bg: #252525;
|
||||
--color-menu-bg-hover: #37474f;
|
||||
--color-panel-bg: #2a2a2a;
|
||||
--color-page-bg: #121212;
|
||||
--color-text: #ffffff;
|
||||
--color-border-light: #37474f;
|
||||
--color-border: #455a64;
|
||||
--color-border-strong: #546e7a;
|
||||
--color-error: #cf6679;
|
||||
}
|
||||
|
||||
/* Ocean Light */
|
||||
:root[data-theme='ocean'] {
|
||||
--color-primary: #039be5;
|
||||
--color-primary-button: #ff9500;
|
||||
--color-primary-button-text: #000000;
|
||||
--color-secondary: #4fc3f7;
|
||||
--color-secondary-button: #e1f5fe;
|
||||
--color-content-bg: #e1f5fe;
|
||||
--color-content-bg-hover: #b3e5fc;
|
||||
--color-content-page-bg: #ffffff;
|
||||
--color-menu-bg: #e1f5fe;
|
||||
--color-menu-bg-hover: #b3e5fc;
|
||||
--color-panel-bg: #ecf8fe;
|
||||
--color-page-bg: #f5fcff;
|
||||
--color-text: #01579b;
|
||||
--color-border-light: #e1f5fe;
|
||||
--color-border: #b3e5fc;
|
||||
--color-border-strong: #81d4fa;
|
||||
--color-error: #ef5350;
|
||||
}
|
||||
|
||||
/* Ocean Dark */
|
||||
:root[data-theme='ocean'].dark {
|
||||
--color-primary: #039be5;
|
||||
--color-primary-button: #ff9500;
|
||||
--color-primary-button-text: #000000;
|
||||
--color-secondary: #4fc3f7;
|
||||
--color-secondary-button: #1e1e1e;
|
||||
--color-content-bg: #1e1e1e;
|
||||
--color-content-bg-hover: #0277bd;
|
||||
--color-content-page-bg: #121212;
|
||||
--color-menu-bg: #252525;
|
||||
--color-menu-bg-hover: #0277bd;
|
||||
--color-panel-bg: #2a2a2a;
|
||||
--color-page-bg: #121212;
|
||||
--color-text: #ffffff;
|
||||
--color-border-light: #01579b;
|
||||
--color-border: #0277bd;
|
||||
--color-border-strong: #0288d1;
|
||||
--color-error: #cf6679;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
h1 {
|
||||
@apply text-3xl font-bold;
|
||||
color: var(--color-text);
|
||||
}
|
||||
h2 {
|
||||
@apply text-2xl font-semibold;
|
||||
color: var(--color-text);
|
||||
}
|
||||
h3 {
|
||||
@apply text-xl font-semibold;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply rounded-lg px-4 py-2 font-semibold transition-colors;
|
||||
background-color: var(--color-primary-button);
|
||||
color: var(--color-primary-button-text);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply rounded-lg px-4 py-2 font-semibold transition-colors;
|
||||
background-color: var(--color-secondary-button);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--color-content-bg-hover);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply rounded-lg px-4 py-2 font-semibold transition-colors;
|
||||
background-color: var(--color-error);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply w-full rounded-lg px-4 py-2 transition-colors;
|
||||
background-color: var(--color-content-bg);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-lg p-6 shadow-sm;
|
||||
background-color: var(--color-content-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Header & Navigation */
|
||||
.header-style {
|
||||
background-color: var(--color-menu-bg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
@apply text-2xl font-bold;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@apply transition-colors;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.user-email {
|
||||
@apply text-sm;
|
||||
color: var(--color-text);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.main-content {
|
||||
background-color: var(--color-page-bg);
|
||||
}
|
||||
|
||||
/* Selected/Active State */
|
||||
.bg-selected {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
}
|
||||
|
||||
/* Status Badge Colors */
|
||||
.status-completed {
|
||||
background-color: rgba(76, 175, 80, 0.15);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 15%, transparent);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: color-mix(in srgb, var(--color-error) 15%, transparent);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.status-default {
|
||||
background-color: color-mix(in srgb, var(--color-text) 10%, transparent);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Info/Alert Boxes */
|
||||
.info-box {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary) 30%, transparent);
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.spinner-border {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Focus Ring */
|
||||
.focus\:ring-primary:focus {
|
||||
--tw-ring-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.focus\:ring-2:focus {
|
||||
box-shadow: 0 0 0 2px var(--tw-ring-color, var(--color-primary));
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Theme Color Utilities - in utilities layer for @apply support */
|
||||
.bg-content {
|
||||
background-color: var(--color-content-bg);
|
||||
}
|
||||
|
||||
.bg-content-hover {
|
||||
background-color: var(--color-content-bg-hover);
|
||||
}
|
||||
|
||||
.hover\:bg-content-hover:hover {
|
||||
background-color: var(--color-content-bg-hover);
|
||||
}
|
||||
|
||||
.bg-menu {
|
||||
background-color: var(--color-menu-bg);
|
||||
}
|
||||
|
||||
.bg-menu-hover {
|
||||
background-color: var(--color-menu-bg-hover);
|
||||
}
|
||||
|
||||
.hover\:bg-menu-hover:hover {
|
||||
background-color: var(--color-menu-bg-hover);
|
||||
}
|
||||
|
||||
.bg-panel {
|
||||
background-color: var(--color-panel-bg);
|
||||
}
|
||||
|
||||
.bg-page {
|
||||
background-color: var(--color-page-bg);
|
||||
}
|
||||
|
||||
.border-theme {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.border-theme-light {
|
||||
border-color: var(--color-border-light);
|
||||
}
|
||||
|
||||
.text-theme {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.text-theme-secondary {
|
||||
color: var(--color-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.text-theme-muted {
|
||||
color: var(--color-text);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.bg-primary-button {
|
||||
background-color: var(--color-primary-button);
|
||||
}
|
||||
|
||||
.text-primary-button-text {
|
||||
color: var(--color-primary-button-text);
|
||||
}
|
||||
|
||||
.bg-secondary-button {
|
||||
background-color: var(--color-secondary-button);
|
||||
}
|
||||
}
|
||||
@import '@manacore/shared-tailwind/components.css';
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
}
|
||||
|
||||
if (data && data.advice) {
|
||||
advice = data.advice as AdviceData;
|
||||
advice = data.advice as unknown as AdviceData;
|
||||
currentIndex = 0; // Reset to first section
|
||||
} else {
|
||||
advice = null;
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@
|
|||
$effect(() => {
|
||||
if (selectedApp !== null && modalScrollContainer) {
|
||||
setTimeout(() => {
|
||||
if (selectedApp === null) return;
|
||||
const cardWidth = 360 + 24; // card width + gap
|
||||
const scrollPosition = selectedApp * cardWidth;
|
||||
modalScrollContainer?.scrollTo({
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { initializeAppleAuth, signInWithApple, waitForAppleAuth } from '$lib/utils/appleAuth';
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
let { onError }: Props = $props();
|
||||
|
||||
// State
|
||||
let isLoading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let sdkLoaded = $state(false);
|
||||
|
||||
// Handle Apple Sign-In click
|
||||
async function handleAppleSignIn() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Redirect to Apple (will come back to /auth/apple-callback)
|
||||
await signInWithApple();
|
||||
} catch (err) {
|
||||
console.error('Error initiating Apple Sign-In:', err);
|
||||
error = err instanceof Error ? err.message : 'Failed to initiate Apple Sign-In';
|
||||
onError?.(err instanceof Error ? err : new Error('Unknown error during Apple Sign-In'));
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Apple SDK on mount
|
||||
onMount(async () => {
|
||||
try {
|
||||
// Wait for Apple ID SDK to load
|
||||
await waitForAppleAuth();
|
||||
|
||||
// Initialize Apple Sign-In
|
||||
const initialized = initializeAppleAuth();
|
||||
if (initialized) {
|
||||
sdkLoaded = true;
|
||||
} else {
|
||||
// Don't show error - just hide the button if not configured
|
||||
console.warn('Apple Sign-In not configured - hiding button');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading Apple Sign-In:', err);
|
||||
// Don't show error - just hide the button
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Only show if SDK loaded successfully (i.e., properly configured) -->
|
||||
{#if sdkLoaded}
|
||||
<div class="space-y-3">
|
||||
{#if error}
|
||||
<div class="rounded-xl bg-red-500/20 border border-red-500/30 p-3 text-sm text-red-500">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Apple Sign-In Button -->
|
||||
<button
|
||||
onclick={handleAppleSignIn}
|
||||
disabled={isLoading}
|
||||
class="flex h-14 w-full items-center justify-center gap-2 rounded-xl bg-black border border-gray-800 px-4 font-medium text-white transition-all hover:bg-gray-900 disabled:opacity-50"
|
||||
>
|
||||
{#if isLoading}
|
||||
<div class="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent"></div>
|
||||
{:else}
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<span>{isLoading ? 'Signing in...' : 'Continue with Apple'}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
let { src, duration }: { src: string; duration?: number } = $props();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { recording } from '$lib/stores/recording';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
let mediaRecorder: MediaRecorder | null = null;
|
||||
let audioChunks: Blob[] = [];
|
||||
let stream: MediaStream | null = null;
|
||||
let durationInterval: number;
|
||||
let durationInterval: ReturnType<typeof setInterval> | undefined;
|
||||
let startTime: number = 0;
|
||||
|
||||
let hasPermission = $state(false);
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
<script lang="ts">
|
||||
export type BillingCycle = 'monthly' | 'yearly';
|
||||
|
||||
interface Props {
|
||||
billingCycle: BillingCycle;
|
||||
onChange: (cycle: BillingCycle) => void;
|
||||
yearlyDiscount?: string;
|
||||
}
|
||||
|
||||
let { billingCycle, onChange, yearlyDiscount = '33%' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mb-2 flex max-w-lg rounded-lg p-1 bg-menu">
|
||||
<button
|
||||
onclick={() => onChange('monthly')}
|
||||
class="flex flex-1 items-center justify-center rounded-md py-3 transition-colors"
|
||||
class:bg-content={billingCycle === 'monthly'}
|
||||
class:text-mana={billingCycle === 'monthly'}
|
||||
class:font-bold={billingCycle === 'monthly'}
|
||||
class:text-theme={billingCycle !== 'monthly'}
|
||||
>
|
||||
<span class="text-sm">
|
||||
Monatlich
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => onChange('yearly')}
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-md py-3 transition-colors"
|
||||
class:bg-content={billingCycle === 'yearly'}
|
||||
class:text-mana={billingCycle === 'yearly'}
|
||||
class:font-bold={billingCycle === 'yearly'}
|
||||
class:text-theme={billingCycle !== 'yearly'}
|
||||
>
|
||||
<span class="text-sm">
|
||||
Jährlich
|
||||
</span>
|
||||
{#if yearlyDiscount}
|
||||
<span class="rounded-xl bg-mana px-2 py-1 text-xs font-bold text-white">
|
||||
-{yearlyDiscount}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { createAuthClient } from '$lib/supabaseClient';
|
||||
import Modal from './Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
|
||||
interface Prompt {
|
||||
id: string;
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
}
|
||||
|
||||
// Sort prompts by sort_order (ascending) then created_at (descending)
|
||||
const sortedPrompts = [...promptsData].sort((a, b) => {
|
||||
const sortedPrompts = [...(promptsData as Prompt[])].sort((a, b) => {
|
||||
// First sort by sort_order (ascending)
|
||||
if (a.sort_order !== undefined && b.sort_order !== undefined) {
|
||||
if (a.sort_order !== b.sort_order) {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
blueprints = data || [];
|
||||
blueprints = (data || []) as Blueprint[];
|
||||
} catch (err) {
|
||||
console.error('Unexpected error:', err);
|
||||
error = $t('errors.unexpected');
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
<script lang="ts">
|
||||
export interface CostItem {
|
||||
action: string;
|
||||
actionKey?: string;
|
||||
cost: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
costs: CostItem[];
|
||||
}
|
||||
|
||||
let { costs }: Props = $props();
|
||||
|
||||
// Icon mapping
|
||||
const iconPaths: Record<string, string> = {
|
||||
'mic-outline':
|
||||
'M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z',
|
||||
'chatbubble-outline':
|
||||
'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
|
||||
'add-circle-outline':
|
||||
'M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'copy-outline':
|
||||
'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl p-4 bg-content border border-theme">
|
||||
<h3 class="mb-4 text-xl font-bold text-theme">Mana-Kosten</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
{#each costs as item}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
class="mr-2 h-[18px] w-[18px]"
|
||||
fill="none"
|
||||
stroke="#4287f5"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d={iconPaths[item.icon] || iconPaths['mic-outline']} />
|
||||
</svg>
|
||||
<p class="text-sm text-theme-secondary">
|
||||
{item.action}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-base font-semibold text-theme">
|
||||
{item.cost} Mana
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { initializeGoogleAuth, renderGoogleButton, waitForGoogleAuth } from '$lib/utils/googleAuth';
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
let { onSuccess, onError }: Props = $props();
|
||||
|
||||
// State
|
||||
let buttonContainer: HTMLDivElement;
|
||||
let isLoading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Handle Google Sign-In callback
|
||||
async function handleGoogleSignIn(idToken: string) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
console.log('Google Sign-In successful, received ID token');
|
||||
|
||||
// Call auth store's signInWithGoogle method
|
||||
// This handles everything: middleware call, token storage, state update
|
||||
const result = await auth.signInWithGoogle(idToken);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to authenticate with Google');
|
||||
}
|
||||
|
||||
console.log('Successfully authenticated with middleware');
|
||||
|
||||
// Navigate to dashboard
|
||||
goto('/dashboard');
|
||||
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
console.error('Error during Google Sign-In:', err);
|
||||
error = err instanceof Error ? err.message : 'Google Sign-In failed';
|
||||
onError?.(err instanceof Error ? err : new Error('Unknown error during Google Sign-In'));
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Google Sign-In on mount
|
||||
onMount(async () => {
|
||||
try {
|
||||
// Wait for Google Identity Services to load
|
||||
await waitForGoogleAuth();
|
||||
|
||||
// Initialize with callback
|
||||
initializeGoogleAuth(handleGoogleSignIn);
|
||||
|
||||
// Render the button
|
||||
if (buttonContainer) {
|
||||
renderGoogleButton(buttonContainer, {
|
||||
type: 'standard',
|
||||
theme: 'outline',
|
||||
size: 'large',
|
||||
text: 'signin_with',
|
||||
shape: 'pill'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error initializing Google Sign-In:', err);
|
||||
error = 'Failed to load Google Sign-In';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<div class="rounded-xl bg-red-500/20 border border-red-500/30 p-3 text-sm text-red-500 mb-2">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Google button container with fixed height to prevent layout shift -->
|
||||
<div bind:this={buttonContainer} class="relative w-full google-btn-wrapper" style="min-height: 56px;">
|
||||
{#if isLoading}
|
||||
<div class="absolute inset-0 flex items-center justify-center rounded-xl bg-menu/80 backdrop-blur-sm z-10">
|
||||
<div class="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.google-btn-wrapper > div) {
|
||||
width: 100% !important;
|
||||
height: 56px !important;
|
||||
}
|
||||
|
||||
:global(.google-btn-wrapper iframe) {
|
||||
height: 56px !important;
|
||||
border-radius: 0.75rem !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Fallback message if Google SDK doesn't load -->
|
||||
<noscript>
|
||||
<div class="rounded-xl bg-yellow-500/20 border border-yellow-500/30 p-3 text-sm text-yellow-600 dark:text-yellow-400">
|
||||
Please enable JavaScript to use Google Sign-In
|
||||
</div>
|
||||
</noscript>
|
||||
|
|
@ -1,21 +1,18 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Central Icon Component for Memoro Web App
|
||||
* Uses Phosphor Icons (Bold weight) from @phosphor-icons/core
|
||||
*
|
||||
* Usage:
|
||||
* <Icon name="user-plus" size={24} />
|
||||
* <Icon name="sign-in" size={20} class="text-primary" />
|
||||
* Icon Component - Re-exports from @manacore/shared-icons
|
||||
* Uses Phosphor Icons (Bold weight)
|
||||
*/
|
||||
import { iconPaths } from './icons/iconPaths';
|
||||
import { iconPaths } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
name: keyof typeof iconPaths;
|
||||
size?: number;
|
||||
class?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { name, size = 24, class: className = '' }: Props = $props();
|
||||
let { name, size = 24, class: className = '', color }: Props = $props();
|
||||
|
||||
const path = $derived(iconPaths[name]);
|
||||
</script>
|
||||
|
|
@ -25,7 +22,7 @@
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="currentColor"
|
||||
fill={color || 'currentColor'}
|
||||
viewBox="0 0 256 256"
|
||||
class={className}
|
||||
aria-hidden="true"
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
color?: string;
|
||||
size?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { color = '#0099FF', size = 24, class: className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" class={className}>
|
||||
<path
|
||||
d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
icon?: Snippet;
|
||||
children: Snippet;
|
||||
footer?: Snippet;
|
||||
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
let { visible, onClose, title, icon, children, footer, maxWidth = 'lg', showHeader = true }: Props = $props();
|
||||
|
||||
const maxWidthClasses = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl',
|
||||
'2xl': 'max-w-2xl',
|
||||
'3xl': 'max-w-3xl'
|
||||
};
|
||||
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && visible) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if visible}
|
||||
<!-- Modal Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
||||
onclick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- Modal Content -->
|
||||
<div
|
||||
class="relative flex max-h-[90vh] w-full {maxWidthClasses[
|
||||
maxWidth
|
||||
]} flex-col rounded-xl border border-theme bg-menu shadow-xl"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{#if showHeader}
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-theme">
|
||||
<div class="flex items-center gap-2 flex-1">
|
||||
{#if icon}
|
||||
{@render icon()}
|
||||
{/if}
|
||||
{#if title}
|
||||
<Text variant="large" weight="semibold">
|
||||
{title}
|
||||
</Text>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="p-2 rounded-full hover:bg-menu-hover transition-colors"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<Icon name="x" size={20} class="text-theme-muted" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Body (scrollable) -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<!-- Footer (optional) -->
|
||||
{#if footer}
|
||||
<div class="border-t border-theme p-6">
|
||||
{@render footer()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
<script lang="ts">
|
||||
import SubscriptionButton from './SubscriptionButton.svelte';
|
||||
import ManaIcon from './ManaIcon.svelte';
|
||||
|
||||
export interface Package {
|
||||
id: string;
|
||||
name: string;
|
||||
manaAmount: number;
|
||||
price: number;
|
||||
priceString?: string;
|
||||
currencyCode?: string;
|
||||
isTeamPackage?: boolean;
|
||||
isEnterprisePackage?: boolean;
|
||||
popular?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
package: Package;
|
||||
onSelect: (packageId: string) => void;
|
||||
}
|
||||
|
||||
let { package: pkg, onSelect }: Props = $props();
|
||||
|
||||
function formatPrice(pkg: Package) {
|
||||
return pkg.priceString || `${pkg.price.toFixed(2).replace('.', ',')}€`;
|
||||
}
|
||||
|
||||
// Package-specific colors and background sizes
|
||||
function getPackageStyles() {
|
||||
switch (pkg.id) {
|
||||
case 'Mana_Potion_Small_v1':
|
||||
return { bg: '#E3F2FD', icon: '#2196F3', bgSize: '45%' };
|
||||
case 'Mana_Potion_Medium_v1':
|
||||
return { bg: '#BBDEFB', icon: '#1976D2', bgSize: '60%' };
|
||||
case 'Mana_Potion_Large_v1':
|
||||
return { bg: '#90CAF9', icon: '#1565C0', bgSize: '75%' };
|
||||
case 'Mana_Potion_Giant_v2':
|
||||
return { bg: '#64B5F6', icon: '#0D47A1', bgSize: '90%' };
|
||||
default:
|
||||
return { bg: '#E1F5FE', icon: '#0288D1', bgSize: '50%' };
|
||||
}
|
||||
}
|
||||
|
||||
const packageStyles = $derived(getPackageStyles());
|
||||
|
||||
// Hover state
|
||||
let isHovered = $state(false);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative rounded-xl p-4 transition-all duration-200 bg-content border hover:-translate-y-0.5 hover:shadow-lg"
|
||||
class:border-mana={pkg.popular}
|
||||
class:border-theme={!pkg.popular}
|
||||
onmouseenter={() => (isHovered = true)}
|
||||
onmouseleave={() => (isHovered = false)}
|
||||
>
|
||||
{#if pkg.popular}
|
||||
<div class="absolute -top-3 right-4 rounded-xl bg-mana px-3 py-1 text-xs font-bold text-white">
|
||||
Popular
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Package Name -->
|
||||
<h3 class="mb-4 text-center text-lg font-bold text-theme">
|
||||
{pkg.name}
|
||||
</h3>
|
||||
|
||||
<!-- Three column layout -->
|
||||
<div class="mb-5 flex justify-between gap-2">
|
||||
<!-- Mana Icon with background -->
|
||||
<div class="flex aspect-square flex-1 items-center justify-center rounded-xl bg-menu" style="min-height: 80px;">
|
||||
<div
|
||||
class="flex items-center justify-center rounded-lg"
|
||||
style="width: {packageStyles.bgSize}; height: {packageStyles.bgSize}; background-color: {packageStyles.bg};"
|
||||
>
|
||||
<ManaIcon size={32} color={packageStyles.icon} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mana Amount -->
|
||||
<div class="flex aspect-square flex-1 flex-col items-center justify-center rounded-xl bg-menu" style="min-height: 80px;">
|
||||
<p class="mb-0.5 text-2xl font-bold text-theme">
|
||||
{pkg.manaAmount}
|
||||
</p>
|
||||
<p class="text-center text-xs text-theme-secondary">Mana</p>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="flex aspect-square flex-1 flex-col items-center justify-center rounded-xl bg-menu" style="min-height: 80px;">
|
||||
<p class="text-xl font-bold text-theme">
|
||||
{formatPrice(pkg)}
|
||||
</p>
|
||||
<p class="mt-0.5 text-[10px] text-theme-secondary">Einmalig</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SubscriptionButton
|
||||
label="Kaufen"
|
||||
onclick={() => onSelect(pkg.id)}
|
||||
iconName="arrow-forward-outline"
|
||||
leftIconName="cart-outline"
|
||||
variant={pkg.popular ? 'accent' : 'primary'}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Toggle from './Toggle.svelte';
|
||||
import { Toggle } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import { user } from '$lib/stores/auth';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
onLogout: () => void;
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string;
|
||||
onclick: () => void;
|
||||
iconName?: string;
|
||||
leftIconName?: string;
|
||||
variant?: 'primary' | 'secondary' | 'accent';
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
onclick,
|
||||
iconName = 'arrow-forward-outline',
|
||||
leftIconName = 'cart-outline',
|
||||
variant = 'primary',
|
||||
disabled = false
|
||||
}: Props = $props();
|
||||
|
||||
// Icon mapping (simple SVG paths for common icons)
|
||||
const iconPaths: Record<string, string> = {
|
||||
'arrow-forward-outline': 'M5 12h14M12 5l7 7-7 7',
|
||||
'checkmark-circle-outline': 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'cart-outline':
|
||||
'M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z'
|
||||
};
|
||||
</script>
|
||||
|
||||
<button
|
||||
{disabled}
|
||||
onclick={disabled ? undefined : onclick}
|
||||
class="flex w-full h-12 items-center justify-between rounded-lg border px-4 font-medium transition-all duration-200"
|
||||
class:bg-mana={variant === 'accent' && !disabled}
|
||||
class:border-mana={variant === 'accent' && !disabled}
|
||||
class:text-white={variant === 'accent' && !disabled}
|
||||
class:bg-menu={variant === 'primary' && !disabled}
|
||||
class:border-theme={variant === 'primary' && !disabled}
|
||||
class:text-theme={variant === 'primary' && !disabled}
|
||||
class:bg-content={variant === 'secondary' && !disabled}
|
||||
class:hover:bg-menu-hover={!disabled}
|
||||
class:opacity-50={disabled}
|
||||
class:cursor-not-allowed={disabled}
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg
|
||||
class="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d={iconPaths[leftIconName] || iconPaths['cart-outline']} />
|
||||
</svg>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
|
||||
<svg
|
||||
class="ml-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d={iconPaths[iconName] || iconPaths['arrow-forward-outline']} />
|
||||
</svg>
|
||||
</button>
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
<script lang="ts">
|
||||
import SubscriptionButton from './SubscriptionButton.svelte';
|
||||
import ManaIcon from './ManaIcon.svelte';
|
||||
|
||||
export interface SubscriptionPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
priceString?: string;
|
||||
currencyCode?: string;
|
||||
priceBreakdown?: string;
|
||||
monthlyMana: number;
|
||||
canGiftMana: boolean;
|
||||
popular?: boolean;
|
||||
billingCycle?: 'monthly' | 'yearly';
|
||||
monthlyEquivalent?: number;
|
||||
isTeamSubscription?: boolean;
|
||||
isEnterpriseSubscription?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
plan: SubscriptionPlan;
|
||||
onSelect: (planId: string) => void;
|
||||
isCurrentPlan?: boolean;
|
||||
isLegacy?: boolean;
|
||||
}
|
||||
|
||||
let { plan, onSelect, isCurrentPlan = false, isLegacy = false }: Props = $props();
|
||||
|
||||
function formatPrice(plan: SubscriptionPlan) {
|
||||
return plan.priceString || `${plan.price.toFixed(2).replace('.', ',')}€`;
|
||||
}
|
||||
|
||||
// Tier-specific background colors and sizes for Mana icon
|
||||
function getTierStyles() {
|
||||
switch (plan.id) {
|
||||
case 'free':
|
||||
return { bg: '#F5F5F5', icon: '#9E9E9E', bgSize: '30%' };
|
||||
case 'Mana_Stream_Small_v1':
|
||||
case 'Mana_Stream_Small_Yearly_v1':
|
||||
return { bg: '#E3F2FD', icon: '#2196F3', bgSize: '45%' };
|
||||
case 'Mana_Stream_Medium_v1':
|
||||
case 'Mana_Stream_Medium_Yearly_v1':
|
||||
return { bg: '#BBDEFB', icon: '#1976D2', bgSize: '60%' };
|
||||
case 'Mana_Stream_Large_v1':
|
||||
case 'Mana_Stream_Large_Yearly_v1':
|
||||
return { bg: '#90CAF9', icon: '#1565C0', bgSize: '75%' };
|
||||
case 'Mana_Stream_Giant_v1':
|
||||
case 'Mana_Stream_Giant_Yearly_v1':
|
||||
return { bg: '#64B5F6', icon: '#0D47A1', bgSize: '90%' };
|
||||
default:
|
||||
return { bg: '#E1F5FE', icon: '#0288D1', bgSize: '50%' };
|
||||
}
|
||||
}
|
||||
|
||||
const tierStyles = $derived(getTierStyles());
|
||||
|
||||
// Hover state
|
||||
let isHovered = $state(false);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative rounded-xl p-4 transition-all duration-200 bg-content border hover:-translate-y-0.5 hover:shadow-lg"
|
||||
class:border-2={isCurrentPlan}
|
||||
class:border-mana={isCurrentPlan || plan.popular}
|
||||
class:border-theme={!isCurrentPlan && !plan.popular}
|
||||
onmouseenter={() => (isHovered = true)}
|
||||
onmouseleave={() => (isHovered = false)}
|
||||
>
|
||||
{#if isCurrentPlan}
|
||||
<div class="absolute -top-3 left-4 rounded-xl bg-mana px-3 py-1 text-xs font-bold text-white">
|
||||
{isLegacy ? 'Legacy Plan' : 'Current Plan'}
|
||||
</div>
|
||||
{/if}
|
||||
{#if plan.popular && !isCurrentPlan}
|
||||
<div class="absolute -top-3 right-4 rounded-xl bg-mana px-3 py-1 text-xs font-bold text-white">
|
||||
Popular
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Tier Name -->
|
||||
<h3 class="mb-4 text-center text-lg font-bold text-theme">
|
||||
{plan.name}
|
||||
</h3>
|
||||
|
||||
<!-- Three column layout -->
|
||||
<div class="mb-5 flex justify-between gap-2">
|
||||
<!-- Mana Icon with background -->
|
||||
<div class="flex aspect-square flex-1 items-center justify-center rounded-xl bg-menu" style="min-height: 80px;">
|
||||
<div
|
||||
class="flex items-center justify-center rounded-lg"
|
||||
style="width: {tierStyles.bgSize}; height: {tierStyles.bgSize}; background-color: {tierStyles.bg};"
|
||||
>
|
||||
<ManaIcon size={32} color={tierStyles.icon} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mana Amount -->
|
||||
<div class="flex aspect-square flex-1 flex-col items-center justify-center rounded-xl bg-menu" style="min-height: 80px;">
|
||||
<p class="mb-0.5 text-2xl font-bold text-theme">
|
||||
{plan.monthlyMana}
|
||||
</p>
|
||||
<p class="text-center text-xs text-theme-secondary">pro Monat</p>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="flex aspect-square flex-1 flex-col items-center justify-center rounded-xl bg-menu" style="min-height: 80px;">
|
||||
<p class="text-xl font-bold text-theme">
|
||||
{formatPrice(plan)}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-theme-secondary">
|
||||
{plan.billingCycle === 'yearly' ? 'pro Jahr' : 'pro Monat'}
|
||||
</p>
|
||||
{#if plan.billingCycle === 'yearly' && plan.monthlyEquivalent}
|
||||
<p class="mt-0 text-[9px] text-theme-secondary">
|
||||
({plan.monthlyEquivalent.toFixed(2).replace('.', ',')}€/Monat)
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button only show if NOT free plan -->
|
||||
{#if plan.id !== 'free'}
|
||||
<SubscriptionButton
|
||||
label={isCurrentPlan
|
||||
? isLegacy
|
||||
? 'Dein Legacy-Plan'
|
||||
: 'Dein Plan'
|
||||
: 'Kaufen'}
|
||||
onclick={() => onSelect(plan.id)}
|
||||
iconName={isCurrentPlan ? 'checkmark-circle-outline' : 'arrow-forward-outline'}
|
||||
variant={isCurrentPlan ? 'secondary' : plan.popular ? 'accent' : 'primary'}
|
||||
disabled={isCurrentPlan}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { Tag } from '$lib/types/memo.types';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal, Text } from '@manacore/shared-ui';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
|
||||
interface Props {
|
||||
tag: Tag;
|
||||
|
|
@ -65,9 +64,9 @@
|
|||
{#snippet children()}
|
||||
<!-- Tag Name -->
|
||||
<div class="mb-6">
|
||||
<Text variant="small" weight="medium" class="mb-2 block" for="tag-name">
|
||||
<label for="tag-name" class="mb-2 block text-sm font-medium text-theme">
|
||||
Tag-Name
|
||||
</Text>
|
||||
</label>
|
||||
<input
|
||||
id="tag-name"
|
||||
type="text"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
let newTagName = $state('');
|
||||
let newTagColor = $state('#3b82f6');
|
||||
|
||||
const tagService = new TagService(supabase);
|
||||
const tagService = new TagService();
|
||||
|
||||
onMount(async () => {
|
||||
if ($tags.length === 0) {
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
isOn: boolean;
|
||||
onToggle: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let { isOn = false, onToggle, disabled = false }: Props = $props();
|
||||
|
||||
function handleToggle() {
|
||||
if (!disabled) {
|
||||
onToggle(!isOn);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={handleToggle}
|
||||
class="relative h-8 w-14 flex-shrink-0 rounded-full transition-colors {isOn
|
||||
? 'bg-primary'
|
||||
: 'bg-menu'} {disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}"
|
||||
role="switch"
|
||||
aria-checked={isOn}
|
||||
{disabled}
|
||||
>
|
||||
<span
|
||||
class="absolute top-1 left-1 h-6 w-6 rounded-full bg-white shadow-md transition-transform {isOn
|
||||
? 'translate-x-6'
|
||||
: 'translate-x-0'}"
|
||||
></span>
|
||||
</button>
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
<script lang="ts">
|
||||
export interface UsageData {
|
||||
total: number;
|
||||
lastWeek: number;
|
||||
lastMonth: number;
|
||||
currentMana: number;
|
||||
maxMana: number;
|
||||
history?: Array<{
|
||||
date: string;
|
||||
amount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
usageData: UsageData;
|
||||
currentPlan?: string;
|
||||
}
|
||||
|
||||
let { title, usageData, currentPlan }: Props = $props();
|
||||
|
||||
// Use real credits (this would normally come from a store/API)
|
||||
const currentMana = usageData.currentMana;
|
||||
|
||||
// Calculate used vs available Mana
|
||||
const usedMana = usageData.maxMana - currentMana;
|
||||
const formattedCurrentMana = currentMana.toString();
|
||||
const formattedUsedMana = usedMana.toString();
|
||||
const calculatedPercentage = Math.round((currentMana / usageData.maxMana) * 100);
|
||||
// Minimum 1% for numbers up to 5, so that a small blue bar is always visible
|
||||
const availablePercentage =
|
||||
currentMana <= 5 && currentMana > 0 ? Math.max(1, calculatedPercentage) : calculatedPercentage;
|
||||
</script>
|
||||
|
||||
<div class="rounded-2xl p-5 bg-content border border-theme shadow-lg">
|
||||
<!-- Mana Progress Bar -->
|
||||
<div>
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-xl font-bold text-theme">Dein Mana</h2>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<div class="self-start rounded-xl px-4 py-1.5 bg-menu">
|
||||
<p class="text-xl font-bold text-theme">
|
||||
{formattedCurrentMana}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="relative mb-2 h-4 overflow-hidden rounded-lg bg-menu">
|
||||
<div
|
||||
class="h-full rounded-lg"
|
||||
style="width: {availablePercentage}%; background: linear-gradient(90deg, #4287f5 0%, #66B2FF 100%); box-shadow: 0 0 4px #4287f580;"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Percentage -->
|
||||
<div class="flex justify-between">
|
||||
<p class="text-sm font-medium text-theme-secondary">
|
||||
{availablePercentage}% verfügbar
|
||||
</p>
|
||||
<p class="text-sm font-medium text-theme-secondary">
|
||||
{formattedUsedMana} verbraucht
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Current Plan -->
|
||||
{#if currentPlan}
|
||||
<div class="mt-3 border-t border-theme pt-3">
|
||||
<p class="text-center text-sm font-medium text-theme-secondary">
|
||||
Aktueller Plan: {currentPlan}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<script lang="ts">
|
||||
type TextVariant = 'body' | 'body-secondary' | 'small' | 'large' | 'muted';
|
||||
type TextAlign = 'left' | 'center' | 'right';
|
||||
type TextWeight = 'normal' | 'medium' | 'semibold' | 'bold';
|
||||
|
||||
interface Props {
|
||||
variant?: TextVariant;
|
||||
align?: TextAlign;
|
||||
weight?: TextWeight;
|
||||
class?: string;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'body',
|
||||
align = 'left',
|
||||
weight = 'normal',
|
||||
class: className = '',
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
const variantClasses: Record<TextVariant, string> = {
|
||||
body: 'text-base text-theme leading-relaxed',
|
||||
'body-secondary': 'text-base text-theme-secondary leading-relaxed',
|
||||
small: 'text-sm text-theme',
|
||||
large: 'text-lg text-theme leading-relaxed',
|
||||
muted: 'text-sm text-theme-muted'
|
||||
};
|
||||
|
||||
const alignClasses: Record<TextAlign, string> = {
|
||||
left: 'text-left',
|
||||
center: 'text-center',
|
||||
right: 'text-right'
|
||||
};
|
||||
|
||||
const weightClasses: Record<TextWeight, string> = {
|
||||
normal: 'font-normal',
|
||||
medium: 'font-medium',
|
||||
semibold: 'font-semibold',
|
||||
bold: 'font-bold'
|
||||
};
|
||||
|
||||
const classes = $derived(
|
||||
`${variantClasses[variant]} ${alignClasses[align]} ${weightClasses[weight]} ${className}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<p class={classes}>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as Text } from './Text.svelte';
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { AudioArchiveStats } from '$lib/services/audioStorageService';
|
||||
import { audioStorageService } from '$lib/services/audioStorageService';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
stats: AudioArchiveStats;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
audioFile: AudioFileInfo;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { AudioFileInfo } from '$lib/services/audioStorageService';
|
||||
import AudioFileCard from './AudioFileCard.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
audioFiles: AudioFileInfo[];
|
||||
|
|
|
|||
|
|
@ -1,131 +0,0 @@
|
|||
# Icon System
|
||||
|
||||
Centralized icon management using **Phosphor Icons (Bold weight)** from `@phosphor-icons/core`.
|
||||
|
||||
## Usage
|
||||
|
||||
Import and use the `Icon` component throughout the application:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
</script>
|
||||
|
||||
<!-- Basic usage -->
|
||||
<Icon name="user" size={24} />
|
||||
|
||||
<!-- With custom size -->
|
||||
<Icon name="heart" size={32} />
|
||||
|
||||
<!-- With custom classes -->
|
||||
<Icon name="star" size={20} class="text-primary hover:text-primary-dark" />
|
||||
```
|
||||
|
||||
## Available Icons
|
||||
|
||||
See `iconPaths.ts` for the complete list of available icons. Some commonly used icons:
|
||||
|
||||
### Auth & User
|
||||
- `user`, `user-plus`, `users`, `sign-in`, `sign-out`
|
||||
|
||||
### Navigation
|
||||
- `arrow-left`, `arrow-right`, `arrow-up`, `arrow-down`
|
||||
- `caret-down`, `caret-up`, `caret-left`, `caret-right`
|
||||
|
||||
### Actions
|
||||
- `plus`, `minus`, `x`, `check`
|
||||
- `trash`, `copy`, `share`
|
||||
- `download`, `upload`
|
||||
|
||||
### Media
|
||||
- `play`, `pause`, `microphone`
|
||||
|
||||
### Edit
|
||||
- `pencil`, `pen`, `note-pencil`
|
||||
|
||||
### Files & Folders
|
||||
- `folder`, `folder-open`, `file`
|
||||
|
||||
### UI Elements
|
||||
- `dots-three`, `dots-three-vertical`, `list`
|
||||
- `magnifying-glass`, `eye`, `eye-slash`
|
||||
|
||||
### Misc
|
||||
- `key`, `tag`, `link`, `lock`
|
||||
- `star`, `heart`, `bell`
|
||||
- `calendar`, `clock`, `image`
|
||||
|
||||
## Adding New Icons
|
||||
|
||||
1. Find the icon you need at [phosphoricons.com](https://phosphoricons.com/)
|
||||
2. Locate the bold version in `node_modules/@phosphor-icons/core/assets/bold/`
|
||||
3. Copy the SVG `<path>` content
|
||||
4. Add it to `iconPaths.ts`:
|
||||
|
||||
```typescript
|
||||
export const iconPaths = {
|
||||
// ... existing icons
|
||||
'new-icon': '<path d="..." />',
|
||||
}
|
||||
```
|
||||
|
||||
5. Use it:
|
||||
|
||||
```svelte
|
||||
<Icon name="new-icon" size={24} />
|
||||
```
|
||||
|
||||
## Icon Weight
|
||||
|
||||
All icons use the **Bold** weight for consistency across the application. This provides:
|
||||
- Better visibility
|
||||
- Consistent visual hierarchy
|
||||
- Improved readability at smaller sizes
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
The Icon component is fully typed. TypeScript will autocomplete icon names and show errors if you use an icon that doesn't exist.
|
||||
|
||||
```svelte
|
||||
<!-- ✅ Valid - TypeScript knows this icon exists -->
|
||||
<Icon name="user" />
|
||||
|
||||
<!-- ❌ Error - TypeScript will show an error -->
|
||||
<Icon name="invalid-icon" />
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use semantic names**: Icons should have clear, descriptive names
|
||||
2. **Consistent sizing**: Stick to common sizes (16, 20, 24, 32, 40)
|
||||
3. **Color inheritance**: Icons use `currentColor` - control color via text color classes
|
||||
4. **Accessibility**: Icons are marked with `aria-hidden="true"` - provide text alternatives when needed
|
||||
|
||||
## Examples
|
||||
|
||||
### Button with Icon
|
||||
```svelte
|
||||
<button class="flex items-center gap-2">
|
||||
<Icon name="plus" size={20} />
|
||||
Add Item
|
||||
</button>
|
||||
```
|
||||
|
||||
### Icon Button
|
||||
```svelte
|
||||
<button class="p-2 hover:bg-gray-100 rounded">
|
||||
<Icon name="trash" size={20} class="text-red-500" />
|
||||
</button>
|
||||
```
|
||||
|
||||
### Loading State
|
||||
```svelte
|
||||
<button disabled={loading}>
|
||||
{#if loading}
|
||||
<Icon name="arrow-clockwise" size={20} class="animate-spin" />
|
||||
{:else}
|
||||
<Icon name="upload" size={20} />
|
||||
{/if}
|
||||
Upload
|
||||
</button>
|
||||
```
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
/**
|
||||
* Phosphor Icons (Bold weight) - Official icons from @phosphor-icons/core
|
||||
*
|
||||
* This is a centralized icon catalog for the entire application.
|
||||
* All icons use the Bold weight for consistency.
|
||||
*
|
||||
* To add new icons:
|
||||
* 1. Find the icon in node_modules/@phosphor-icons/core/assets/bold/
|
||||
* 2. Copy the SVG content (the <path> tag)
|
||||
* 3. Add it to this file with a descriptive key
|
||||
*
|
||||
* Usage:
|
||||
* import Icon from '$lib/components/Icon.svelte';
|
||||
* <Icon name="user-plus" size={24} />
|
||||
*/
|
||||
|
||||
export const iconPaths = {
|
||||
// Auth & User
|
||||
'user-plus': '<path d="M256,136a12,12,0,0,1-12,12h-8v8a12,12,0,0,1-24,0v-8h-8a12,12,0,0,1,0-24h8v-8a12,12,0,0,1,24,0v8h8A12,12,0,0,1,256,136Zm-54.81,56.28a12,12,0,1,1-18.38,15.44C169.12,191.42,145,172,108,172c-28.89,0-55.46,12.68-74.81,35.72a12,12,0,0,1-18.38-15.44A124.08,124.08,0,0,1,63.5,156.53a72,72,0,1,1,89,0A124,124,0,0,1,201.19,192.28ZM108,148a48,48,0,1,0-48-48A48.05,48.05,0,0,0,108,148Z"/>',
|
||||
'sign-in': '<path d="M144.49,136.49l-40,40a12,12,0,0,1-17-17L107,140H24a12,12,0,0,1,0-24h83L87.51,96.49a12,12,0,0,1,17-17l40,40A12,12,0,0,1,144.49,136.49ZM200,28H136a12,12,0,0,0,0,24h52V204H136a12,12,0,0,0,0,24h64a12,12,0,0,0,12-12V40A12,12,0,0,0,200,28Z"/>',
|
||||
'sign-out': '<path d="M116,216a12,12,0,0,1-12,12H48a20,20,0,0,1-20-20V48A20,20,0,0,1,48,28h56a12,12,0,0,1,0,24H52V204h52A12,12,0,0,1,116,216Zm108.49-96.49-40-40a12,12,0,0,0-17,17L187,116H104a12,12,0,0,0,0,24h83l-19.52,19.51a12,12,0,0,0,17,17l40-40A12,12,0,0,0,224.49,119.51Z"/>',
|
||||
'user': '<path d="M234.38,210a123.36,123.36,0,0,0-60.78-53.23,76,76,0,1,0-91.2,0A123.36,123.36,0,0,0,21.62,210a12,12,0,1,0,20.77,12c18.12-31.32,50.12-50,85.61-50s67.49,18.69,85.61,50a12,12,0,0,0,20.77-12ZM76,96a52,52,0,1,1,52,52A52.06,52.06,0,0,1,76,96Z"/>',
|
||||
'users': '<path d="M125.18,156.94a64,64,0,1,0-82.36,0,100.23,100.23,0,0,0-39.49,32,12,12,0,0,0,20.54,12.08C36.18,184.88,55.25,172,76,172c22.44,0,41.84,14.31,54.86,40.39a12,12,0,1,0,21.19-11.17A99.33,99.33,0,0,0,125.18,156.94ZM44,108a40,40,0,1,1,40,40A40,40,0,0,1,44,108Zm206.1,97.67a12,12,0,0,1-16.78-2.57C221.44,183.6,202,172,180,172a41.86,41.86,0,0,0-13.54,2.24,12,12,0,0,1-7.84-22.68,64.36,64.36,0,0,0,35.69-27.15,64,64,0,1,0-43.49-45.85,12,12,0,0,1-23.71-3.15,88,88,0,1,1,73.54,84.28,99.33,99.33,0,0,1,26.84,44.53A12,12,0,0,1,250.1,205.67Z"/>',
|
||||
|
||||
// Navigation & Arrows
|
||||
'arrow-left': '<path d="M228,128a12,12,0,0,1-12,12H69l51.52,51.51a12,12,0,0,1-17,17l-72-72a12,12,0,0,1,0-17l72-72a12,12,0,0,1,17,17L69,116H216A12,12,0,0,1,228,128Z"/>',
|
||||
'arrow-right': '<path d="M224.49,136.49l-72,72a12,12,0,0,1-17-17L187,140H40a12,12,0,0,1,0-24H187L135.51,64.48a12,12,0,0,1,17-17l72,72A12,12,0,0,1,224.49,136.49Z"/>',
|
||||
'arrow-up': '<path d="M208.49,152.49a12,12,0,0,1-17,17L140,118v98a12,12,0,0,1-24,0V118L64.49,169.51a12,12,0,0,1-17-17l72-72a12,12,0,0,1,17,0Z"/>',
|
||||
'arrow-down': '<path d="M208.49,168.49l-72,72a12,12,0,0,1-17,0l-72-72a12,12,0,0,1,17-17L116,203V40a12,12,0,0,1,24,0V203l51.51-51.52a12,12,0,0,1,17,17Z"/>',
|
||||
'caret-down': '<path d="M216.49,104.49l-80,80a12,12,0,0,1-17,0l-80-80a12,12,0,0,1,17-17L128,159l71.51-71.52a12,12,0,0,1,17,17Z"/>',
|
||||
'caret-up': '<path d="M216.49,168.49a12,12,0,0,1-17,17L128,114,56.49,185.51a12,12,0,0,1-17-17l80-80a12,12,0,0,1,17,0Z"/>',
|
||||
'caret-left': '<path d="M168.49,199.51a12,12,0,0,1-17,17l-80-80a12,12,0,0,1,0-17l80-80a12,12,0,0,1,17,17L97,128Z"/>',
|
||||
'caret-right': '<path d="M184.49,136.49l-80,80a12,12,0,0,1-17-17L159,128,87.51,56.49a12,12,0,0,1,17-17l80,80A12,12,0,0,1,184.49,136.49Z"/>',
|
||||
|
||||
// Actions
|
||||
'plus': '<path d="M228,128a12,12,0,0,1-12,12H140v76a12,12,0,0,1-24,0V140H40a12,12,0,0,1,0-24h76V40a12,12,0,0,1,24,0v76h76A12,12,0,0,1,228,128Z"/>',
|
||||
'minus': '<path d="M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128Z"/>',
|
||||
'x': '<path d="M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z"/>',
|
||||
'check': '<path d="M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z"/>',
|
||||
'trash': '<path d="M216,48H180V36A28,28,0,0,0,152,8H104A28,28,0,0,0,76,36V48H40a12,12,0,0,0,0,24h4V208a20,20,0,0,0,20,20H192a20,20,0,0,0,20-20V72h4a12,12,0,0,0,0-24ZM100,36a4,4,0,0,1,4-4h48a4,4,0,0,1,4,4V48H100Zm88,168H68V72H188ZM116,104v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Zm48,0v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Z"/>',
|
||||
'copy': '<path d="M216,28H88A12,12,0,0,0,76,40V76H40A12,12,0,0,0,28,88V216a12,12,0,0,0,12,12H168a12,12,0,0,0,12-12V180h36a12,12,0,0,0,12-12V40A12,12,0,0,0,216,28ZM156,204H52V100H156Zm48-48H180V88a12,12,0,0,0-12-12H100V52H204Z"/>',
|
||||
|
||||
// Media
|
||||
'play': '<path d="M240.82,114.67,88.82,26.67a16,16,0,0,0-16.12-.41A15.68,15.68,0,0,0,64,40.36V215.64a15.68,15.68,0,0,0,8.7,14.1,16,16,0,0,0,16.12-.41l152-88a15.76,15.76,0,0,0,0-26.66ZM88,199.93V56.07L216.16,128Z"/>',
|
||||
'pause': '<path d="M200,28H160a20,20,0,0,0-20,20V208a20,20,0,0,0,20,20h40a20,20,0,0,0,20-20V48A20,20,0,0,0,200,28Zm-4,176H164V52h32ZM96,28H56A20,20,0,0,0,36,48V208a20,20,0,0,0,20,20H96a20,20,0,0,0,20-20V48A20,20,0,0,0,96,28ZM92,204H60V52H92Z"/>',
|
||||
'microphone': '<path d="M128,176a52.06,52.06,0,0,0,52-52V64a52,52,0,0,0-104,0v60A52.06,52.06,0,0,0,128,176ZM100,64a28,28,0,0,1,56,0v60a28,28,0,0,1-56,0Zm112,56a12,12,0,0,1-24,0,76,76,0,0,0-152,0,12,12,0,0,1-24,0,100.11,100.11,0,0,1,88-99.26V8a12,12,0,0,1,24,0V20.74A100.11,100.11,0,0,1,212,120Z"/>',
|
||||
'skip-back': '<path d="M201.75,30.52a20,20,0,0,0-20.3.53L68,102V40a12,12,0,0,0-24,0V216a12,12,0,0,0,24,0V154l113.45,71A20,20,0,0,0,212,208.12V47.88A19.86,19.86,0,0,0,201.75,30.52ZM188,200.73,71.7,128,188,55.27Z"/>',
|
||||
'skip-forward': '<path d="M200,28a12,12,0,0,0-12,12v62l-113.45-71A20,20,0,0,0,44,47.88V208.12A20,20,0,0,0,74.55,225L188,154v62a12,12,0,0,0,24,0V40A12,12,0,0,0,200,28ZM68,200.73V55.27L184.3,128Z"/>',
|
||||
|
||||
// Edit
|
||||
'pencil': '<path d="M230.14,70.54,185.46,25.85a20,20,0,0,0-28.29,0L33.86,149.17A19.85,19.85,0,0,0,28,163.31V208a20,20,0,0,0,20,20H92.69a19.86,19.86,0,0,0,14.14-5.86L230.14,98.82a20,20,0,0,0,0-28.28ZM91,204H52V165l84-84,39,39ZM192,103,153,64l18.34-18.34,39,39Z"/>',
|
||||
'pen': '<path d="M228.12,67.07,188.93,27.88a20,20,0,0,0-28.28,0L33.88,154.64A19.86,19.86,0,0,0,28,168.78V208a20,20,0,0,0,20,20H87.22a19.86,19.86,0,0,0,14.14-5.86L228.12,95.35a20,20,0,0,0,0-28.28ZM91,204H52V165l84-84,39,39ZM192,103,153,64l21.52-21.52L213.49,81.5Z"/>',
|
||||
'note-pencil': '<path d="M228,128v80a20,20,0,0,1-20,20H48a20,20,0,0,1-20-20V48A20,20,0,0,1,48,28h80a12,12,0,0,1,0,24H52V204H204V132a12,12,0,0,1,24,0Zm-26.34-84.49a12,12,0,0,0-17,0L112.3,115.86a20,20,0,0,0-5.59,11.63l-5.46,32.74a12,12,0,0,0,14.79,14.05l32.73-8.73a20.08,20.08,0,0,0,11.07-6.41l72.4-72.39a12,12,0,0,0,0-17Zm-47.29,63.9-18.64,5-3.53-3.52,4.95-18.63,40.31-40.31,16.72,16.72Z"/>',
|
||||
|
||||
// Files & Folders
|
||||
'folder': '<path d="M216,68H130.67L102.93,51.2a20.12,20.12,0,0,0-11.07-3.2H40A20,20,0,0,0,20,68V200a20,20,0,0,0,20,20H216a20,20,0,0,0,20-20V88A20,20,0,0,0,216,68Zm-4,128H44V92H212Z"/>',
|
||||
'folder-open': '<path d="M248.23,112.31l-60,112A12,12,0,0,1,177.6,228H36a20,20,0,0,1-20-20V88A20,20,0,0,1,36,68H88a11.93,11.93,0,0,1,7.88,2.93l29.51,25.89A4,4,0,0,0,128,98h88a12,12,0,0,1,10.62,6.38A11.88,11.88,0,0,1,248.23,112.31ZM218.72,122H123.88L96,99.47,40,92v112H172.52Z"/>',
|
||||
'file': '<path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A20,20,0,0,0,36,44V212a20,20,0,0,0,20,20H200a20,20,0,0,0,20-20V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM196,208H60V48h76V92a12,12,0,0,0,12,12h48Z"/>',
|
||||
|
||||
// UI Elements
|
||||
'dots-three': '<path d="M144,128a16,16,0,1,1-16-16A16,16,0,0,1,144,128Zm56-16a16,16,0,1,0,16,16A16,16,0,0,0,200,112ZM56,112a16,16,0,1,0,16,16A16,16,0,0,0,56,112Z"/>',
|
||||
'dots-three-vertical': '<path d="M128,112a16,16,0,1,0,16,16A16,16,0,0,0,128,112Zm0-56a16,16,0,1,0-16-16A16,16,0,0,0,128,56Zm0,144a16,16,0,1,0,16,16A16,16,0,0,0,128,200Z"/>',
|
||||
'list': '<path d="M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128ZM40,76H216a12,12,0,0,0,0-24H40a12,12,0,0,0,0,24ZM216,180H40a12,12,0,0,0,0,24H216a12,12,0,0,0,0-24Z"/>',
|
||||
'magnifying-glass': '<path d="M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"/>',
|
||||
|
||||
// Misc
|
||||
'key': '<path d="M196,76a16,16,0,1,1-16-16A16,16,0,0,1,196,76Zm48,22.74A84.3,84.3,0,0,1,160.11,180H160a83.52,83.52,0,0,1-23.65-3.38l-7.86,7.87A12,12,0,0,1,120,188H108v12a12,12,0,0,1-12,12H84v12a12,12,0,0,1-12,12H40a20,20,0,0,1-20-20V187.31a19.86,19.86,0,0,1,5.86-14.14l53.52-53.52A84,84,0,1,1,244,98.74ZM202.43,53.57A59.48,59.48,0,0,0,158,36c-32,1-58,27.89-58,59.89a59.69,59.69,0,0,0,4.2,22.19,12,12,0,0,1-2.55,13.21L44,189v23H60V200a12,12,0,0,1,12-12H84V176a12,12,0,0,1,12-12h19l9.65-9.65a12,12,0,0,1,13.22-2.55A59.58,59.58,0,0,0,160,156h.08c32,0,58.87-26.07,59.89-58A59.55,59.55,0,0,0,202.43,53.57Z"/>',
|
||||
'info': '<path d="M108,84a16,16,0,1,1,16,16A16,16,0,0,1,108,84Zm128,44A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Zm-72,36.68V132a20,20,0,0,0-20-20,12,12,0,0,0-4,23.32V168a20,20,0,0,0,20,20,12,12,0,0,0,4-23.32Z"/>',
|
||||
'tag': '<path d="M246.66,123.56,201,55.13A20,20,0,0,0,184.06,44H39.38A20.07,20.07,0,0,0,20,63.38V192.62A20.07,20.07,0,0,0,39.38,212H184.06a20,20,0,0,0,16.94-11.13l45.66-68.43A12,12,0,0,0,246.66,123.56ZM180.91,188H44V68H180.91l40,60ZM96,84a16,16,0,1,1-16,16A16,16,0,0,1,96,84Z"/>',
|
||||
'share': '<path d="M236,200a12,12,0,0,1-12,12H32a12,12,0,0,1,0-24H224A12,12,0,0,1,236,200ZM88.49,80.49,116,53v83a12,12,0,0,0,24,0V53l27.51,27.52a12,12,0,1,0,17-17l-48-48a12,12,0,0,0-17,0l-48,48a12,12,0,1,0,17,17Z"/>',
|
||||
'download': '<path d="M228,144v64a20,20,0,0,1-20,20H48a20,20,0,0,1-20-20V144a12,12,0,0,1,24,0v60H204V144a12,12,0,0,1,24,0Zm-108.49,8.49a12,12,0,0,0,17,0l40-40a12,12,0,0,0-17-17L140,115V24a12,12,0,0,0-24,0v91L96.49,95.51a12,12,0,0,0-17,17Z"/>',
|
||||
'upload': '<path d="M228,144v64a20,20,0,0,1-20,20H48a20,20,0,0,1-20-20V144a12,12,0,0,1,24,0v60H204V144a12,12,0,0,1,24,0Zm-108.49-4.49a12,12,0,0,0,17,0l40-40a12,12,0,0,0-17-17L140,101V24a12,12,0,0,0-24,0v77L96.49,81.51a12,12,0,1,0-17,17Z"/>',
|
||||
'link': '<path d="M122.34,109.66a12,12,0,0,1,0-17l32-32a52,52,0,0,1,73.56,73.56L196,166.14a52,52,0,0,1-73.56,0,12,12,0,1,1,17-17,28,28,0,0,0,39.6,0l31.89-31.88a28,28,0,0,0-39.6-39.6l-32,32A12,12,0,0,1,122.34,109.66Zm-20.68,36.68-32,32a28,28,0,0,0,39.6,39.6l31.89-31.88a28,28,0,0,0,0-39.6,12,12,0,0,0-17,17,4,4,0,0,1,0,5.66L92.22,200.94a4,4,0,0,1-5.66,0,4,4,0,0,1,0-5.66l32-32a12,12,0,0,0-17-17Z"/>',
|
||||
'eye': '<path d="M251.89,122.47a140.13,140.13,0,0,0-46.65-50.37C185,60,157.49,52,128,52S71,60,50.76,72.1a140.13,140.13,0,0,0-46.65,50.37,12.44,12.44,0,0,0,0,11.06,140.13,140.13,0,0,0,46.65,50.37C71,196,98.51,204,128,204s57-8,77.24-20.1a140.13,140.13,0,0,0,46.65-50.37A12.44,12.44,0,0,0,251.89,122.47ZM128,180c-42.86,0-78.64-23.44-105.73-63C49.36,77.44,85.14,54,128,54s78.64,23.44,105.73,63C206.64,156.56,170.86,180,128,180Zm0-98a34,34,0,1,0,34,34A34,34,0,0,0,128,82Zm0,44a10,10,0,1,1,10-10A10,10,0,0,1,128,126Z"/>',
|
||||
'eye-slash': '<path d="M55.58,37.76a12,12,0,0,0-17,17l20.7,22.77C29.09,102.4,11.72,137.48,4.09,151.56a12.44,12.44,0,0,0,0,11.06A140.13,140.13,0,0,0,50.74,213c20.26,12.12,47.75,20.1,77.26,20.1a127.36,127.36,0,0,0,51.71-10.52l22.71,25a12,12,0,0,0,17-17Zm49.77,120A34,34,0,0,1,94,128a33.55,33.55,0,0,1,1-8.27l45.64,50.21A33.56,33.56,0,0,1,128,172,34,34,0,0,1,105.35,157.76Zm116.81,53.3a12,12,0,0,1-6.16,16,128,128,0,0,1-51.62,9.78,34,34,0,0,1-45.31-50.35L98.11,163.44a58.05,58.05,0,0,0,79.46,19.74,34,34,0,0,0,12.33-12A127.85,127.85,0,0,0,206.16,211.06ZM237.94,133.56a140.13,140.13,0,0,1-46.65,50.37A130.12,130.12,0,0,1,128,203.93a57.6,57.6,0,0,1-15.47-1.9l-19.24-21.16a82.06,82.06,0,0,1,0-117.74A82,82,0,0,1,128,50h.09a133.85,133.85,0,0,1,63,19.93,140.13,140.13,0,0,1,46.65,50.37A12.44,12.44,0,0,1,237.94,133.56ZM215.82,128C188.83,87.44,153,64,128,64a58.1,58.1,0,0,0-40.66,16.55L128,123.3a10,10,0,0,1,0,14.13,10.19,10.19,0,0,1-14.13,0l-40.66,44.72A82,82,0,0,0,128,204c25,0,60.83-23.44,87.82-64C215.82,128,215.82,128,215.82,128Z"/>',
|
||||
'lock': '<path d="M208,76H180V56a52,52,0,0,0-104,0V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76ZM100,56a28,28,0,0,1,56,0V76H100ZM204,204H52V100H204Zm-72-76a16,16,0,1,1-16-16A16,16,0,0,1,132,128Z"/>',
|
||||
'star': '<path d="M234.29,114.85l-45,38.83L203,211.75a20.81,20.81,0,0,1-7.24,21.31,20.25,20.25,0,0,1-21.7,1.32L128,213.15,81.94,234.38a20.25,20.25,0,0,1-21.7-1.32,20.81,20.81,0,0,1-7.24-21.31l13.72-58.07-45-38.83A20.86,20.86,0,0,1,33.31,86.56l59.51-5.16,23.17-55.23a20.88,20.88,0,0,1,38,0l23.17,55.23,59.51,5.16a20.86,20.86,0,0,1,11.58,28.29Zm-74.81-28.8-18.28-43.54a.84.84,0,0,0-1.58,0L121.34,86.05a12,12,0,0,1-10.09,7.34L51.58,98.83a.87.87,0,0,0-.49,1.53L89.83,131.8a12,12,0,0,1,3.86,11.93L82.84,201.1a.87.87,0,0,0,.34.94.88.88,0,0,0,1,0l43.45-19.95a12.09,12.09,0,0,1,10.68,0l43.45,19.95a.88.88,0,0,0,1,0,.87.87,0,0,0,.34-.94l-10.85-57.37a12,12,0,0,1,3.86-11.93l38.74-33.44a.87.87,0,0,0-.49-1.53l-59.67-5.17A12,12,0,0,1,159.48,86.05Z"/>',
|
||||
'heart': '<path d="M178,36c-21.4,0-39.31,10.64-50,27.14C117.31,46.64,99.4,36,78,36a66.08,66.08,0,0,0-66,66c0,72.25,105.53,130.44,110.66,132.94a12,12,0,0,0,10.68,0C138.47,232.44,244,174.25,244,102A66.08,66.08,0,0,0,178,36Zm-5.42,142.24c-18.3,10.88-38.26,21.14-44.58,24.78-6.32-3.64-26.28-13.9-44.58-24.78C56.75,159.16,28,136,28,102A42,42,0,0,1,70,60c18.12,0,33.05,9.48,42.64,27.22a12,12,0,0,0,20.72,0C142.94,69.48,157.88,60,176,60a42,42,0,0,1,42,42C220,136,191.25,159.16,172.58,178.24Z"/>',
|
||||
'bell': '<path d="M225.29,165.93C216.61,151,212,129.57,212,104a84,84,0,0,0-168,0c0,25.58-4.59,47-13.27,61.93A20.08,20.08,0,0,0,30.66,186a19.77,19.77,0,0,0,17.79,11H94.2a36,36,0,0,0,67.6,0H207.55a19.77,19.77,0,0,0,17.79-11A20.08,20.08,0,0,0,225.29,165.93ZM128,216a12,12,0,0,1-11.62-9h23.24A12,12,0,0,1,128,216Zm-76.55-39C62.19,157.17,68,133.1,68,104a60,60,0,0,1,120,0c0,29.1,5.81,53.17,16.55,73Z"/>',
|
||||
'calendar': '<path d="M208,28H188V24a12,12,0,0,0-24,0v4H92V24a12,12,0,0,0-24,0v4H48A20,20,0,0,0,28,48V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V48A20,20,0,0,0,208,28ZM68,52a12,12,0,0,0,24,0h72a12,12,0,0,0,24,0h16V76H52V52ZM52,204V100H204V204Z"/>',
|
||||
'clock': '<path d="M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm76-84a12,12,0,0,1-12,12H128a12,12,0,0,1-12-12V72a12,12,0,0,1,24,0v44h52A12,12,0,0,1,204,128Z"/>',
|
||||
'image': '<path d="M216,36H40A20,20,0,0,0,20,56V200a20,20,0,0,0,20,20H216a20,20,0,0,0,20-20V56A20,20,0,0,0,216,36Zm-4,24V158.75l-26.07-26.06a20,20,0,0,0-28.28,0l-23.23,23.23-59.8-59.8a20,20,0,0,0-28.28,0L44,98.75V60ZM44,196V121.66l56-56,59.8,59.8a20,20,0,0,0,28.28,0l23.23-23.23L212,103V196Zm28-96a20,20,0,1,0-20-20A20,20,0,0,0,72,100Z"/>',
|
||||
'shield-check': '<path d="M208,36H48A20,20,0,0,0,28,56v56c0,54.29,26.32,87.22,48.4,105.29,23.71,19.39,47.44,26,48.44,26.29a12.1,12.1,0,0,0,6.32,0c1-.28,24.73-6.9,48.44-26.29,22.08-18.07,48.4-51,48.4-105.29V56A20,20,0,0,0,208,36Zm-4,76c0,35.71-13.09,64.69-38.91,86.15A126.28,126.28,0,0,1,128,219.38a126.14,126.14,0,0,1-37.09-21.23C65.09,176.69,52,147.71,52,112V60H204ZM79.51,144.49a12,12,0,1,1,17-17L112,143l47.51-47.52a12,12,0,0,1,17,17l-56,56a12,12,0,0,1-17,0Z"/>',
|
||||
'envelope': '<path d="M224,44H32A12,12,0,0,0,20,56V192a20,20,0,0,0,20,20H216a20,20,0,0,0,20-20V56A12,12,0,0,0,224,44Zm-96,83.72L62.85,68h130.3ZM92.79,128,44,172.72V83.28Zm17.76,16.28,9.34,8.57a12,12,0,0,0,16.22,0l9.34-8.57L193.15,188H62.85ZM163.21,128,212,83.28v89.44Z"/>',
|
||||
'arrows-left-right': '<path d="M216.49,184.49l-32,32a12,12,0,0,1-17-17L179,188H48a12,12,0,0,1,0-24H179l-11.52-11.51a12,12,0,0,1,17-17l32,32A12,12,0,0,1,216.49,184.49Zm-145-64a12,12,0,0,0,17-17L77,92H208a12,12,0,0,0,0-24H77L88.49,56.49a12,12,0,0,0-17-17l-32,32a12,12,0,0,0,0,17Z"/>',
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof iconPaths;
|
||||
|
|
@ -1,18 +1,10 @@
|
|||
<script lang="ts">
|
||||
import AudioPlayer from '$lib/components/AudioPlayer.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
|
||||
interface Recording {
|
||||
id: string;
|
||||
url: string;
|
||||
duration?: number;
|
||||
created_at: string;
|
||||
label?: string;
|
||||
size?: number;
|
||||
}
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
import type { AdditionalRecording } from '$lib/types/memo.types';
|
||||
|
||||
interface Props {
|
||||
recordings: Recording[];
|
||||
recordings: AdditionalRecording[];
|
||||
onRecordingAdd?: () => void;
|
||||
onRecordingDelete?: (recordingId: string) => void;
|
||||
onRecordingRename?: (recordingId: string, newLabel: string) => void;
|
||||
|
|
@ -25,12 +17,13 @@
|
|||
let editingId = $state<string | null>(null);
|
||||
let editLabel = $state('');
|
||||
|
||||
function formatDuration(seconds?: number): string {
|
||||
if (!seconds) return '--:--';
|
||||
function formatDuration(millis?: number): string {
|
||||
if (!millis) return '--:--';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
const totalSeconds = Math.floor(millis / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const secs = Math.floor(totalSeconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
|
|
@ -60,9 +53,9 @@
|
|||
});
|
||||
}
|
||||
|
||||
function startEditing(recording: Recording) {
|
||||
function startEditing(recording: AdditionalRecording) {
|
||||
editingId = recording.id;
|
||||
editLabel = recording.label || '';
|
||||
editLabel = '';
|
||||
}
|
||||
|
||||
function cancelEditing() {
|
||||
|
|
@ -140,7 +133,7 @@
|
|||
<!-- View Mode -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Text variant="body" weight="semibold">
|
||||
{recording.label || `Recording ${recordings.indexOf(recording) + 1}`}
|
||||
Recording {recordings.indexOf(recording) + 1}
|
||||
</Text>
|
||||
{#if canEdit && onRecordingRename}
|
||||
<button
|
||||
|
|
@ -172,7 +165,7 @@
|
|||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{formatDuration(recording.duration)}
|
||||
{formatDuration(recording.duration_millis)}
|
||||
</Text>
|
||||
<Text variant="muted" class="flex items-center gap-1">
|
||||
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -183,7 +176,7 @@
|
|||
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{formatSize(recording.size)}
|
||||
--
|
||||
</Text>
|
||||
<Text variant="muted" class="flex items-center gap-1">
|
||||
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -223,7 +216,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Audio Player -->
|
||||
<AudioPlayer src={recording.url} />
|
||||
<AudioPlayer src={recording.audio_url} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Memo } from '$lib/types/memo.types';
|
||||
import { formatDuration, getMemooDuration, formatTimestamp } from '$lib/utils/formatters';
|
||||
import TagBadge from '$lib/components/TagBadge.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Photo {
|
||||
id: string;
|
||||
|
|
@ -50,7 +50,8 @@
|
|||
function navigatePhoto(direction: number) {
|
||||
if (!selectedPhoto) return;
|
||||
|
||||
const currentIndex = photos.findIndex((p) => p.id === selectedPhoto.id);
|
||||
const currentPhoto = selectedPhoto;
|
||||
const currentIndex = photos.findIndex((p) => p.id === currentPhoto.id);
|
||||
const newIndex = currentIndex + direction;
|
||||
|
||||
if (newIndex >= 0 && newIndex < photos.length) {
|
||||
|
|
@ -162,6 +163,7 @@
|
|||
|
||||
<!-- Lightbox Modal -->
|
||||
{#if showLightbox && selectedPhoto}
|
||||
{@const currentPhoto = selectedPhoto}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 bg-black/90 backdrop-blur-sm"
|
||||
|
|
@ -173,15 +175,15 @@
|
|||
<div class="relative max-h-full max-w-5xl" onclick={(e) => e.stopPropagation()}>
|
||||
<!-- Image -->
|
||||
<img
|
||||
src={selectedPhoto.url}
|
||||
alt={selectedPhoto.caption || 'Photo'}
|
||||
src={currentPhoto.url}
|
||||
alt={currentPhoto.caption || 'Photo'}
|
||||
class="max-h-[90vh] w-auto rounded-lg shadow-2xl"
|
||||
/>
|
||||
|
||||
<!-- Caption -->
|
||||
{#if selectedPhoto.caption}
|
||||
{#if currentPhoto.caption}
|
||||
<div class="mt-4 rounded-lg bg-menu p-4">
|
||||
<Text variant="body">{selectedPhoto.caption}</Text>
|
||||
<Text variant="body">{currentPhoto.caption}</Text>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -206,7 +208,7 @@
|
|||
<button
|
||||
onclick={() => navigatePhoto(-1)}
|
||||
class="absolute top-1/2 left-4 -translate-y-1/2 rounded-full bg-black/50 p-3 text-white transition-colors hover:bg-black/70 disabled:opacity-50"
|
||||
disabled={photos.findIndex((p) => p.id === selectedPhoto.id) === 0}
|
||||
disabled={photos.findIndex((p) => p.id === currentPhoto.id) === 0}
|
||||
title="Previous (←)"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -222,7 +224,7 @@
|
|||
<button
|
||||
onclick={() => navigatePhoto(1)}
|
||||
class="absolute top-1/2 right-4 -translate-y-1/2 rounded-full bg-black/50 p-3 text-white transition-colors hover:bg-black/70 disabled:opacity-50"
|
||||
disabled={photos.findIndex((p) => p.id === selectedPhoto.id) === photos.length - 1}
|
||||
disabled={photos.findIndex((p) => p.id === currentPhoto.id) === photos.length - 1}
|
||||
title="Next (→)"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -239,7 +241,7 @@
|
|||
<!-- Photo Counter -->
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full bg-black/50 px-4 py-2">
|
||||
<Text variant="small" class="text-white">
|
||||
{photos.findIndex((p) => p.id === selectedPhoto.id) + 1} / {photos.length}
|
||||
{photos.findIndex((p) => p.id === currentPhoto.id) + 1} / {photos.length}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
|
@ -175,6 +175,8 @@
|
|||
|
||||
<style>
|
||||
.kbd {
|
||||
@apply rounded border border-theme bg-menu-hover px-1.5 py-0.5 font-mono;
|
||||
@apply rounded border px-1.5 py-0.5 font-mono text-xs;
|
||||
border-color: var(--color-border);
|
||||
background-color: var(--color-menu-bg-hover);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
|
||||
interface Blueprint {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Modal, Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
|
||||
interface Blueprint {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -205,6 +205,8 @@
|
|||
|
||||
<style>
|
||||
.kbd {
|
||||
@apply rounded border border-theme bg-menu-hover px-1.5 py-0.5 font-mono;
|
||||
@apply rounded border px-1.5 py-0.5 font-mono text-xs;
|
||||
border-color: var(--color-border);
|
||||
background-color: var(--color-menu-bg-hover);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal, Text } from '@manacore/shared-ui';
|
||||
import type { Memo } from '$lib/types/memo.types';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
import type { ShortcutGroup } from '$lib/utils/keyboardShortcuts';
|
||||
import { formatShortcut } from '$lib/utils/keyboardShortcuts';
|
||||
|
||||
|
|
@ -74,6 +74,9 @@
|
|||
|
||||
<style>
|
||||
.kbd {
|
||||
@apply inline-flex items-center gap-1 rounded border border-theme bg-menu-hover px-2 py-1 font-mono text-xs font-semibold text-theme;
|
||||
@apply inline-flex items-center gap-1 rounded border px-2 py-1 font-mono text-xs font-semibold;
|
||||
border-color: var(--color-border);
|
||||
background-color: var(--color-menu-bg-hover);
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
|
||||
interface Space {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
|
||||
interface Speaker {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
import type { Tag } from '$lib/types/memo.types';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
|
||||
interface Language {
|
||||
code: string;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import GlassCard from './GlassCard.svelte';
|
||||
import StatRow from './StatRow.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
mostViewedMemo: { id: string; title: string; viewCount: number } | null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import GlassCard from './GlassCard.svelte';
|
||||
import StatRow from './StatRow.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
averageAudioDuration: number;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import GlassCard from './GlassCard.svelte';
|
||||
import StatRow from './StatRow.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
memoCount: number;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import GlassCard from './GlassCard.svelte';
|
||||
import StatRow from './StatRow.svelte';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
todayStats: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -114,7 +114,8 @@ export class AudioStorageService {
|
|||
|
||||
if (memos) {
|
||||
totalDurationSeconds = memos.reduce((sum, memo) => {
|
||||
const duration = memo.source?.duration_seconds || memo.source?.duration || 0;
|
||||
const source = memo.source as { duration_seconds?: number; duration?: number } | null;
|
||||
const duration = source?.duration_seconds || source?.duration || 0;
|
||||
return sum + duration;
|
||||
}, 0);
|
||||
}
|
||||
|
|
@ -167,14 +168,22 @@ export class AudioStorageService {
|
|||
try {
|
||||
const supabase = await createAuthClient();
|
||||
|
||||
// Try to find memo by audio_url
|
||||
const { data: memo, error } = await supabase
|
||||
// Try to find memo by source containing the audio file name
|
||||
const { data: memos, error } = await supabase
|
||||
.from('memos')
|
||||
.select('id, title, audio_url')
|
||||
.ilike('audio_url', `%${audioFileName}%`)
|
||||
.single();
|
||||
.select('id, title, source')
|
||||
.not('source', 'is', null);
|
||||
|
||||
if (error || !memo) return null;
|
||||
if (error || !memos) return null;
|
||||
|
||||
// Find memo where source contains the audio file name
|
||||
const memo = memos.find((m) => {
|
||||
const source = m.source as { audioUrl?: string; audio_url?: string } | null;
|
||||
const audioUrl = source?.audioUrl || source?.audio_url || '';
|
||||
return audioUrl.includes(audioFileName);
|
||||
});
|
||||
|
||||
if (!memo) return null;
|
||||
|
||||
return {
|
||||
id: memo.id,
|
||||
|
|
|
|||
|
|
@ -96,11 +96,10 @@ export async function uploadAndProcessAudio({
|
|||
fileName,
|
||||
duration: Math.floor(duration),
|
||||
memoId,
|
||||
spaceId,
|
||||
spaceId: spaceId ?? undefined,
|
||||
title,
|
||||
blueprintId,
|
||||
blueprintId: blueprintId ?? undefined,
|
||||
recordingLanguages,
|
||||
enableDiarization,
|
||||
appToken
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -347,9 +347,9 @@ export class MemoService {
|
|||
.eq('id', memoId)
|
||||
.single();
|
||||
|
||||
const currentMetadata = memo?.metadata || {};
|
||||
const currentStats = currentMetadata.stats || {};
|
||||
const newViewCount = (currentStats.viewCount || 0) + 1;
|
||||
const currentMetadata = (memo?.metadata as Record<string, unknown>) || {};
|
||||
const currentStats = (currentMetadata.stats as Record<string, unknown>) || {};
|
||||
const newViewCount = ((currentStats.viewCount as number) || 0) + 1;
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('memos')
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { env } from '$lib/config/env';
|
||||
import { tokenManager } from './tokenManager';
|
||||
import { createAuthClient } from '$lib/supabaseClient';
|
||||
import type { Memory } from '$lib/types/memo.types';
|
||||
|
||||
export interface QuestionResult {
|
||||
success: boolean;
|
||||
|
|
@ -14,12 +15,7 @@ export interface QuestionResult {
|
|||
creditsConsumed?: number;
|
||||
}
|
||||
|
||||
export interface Memory {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
export type { Memory };
|
||||
|
||||
class QuestionService {
|
||||
/**
|
||||
|
|
@ -131,7 +127,7 @@ class QuestionService {
|
|||
|
||||
const { data, error } = await supabase
|
||||
.from('memories')
|
||||
.select('id, title, content, metadata')
|
||||
.select('id, memo_id, title, content, metadata, style, media, created_at, updated_at')
|
||||
.eq('memo_id', memoId)
|
||||
.order('sort_order', { ascending: true })
|
||||
.order('created_at', { ascending: false });
|
||||
|
|
@ -141,7 +137,18 @@ class QuestionService {
|
|||
return [];
|
||||
}
|
||||
|
||||
return data || [];
|
||||
// Transform data to match Memory interface
|
||||
return (data || []).map(item => ({
|
||||
id: item.id,
|
||||
memo_id: item.memo_id,
|
||||
title: item.title,
|
||||
content: item.content,
|
||||
metadata: item.metadata as Record<string, any> | null | undefined,
|
||||
style: item.style as Record<string, any> | null | undefined,
|
||||
media: item.media as Record<string, any> | null | undefined,
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error loading memories:', error);
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function createRecordingStore() {
|
|||
audioBlob: blob,
|
||||
audioUrl: URL.createObjectURL(blob)
|
||||
})),
|
||||
setError: (error: string) => update((state) => ({ ...state, error })),
|
||||
setError: (error: string | null) => update((state) => ({ ...state, error })),
|
||||
reset: () =>
|
||||
set({
|
||||
status: 'idle',
|
||||
|
|
|
|||
|
|
@ -1,28 +1,611 @@
|
|||
/**
|
||||
* Database types for Supabase
|
||||
* This is a placeholder file - generate actual types using:
|
||||
* npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/lib/types/database.types.ts
|
||||
*/
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
[key: string]: {
|
||||
Row: Record<string, unknown>;
|
||||
Insert: Record<string, unknown>;
|
||||
Update: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
Views: {
|
||||
[key: string]: {
|
||||
Row: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
Functions: {
|
||||
[key: string]: {
|
||||
Args: Record<string, unknown>;
|
||||
Returns: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
public: {
|
||||
Tables: {
|
||||
blueprints: {
|
||||
Row: {
|
||||
advice: Json | null
|
||||
category: Json | null
|
||||
created_at: string
|
||||
description: Json | null
|
||||
id: string
|
||||
is_public: boolean
|
||||
name: Json
|
||||
style: Json | null
|
||||
topic_id: string | null
|
||||
updated_at: string
|
||||
user_id: string | null
|
||||
}
|
||||
Insert: {
|
||||
advice?: Json | null
|
||||
category?: Json | null
|
||||
created_at?: string
|
||||
description?: Json | null
|
||||
id?: string
|
||||
is_public?: boolean
|
||||
name?: Json
|
||||
style?: Json | null
|
||||
topic_id?: string | null
|
||||
updated_at?: string
|
||||
user_id?: string | null
|
||||
}
|
||||
Update: {
|
||||
advice?: Json | null
|
||||
category?: Json | null
|
||||
created_at?: string
|
||||
description?: Json | null
|
||||
id?: string
|
||||
is_public?: boolean
|
||||
name?: Json
|
||||
style?: Json | null
|
||||
topic_id?: string | null
|
||||
updated_at?: string
|
||||
user_id?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
debug_logs: {
|
||||
Row: {
|
||||
created_at: string
|
||||
data: Json
|
||||
id: string
|
||||
type: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
data: Json
|
||||
id?: string
|
||||
type: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
data?: Json
|
||||
id?: string
|
||||
type?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
memo_spaces: {
|
||||
Row: {
|
||||
created_at: string
|
||||
memo_id: string
|
||||
space_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
memo_id: string
|
||||
space_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
memo_id?: string
|
||||
space_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "memo_spaces_memo_id_fkey"
|
||||
columns: ["memo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "memos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
memo_tags: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
memo_id: string
|
||||
tag_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
memo_id: string
|
||||
tag_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
memo_id?: string
|
||||
tag_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "memo_tags_memo_id_fkey"
|
||||
columns: ["memo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "memos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "memo_tags_tag_id_fkey"
|
||||
columns: ["tag_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "tags"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
memories: {
|
||||
Row: {
|
||||
content: string | null
|
||||
created_at: string
|
||||
id: string
|
||||
media: Json | null
|
||||
memo_id: string
|
||||
metadata: Json | null
|
||||
style: Json | null
|
||||
title: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
content?: string | null
|
||||
created_at?: string
|
||||
id?: string
|
||||
media?: Json | null
|
||||
memo_id: string
|
||||
metadata?: Json | null
|
||||
style?: Json | null
|
||||
title?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
content?: string | null
|
||||
created_at?: string
|
||||
id?: string
|
||||
media?: Json | null
|
||||
memo_id?: string
|
||||
metadata?: Json | null
|
||||
style?: Json | null
|
||||
title?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "memories_memo_id_fkey"
|
||||
columns: ["memo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "memos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
memos: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
intro: string | null
|
||||
is_archived: boolean
|
||||
is_pinned: boolean
|
||||
is_public: boolean
|
||||
location: unknown | null
|
||||
metadata: Json | null
|
||||
shared_with_users: string[] | null
|
||||
source: Json
|
||||
style: Json | null
|
||||
title: string | null
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
intro?: string | null
|
||||
is_archived?: boolean
|
||||
is_pinned?: boolean
|
||||
is_public?: boolean
|
||||
location?: unknown | null
|
||||
metadata?: Json | null
|
||||
shared_with_users?: string[] | null
|
||||
source?: Json
|
||||
style?: Json | null
|
||||
title?: string | null
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
intro?: string | null
|
||||
is_archived?: boolean
|
||||
is_pinned?: boolean
|
||||
is_public?: boolean
|
||||
location?: unknown | null
|
||||
metadata?: Json | null
|
||||
shared_with_users?: string[] | null
|
||||
source?: Json
|
||||
style?: Json | null
|
||||
title?: string | null
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
prompt_blueprints: {
|
||||
Row: {
|
||||
blueprint_id: string
|
||||
created_at: string
|
||||
prompt_id: string
|
||||
}
|
||||
Insert: {
|
||||
blueprint_id: string
|
||||
created_at?: string
|
||||
prompt_id: string
|
||||
}
|
||||
Update: {
|
||||
blueprint_id?: string
|
||||
created_at?: string
|
||||
prompt_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "blueprint_prompts_blueprint_id_fkey"
|
||||
columns: ["blueprint_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "blueprints"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "blueprint_prompts_prompt_id_fkey"
|
||||
columns: ["prompt_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "prompts"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
prompts: {
|
||||
Row: {
|
||||
created_at: string
|
||||
description: Json | null
|
||||
id: string
|
||||
is_public: boolean
|
||||
memory_title: Json
|
||||
prompt_text: Json
|
||||
updated_at: string
|
||||
user_id: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
description?: Json | null
|
||||
id?: string
|
||||
is_public?: boolean
|
||||
memory_title?: Json
|
||||
prompt_text?: Json
|
||||
updated_at?: string
|
||||
user_id?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
description?: Json | null
|
||||
id?: string
|
||||
is_public?: boolean
|
||||
memory_title?: Json
|
||||
prompt_text?: Json
|
||||
updated_at?: string
|
||||
user_id?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
space_members: {
|
||||
Row: {
|
||||
added_at: string | null
|
||||
added_by: string | null
|
||||
id: string
|
||||
role: string
|
||||
space_id: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
added_at?: string | null
|
||||
added_by?: string | null
|
||||
id?: string
|
||||
role: string
|
||||
space_id: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
added_at?: string | null
|
||||
added_by?: string | null
|
||||
id?: string
|
||||
role?: string
|
||||
space_id?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
spatial_ref_sys: {
|
||||
Row: {
|
||||
auth_name: string | null
|
||||
auth_srid: number | null
|
||||
proj4text: string | null
|
||||
srid: number
|
||||
srtext: string | null
|
||||
}
|
||||
Insert: {
|
||||
auth_name?: string | null
|
||||
auth_srid?: number | null
|
||||
proj4text?: string | null
|
||||
srid: number
|
||||
srtext?: string | null
|
||||
}
|
||||
Update: {
|
||||
auth_name?: string | null
|
||||
auth_srid?: number | null
|
||||
proj4text?: string | null
|
||||
srid?: number
|
||||
srtext?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
tags: {
|
||||
Row: {
|
||||
created_at: string
|
||||
description: Json | null
|
||||
id: string
|
||||
is_pinned: boolean | null
|
||||
name: string
|
||||
sort_order: number | null
|
||||
style: Json | null
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
description?: Json | null
|
||||
id?: string
|
||||
is_pinned?: boolean | null
|
||||
name?: string
|
||||
sort_order?: number | null
|
||||
style?: Json | null
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
description?: Json | null
|
||||
id?: string
|
||||
is_pinned?: boolean | null
|
||||
name?: string
|
||||
sort_order?: number | null
|
||||
style?: Json | null
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
user_active_blueprints: {
|
||||
Row: {
|
||||
blueprint_id: string
|
||||
created_at: string
|
||||
id: string
|
||||
is_active: boolean
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
blueprint_id: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
is_active?: boolean
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
blueprint_id?: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
is_active?: boolean
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "user_active_blueprints_blueprint_id_fkey"
|
||||
columns: ["blueprint_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "blueprints"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
geography_columns: {
|
||||
Row: {
|
||||
coord_dimension: number | null
|
||||
f_geography_column: unknown | null
|
||||
f_table_catalog: unknown | null
|
||||
f_table_name: unknown | null
|
||||
f_table_schema: unknown | null
|
||||
srid: number | null
|
||||
type: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
geometry_columns: {
|
||||
Row: {
|
||||
coord_dimension: number | null
|
||||
f_geometry_column: unknown | null
|
||||
f_table_catalog: string | null
|
||||
f_table_name: unknown | null
|
||||
f_table_schema: unknown | null
|
||||
srid: number | null
|
||||
type: string | null
|
||||
}
|
||||
Insert: {
|
||||
coord_dimension?: number | null
|
||||
f_geometry_column?: unknown | null
|
||||
f_table_catalog?: string | null
|
||||
f_table_name?: unknown | null
|
||||
f_table_schema?: unknown | null
|
||||
srid?: number | null
|
||||
type?: string | null
|
||||
}
|
||||
Update: {
|
||||
coord_dimension?: number | null
|
||||
f_geometry_column?: unknown | null
|
||||
f_table_catalog?: string | null
|
||||
f_table_name?: unknown | null
|
||||
f_table_schema?: unknown | null
|
||||
srid?: number | null
|
||||
type?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
Functions: {
|
||||
[key: string]: any
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
geometry_dump: {
|
||||
path: number[] | null
|
||||
geom: unknown | null
|
||||
}
|
||||
http_header: {
|
||||
field: string | null
|
||||
value: string | null
|
||||
}
|
||||
http_request: {
|
||||
method: unknown | null
|
||||
uri: string | null
|
||||
headers: Database["public"]["CompositeTypes"]["http_header"][] | null
|
||||
content_type: string | null
|
||||
content: string | null
|
||||
}
|
||||
http_response: {
|
||||
status: number | null
|
||||
content_type: string | null
|
||||
headers: Database["public"]["CompositeTypes"]["http_header"][] | null
|
||||
content: string | null
|
||||
}
|
||||
valid_detail: {
|
||||
valid: boolean | null
|
||||
reason: string | null
|
||||
location: unknown | null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])
|
||||
? (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never
|
||||
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
|
||||
export const Constants = {
|
||||
public: {
|
||||
Enums: {},
|
||||
},
|
||||
} as const
|
||||
|
|
@ -1,3 +1,18 @@
|
|||
export interface MemoPhoto {
|
||||
id: string;
|
||||
url: string;
|
||||
thumbnail_url?: string;
|
||||
caption?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AdditionalRecording {
|
||||
id: string;
|
||||
audio_url: string;
|
||||
duration_millis: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Memo {
|
||||
id: string;
|
||||
user_id: string;
|
||||
|
|
@ -8,6 +23,7 @@ export interface Memo {
|
|||
duration_millis: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
recorded_at?: string;
|
||||
space_id: string | null;
|
||||
blueprint_id: string | null;
|
||||
language: string | null;
|
||||
|
|
@ -35,6 +51,8 @@ export interface Memo {
|
|||
memories?: Memory[];
|
||||
tags?: Tag[];
|
||||
space?: Space;
|
||||
photos?: MemoPhoto[];
|
||||
additional_recordings?: AdditionalRecording[];
|
||||
}
|
||||
|
||||
export type ProcessingStatus = 'pending' | 'processing' | 'completed' | 'failed';
|
||||
|
|
@ -64,6 +82,7 @@ export interface Tag {
|
|||
created_at: string;
|
||||
is_pinned?: boolean;
|
||||
sort_order?: number;
|
||||
usage?: number;
|
||||
}
|
||||
|
||||
export interface MemoTag {
|
||||
|
|
@ -80,12 +99,25 @@ export interface Space {
|
|||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Prompt {
|
||||
id: string;
|
||||
memory_title: string;
|
||||
prompt_text: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Blueprint {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
prompt: string;
|
||||
prompt?: string;
|
||||
user_id: string | null;
|
||||
is_public: boolean;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
category?: string | null;
|
||||
prompts?: Prompt[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,233 +0,0 @@
|
|||
/**
|
||||
* Apple Sign-In integration for web
|
||||
* Uses redirect flow (not popup) - different from mobile native flow
|
||||
*/
|
||||
|
||||
import { env } from '$lib/config/env';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// TypeScript definitions for Apple ID SDK
|
||||
declare global {
|
||||
interface Window {
|
||||
AppleID?: {
|
||||
auth: {
|
||||
init: (config: AppleIDInitConfig) => void;
|
||||
signIn: () => Promise<AppleIDSignInResponse>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface AppleIDInitConfig {
|
||||
clientId: string;
|
||||
scope: string;
|
||||
redirectURI: string;
|
||||
state?: string;
|
||||
nonce?: string;
|
||||
usePopup?: boolean;
|
||||
responseType?: string;
|
||||
responseMode?: string;
|
||||
}
|
||||
|
||||
interface AppleIDSignInResponse {
|
||||
authorization: {
|
||||
code: string;
|
||||
id_token?: string;
|
||||
state?: string;
|
||||
};
|
||||
user?: {
|
||||
email?: string;
|
||||
name?: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface AppleAuthorizationResponse {
|
||||
code: string;
|
||||
id_token?: string;
|
||||
state?: string;
|
||||
user?: string; // JSON string of user info (only on first sign-in)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Apple ID SDK
|
||||
* Must be called before using Apple Sign-In
|
||||
*/
|
||||
export function initializeAppleAuth() {
|
||||
if (!browser || !window.AppleID) {
|
||||
console.warn('Apple ID SDK not loaded');
|
||||
return false;
|
||||
}
|
||||
|
||||
const clientId = env.oauth.appleClientId;
|
||||
// Use the handler endpoint for POST, not the page route
|
||||
const redirectURI = env.oauth.appleRedirectUri?.replace('/auth/apple-callback', '/auth/apple-callback-handler') || 'https://app.memoro.ai/auth/apple-callback-handler';
|
||||
|
||||
// Log configuration for debugging (especially useful in production)
|
||||
console.log('Apple Sign-In Configuration:', {
|
||||
clientId: clientId || '❌ NOT SET',
|
||||
redirectURI: redirectURI,
|
||||
originalRedirectURI: env.oauth.appleRedirectUri || '❌ NOT SET',
|
||||
responseMode: 'form_post',
|
||||
responseType: 'code id_token'
|
||||
});
|
||||
|
||||
if (!clientId) {
|
||||
console.error('❌ Apple Client ID not configured');
|
||||
console.error('Expected: PUBLIC_APPLE_CLIENT_ID=com.memoro.web');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
window.AppleID.auth.init({
|
||||
clientId,
|
||||
scope: 'name email',
|
||||
redirectURI,
|
||||
state: generateState(),
|
||||
usePopup: false, // Must use redirect on web
|
||||
responseType: 'code id_token', // Request both code and id_token
|
||||
responseMode: 'form_post' // Use form_post for secure POST to server - required for email/name scopes
|
||||
});
|
||||
|
||||
console.log('✅ Apple ID SDK initialized successfully with form_post response mode');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error initializing Apple ID SDK:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate Apple Sign-In (redirect flow)
|
||||
* Stores state and redirects to Apple
|
||||
*/
|
||||
export async function signInWithApple(): Promise<void> {
|
||||
if (!browser) {
|
||||
throw new Error('Apple Sign-In only available in browser');
|
||||
}
|
||||
|
||||
if (!window.AppleID) {
|
||||
throw new Error('Apple ID SDK not loaded');
|
||||
}
|
||||
|
||||
try {
|
||||
// Store return URL before redirect
|
||||
const returnTo = window.location.pathname + window.location.search;
|
||||
sessionStorage.setItem('apple_signin_return_to', returnTo);
|
||||
|
||||
// Initiate sign-in (will redirect to Apple)
|
||||
await window.AppleID.auth.signIn();
|
||||
} catch (error) {
|
||||
console.error('Error initiating Apple Sign-In:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Apple authorization response from URL
|
||||
* Called by the callback page after redirect from Apple
|
||||
*/
|
||||
export function parseAppleAuthorizationResponse(
|
||||
urlParams: URLSearchParams
|
||||
): AppleAuthorizationResponse | null {
|
||||
const code = urlParams.get('code');
|
||||
const id_token = urlParams.get('id_token');
|
||||
const state = urlParams.get('state');
|
||||
const user = urlParams.get('user');
|
||||
const error = urlParams.get('error');
|
||||
|
||||
// Check for errors
|
||||
if (error) {
|
||||
console.error('Apple Sign-In error:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate state (CSRF protection)
|
||||
const storedState = sessionStorage.getItem('apple_signin_state');
|
||||
if (state !== storedState) {
|
||||
console.error('State mismatch - possible CSRF attack');
|
||||
return null;
|
||||
}
|
||||
|
||||
// IMPORTANT: We need either id_token OR code
|
||||
// For backend compatibility, we prefer id_token if available
|
||||
if (!id_token && !code) {
|
||||
console.error('No id_token or authorization code in Apple response');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Log what we received
|
||||
console.log('Apple response:', {
|
||||
hasIdToken: !!id_token,
|
||||
hasCode: !!code,
|
||||
hasState: !!state,
|
||||
hasUser: !!user
|
||||
});
|
||||
|
||||
return {
|
||||
code: code || '',
|
||||
id_token: id_token || undefined,
|
||||
state: state || undefined,
|
||||
user: user || undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random state for CSRF protection
|
||||
*/
|
||||
function generateState(): string {
|
||||
const state = Math.random().toString(36).substring(2, 15);
|
||||
if (browser) {
|
||||
sessionStorage.setItem('apple_signin_state', state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored return URL (where to redirect after sign-in)
|
||||
*/
|
||||
export function getStoredReturnUrl(): string {
|
||||
if (!browser) return '/dashboard';
|
||||
return sessionStorage.getItem('apple_signin_return_to') || '/dashboard';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear Apple Sign-In session data
|
||||
*/
|
||||
export function clearAppleSignInSession() {
|
||||
if (!browser) return;
|
||||
sessionStorage.removeItem('apple_signin_state');
|
||||
sessionStorage.removeItem('apple_signin_return_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Apple ID SDK is loaded
|
||||
*/
|
||||
export function isAppleAuthLoaded(): boolean {
|
||||
return browser && !!window.AppleID?.auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Apple ID SDK to load
|
||||
*/
|
||||
export function waitForAppleAuth(timeout = 10000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isAppleAuthLoaded()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
if (isAppleAuthLoaded()) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Apple ID SDK failed to load'));
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
/**
|
||||
* Google Identity Services integration
|
||||
* Provides helper functions for Google Sign-In on web
|
||||
*/
|
||||
|
||||
import { env } from '$lib/config/env';
|
||||
|
||||
// TypeScript definitions for Google Identity Services
|
||||
declare global {
|
||||
interface Window {
|
||||
google?: {
|
||||
accounts: {
|
||||
id: {
|
||||
initialize: (config: GoogleIdConfiguration) => void;
|
||||
prompt: (momentListener?: (notification: PromptMomentNotification) => void) => void;
|
||||
renderButton: (parent: HTMLElement, options: GsiButtonConfiguration) => void;
|
||||
disableAutoSelect: () => void;
|
||||
storeCredential: (credential: { id: string; password: string }) => void;
|
||||
cancel: () => void;
|
||||
onGoogleLibraryLoad: () => void;
|
||||
revoke: (hint: string, callback: (done: RevocationResponse) => void) => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface GoogleIdConfiguration {
|
||||
client_id: string;
|
||||
callback: (response: CredentialResponse) => void;
|
||||
auto_select?: boolean;
|
||||
cancel_on_tap_outside?: boolean;
|
||||
context?: 'signin' | 'signup' | 'use';
|
||||
ux_mode?: 'popup' | 'redirect';
|
||||
login_uri?: string;
|
||||
native_callback?: (response: { id: string; password: string }) => void;
|
||||
itp_support?: boolean;
|
||||
}
|
||||
|
||||
interface CredentialResponse {
|
||||
credential: string; // JWT ID token
|
||||
select_by: string;
|
||||
clientId?: string;
|
||||
}
|
||||
|
||||
interface GsiButtonConfiguration {
|
||||
type?: 'standard' | 'icon';
|
||||
theme?: 'outline' | 'filled_blue' | 'filled_black';
|
||||
size?: 'large' | 'medium' | 'small';
|
||||
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin';
|
||||
shape?: 'rectangular' | 'pill' | 'circle' | 'square';
|
||||
logo_alignment?: 'left' | 'center';
|
||||
width?: string;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
interface PromptMomentNotification {
|
||||
isDisplayMoment: () => boolean;
|
||||
isDisplayed: () => boolean;
|
||||
isNotDisplayed: () => boolean;
|
||||
getNotDisplayedReason: () =>
|
||||
| 'browser_not_supported'
|
||||
| 'invalid_client'
|
||||
| 'missing_client_id'
|
||||
| 'opt_out_or_no_session'
|
||||
| 'secure_http_required'
|
||||
| 'suppressed_by_user'
|
||||
| 'unregistered_origin'
|
||||
| 'unknown_reason';
|
||||
isSkippedMoment: () => boolean;
|
||||
getSkippedReason: () =>
|
||||
| 'auto_cancel'
|
||||
| 'user_cancel'
|
||||
| 'tap_outside'
|
||||
| 'issuing_failed'
|
||||
| 'unknown_reason';
|
||||
isDismissedMoment: () => boolean;
|
||||
getDismissedReason: () => 'credential_returned' | 'cancel_called' | 'flow_restarted' | 'unknown_reason';
|
||||
getMomentType: () => 'display' | 'skipped' | 'dismissed';
|
||||
}
|
||||
|
||||
interface RevocationResponse {
|
||||
successful: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Google Identity Services
|
||||
* @param callback Function to call when user signs in with Google
|
||||
*/
|
||||
export function initializeGoogleAuth(callback: (idToken: string) => void) {
|
||||
if (typeof window === 'undefined') {
|
||||
console.warn('Google Auth: Cannot initialize on server-side');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.google) {
|
||||
console.warn('Google Identity Services not loaded yet');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = env.oauth.googleClientId;
|
||||
if (!clientId) {
|
||||
console.error('Google Client ID not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.google.accounts.id.initialize({
|
||||
client_id: clientId,
|
||||
callback: (response: CredentialResponse) => {
|
||||
// response.credential is the JWT ID token
|
||||
callback(response.credential);
|
||||
},
|
||||
auto_select: false,
|
||||
cancel_on_tap_outside: true,
|
||||
ux_mode: 'popup'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing Google Auth:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Google Sign-In button
|
||||
* @param element HTML element to render button into
|
||||
* @param options Button configuration options
|
||||
*/
|
||||
export function renderGoogleButton(
|
||||
element: HTMLElement,
|
||||
options?: Partial<GsiButtonConfiguration>
|
||||
) {
|
||||
if (typeof window === 'undefined' || !window.google) {
|
||||
console.warn('Google Identity Services not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultOptions: GsiButtonConfiguration = {
|
||||
type: 'standard',
|
||||
theme: 'outline',
|
||||
size: 'large',
|
||||
text: 'signin_with',
|
||||
shape: 'rectangular',
|
||||
logo_alignment: 'left'
|
||||
// Note: width is omitted - Google button will auto-size to container
|
||||
};
|
||||
|
||||
try {
|
||||
window.google.accounts.id.renderButton(element, {
|
||||
...defaultOptions,
|
||||
...options
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error rendering Google button:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Google Identity Services is loaded
|
||||
*/
|
||||
export function isGoogleAuthLoaded(): boolean {
|
||||
return typeof window !== 'undefined' && !!window.google?.accounts?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Google Identity Services to load
|
||||
* @param timeout Maximum time to wait in milliseconds (default: 10000ms)
|
||||
*/
|
||||
export function waitForGoogleAuth(timeout = 10000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isGoogleAuthLoaded()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
if (isGoogleAuthLoaded()) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Google Identity Services failed to load'));
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@
|
|||
// Create a lookup for prompts by ID
|
||||
const promptsById: Record<string, any> = {};
|
||||
if (allPrompts) {
|
||||
for (const prompt of allPrompts) {
|
||||
for (const prompt of allPrompts as Prompt[]) {
|
||||
promptsById[prompt.id] = prompt;
|
||||
}
|
||||
}
|
||||
|
|
@ -151,10 +151,17 @@
|
|||
const blueprintsWithPrompts: Blueprint[] = [];
|
||||
for (const blueprint of blueprintsData || []) {
|
||||
const promptIds = promptLinksByBlueprintId[blueprint.id] || [];
|
||||
const promptsForBlueprint = promptIds.map((id) => promptsById[id]).filter(Boolean);
|
||||
const promptsForBlueprint = promptIds.map((id) => promptsById[id]).filter(Boolean) as Prompt[];
|
||||
|
||||
blueprintsWithPrompts.push({
|
||||
...blueprint,
|
||||
id: blueprint.id,
|
||||
name: blueprint.name as { de?: string; en?: string },
|
||||
description: blueprint.description as { de?: string; en?: string } | undefined,
|
||||
category: blueprint.category as unknown as Category | undefined,
|
||||
is_public: blueprint.is_public,
|
||||
created_at: blueprint.created_at,
|
||||
updated_at: blueprint.updated_at,
|
||||
user_id: blueprint.user_id || '',
|
||||
prompts: promptsForBlueprint
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import { supabase, createAuthClient } from '$lib/supabaseClient';
|
||||
import SplitView from '$lib/components/SplitView.svelte';
|
||||
import ContextMenu from '$lib/components/ContextMenu.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { Modal } from '@manacore/shared-ui';
|
||||
import VirtualList from '$lib/components/VirtualList.svelte';
|
||||
import { DashboardSkeleton } from '$lib/components/skeletons';
|
||||
import type { Memo } from '$lib/types/memo.types';
|
||||
|
|
@ -599,17 +599,18 @@ ${memo.memories && memo.memories.length > 0 ? '\nKI-Analyse:\n' + memo.memories.
|
|||
|
||||
<!-- Context Menu -->
|
||||
{#if contextMenu}
|
||||
{@const menuMemo = contextMenu.memo}
|
||||
<ContextMenu
|
||||
items={[
|
||||
{
|
||||
label: $t('memo.open_in_new_tab'),
|
||||
icon: 'open',
|
||||
action: () => handleOpenInNewTab(contextMenu.memo)
|
||||
action: () => handleOpenInNewTab(menuMemo)
|
||||
},
|
||||
{
|
||||
label: contextMenu.memo.is_pinned ? $t('memo.unpin') : $t('memo.pin'),
|
||||
label: menuMemo.is_pinned ? $t('memo.unpin') : $t('memo.pin'),
|
||||
icon: 'pin',
|
||||
action: () => handlePinMemo(contextMenu.memo)
|
||||
action: () => handlePinMemo(menuMemo)
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
|
|
@ -619,17 +620,17 @@ ${memo.memories && memo.memories.length > 0 ? '\nKI-Analyse:\n' + memo.memories.
|
|||
{
|
||||
label: $t('memo.edit_title'),
|
||||
icon: 'edit',
|
||||
action: () => handleEditTitle(contextMenu.memo)
|
||||
action: () => handleEditTitle(menuMemo)
|
||||
},
|
||||
{
|
||||
label: $t('memo.share'),
|
||||
icon: 'share',
|
||||
action: () => handleShareMemo(contextMenu.memo)
|
||||
action: () => handleShareMemo(menuMemo)
|
||||
},
|
||||
{
|
||||
label: $t('memo.export_text'),
|
||||
icon: 'download',
|
||||
action: () => handleExportMemo(contextMenu.memo)
|
||||
action: () => handleExportMemo(menuMemo)
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
|
|
@ -640,7 +641,7 @@ ${memo.memories && memo.memories.length > 0 ? '\nKI-Analyse:\n' + memo.memories.
|
|||
label: $t('common.delete'),
|
||||
icon: 'delete',
|
||||
danger: true,
|
||||
action: () => handleDeleteMemo(contextMenu.memo)
|
||||
action: () => handleDeleteMemo(menuMemo)
|
||||
}
|
||||
]}
|
||||
x={contextMenu.x}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
|
||||
while (hasMore) {
|
||||
const { data, error } = await supabase
|
||||
.from(tableName)
|
||||
.from(tableName as any)
|
||||
.select(selectFields)
|
||||
.range(from, from + batchSize - 1);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
<script lang="ts">
|
||||
import BillingToggle, { type BillingCycle } from '$lib/components/BillingToggle.svelte';
|
||||
import SubscriptionCard, {
|
||||
type SubscriptionPlan
|
||||
} from '$lib/components/SubscriptionCard.svelte';
|
||||
import PackageCard, { type Package } from '$lib/components/PackageCard.svelte';
|
||||
import UsageCard, { type UsageData } from '$lib/components/UsageCard.svelte';
|
||||
import CostCard, { type CostItem } from '$lib/components/CostCard.svelte';
|
||||
import {
|
||||
BillingToggle,
|
||||
SubscriptionCard,
|
||||
PackageCard,
|
||||
UsageCard,
|
||||
CostCard,
|
||||
type BillingCycle,
|
||||
type SubscriptionPlan,
|
||||
type ManaPackage,
|
||||
type UsageData,
|
||||
type CostItem
|
||||
} from '@manacore/shared-subscription-ui';
|
||||
|
||||
import subscriptionData from '$lib/data/subscriptionData.json';
|
||||
import appCostsData from '$lib/data/appCosts.json';
|
||||
|
|
@ -16,7 +21,7 @@
|
|||
|
||||
// Data from JSON files
|
||||
const subscriptionOptions = subscriptionData.subscriptions as SubscriptionPlan[];
|
||||
const manaPackages = subscriptionData.packages as Package[];
|
||||
const manaPackages = subscriptionData.packages as ManaPackage[];
|
||||
const appCosts = appCostsData.costs as CostItem[];
|
||||
const usage = usageData.usage as UsageData;
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
});
|
||||
|
||||
// Optimistic UI update
|
||||
tags.updateTag(updatedTag);
|
||||
tags.updateTag(tagId, updatedTag);
|
||||
} catch (error) {
|
||||
console.error('Failed to update tag:', error);
|
||||
actionError = error instanceof Error ? error.message : 'Fehler beim Speichern des Tags';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { uploadAndProcessAudio } from '$lib/services/audioUploadService';
|
||||
import { createAuthClient } from '$lib/supabaseClient';
|
||||
import { UploadPageSkeleton } from '$lib/components/skeletons';
|
||||
import Text from '$lib/components/atoms/Text.svelte';
|
||||
import { Text } from '@manacore/shared-ui';
|
||||
|
||||
// Upload state
|
||||
let uploading = $state(false);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,13 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
|
||||
let { children, data } = $props();
|
||||
interface LayoutData {
|
||||
session?: Session | null;
|
||||
}
|
||||
|
||||
let { children, data }: { children: any; data: LayoutData } = $props();
|
||||
|
||||
// Redirect to dashboard if already authenticated
|
||||
onMount(() => {
|
||||
|
|
|
|||
|
|
@ -1,549 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import GoogleSignInButton from '$lib/components/GoogleSignInButton.svelte';
|
||||
import AppleSignInButton from '$lib/components/AppleSignInButton.svelte';
|
||||
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||
import ThemeSelector from '$lib/components/ThemeSelector.svelte';
|
||||
import { LoginPage, setGoogleClientId, setAppleConfig } from '@manacore/shared-auth-ui';
|
||||
import MemoroLogo from '$lib/components/MemoroLogo.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { authService } from '$lib/services/authService';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { t } from 'svelte-i18n';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { env } from '$lib/config/env';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
type AuthMode = 'initial' | 'login' | 'forgot-password' | 'password-reset-success';
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let mode = $state<AuthMode>('initial');
|
||||
let showManaInfoModal = $state(false);
|
||||
let resetEmail = $state(''); // Store email for success message
|
||||
let showPassword = $state(false); // Toggle password visibility
|
||||
|
||||
let currentTheme = $derived($theme);
|
||||
let isDark = $derived(currentTheme.effectiveMode === 'dark');
|
||||
|
||||
// Password validation requirements
|
||||
let passwordRequirements = $derived.by(() => {
|
||||
if (!password) {
|
||||
return {
|
||||
length: false,
|
||||
lowercase: false,
|
||||
uppercase: false,
|
||||
digit: false,
|
||||
special: false
|
||||
};
|
||||
// Configure OAuth on mount
|
||||
onMount(() => {
|
||||
if (env.oauth.googleClientId) {
|
||||
setGoogleClientId(env.oauth.googleClientId);
|
||||
}
|
||||
if (env.oauth.appleClientId && env.oauth.appleRedirectUri) {
|
||||
setAppleConfig(env.oauth.appleClientId, env.oauth.appleRedirectUri);
|
||||
}
|
||||
|
||||
return {
|
||||
length: password.length >= 8,
|
||||
lowercase: /[a-z]/.test(password),
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
digit: /[0-9]/.test(password),
|
||||
special: /[^a-zA-Z0-9]/.test(password)
|
||||
};
|
||||
});
|
||||
|
||||
// Display OAuth error from URL if present
|
||||
let oauthErrorRaw = $derived($page.url.searchParams.get('error'));
|
||||
let oauthError = $derived.by(() => {
|
||||
if (!oauthErrorRaw) return null;
|
||||
|
||||
// Map OAuth error codes to translation keys
|
||||
const errorMap: Record<string, string> = {
|
||||
'access_denied': 'auth.oauth_error_access_denied',
|
||||
'server_error': 'auth.oauth_error_server_error',
|
||||
'temporarily_unavailable': 'auth.oauth_error_temporarily_unavailable',
|
||||
'invalid_request': 'auth.oauth_error_invalid_request',
|
||||
'unauthorized_client': 'auth.oauth_error_unauthorized_client',
|
||||
'unsupported_response_type': 'auth.oauth_error_unsupported_response_type',
|
||||
'invalid_scope': 'auth.oauth_error_invalid_scope'
|
||||
};
|
||||
|
||||
const translationKey = errorMap[oauthErrorRaw] || 'auth.oauth_error_unknown';
|
||||
return $t(translationKey);
|
||||
});
|
||||
|
||||
// Get primary color based on theme variant
|
||||
function getPrimaryColor() {
|
||||
const variant = currentTheme.variant;
|
||||
if (isDark) {
|
||||
const colors = {
|
||||
lume: '#f8d62b',
|
||||
nature: '#4CAF50',
|
||||
stone: '#78909C',
|
||||
ocean: '#039BE5'
|
||||
};
|
||||
return colors[variant];
|
||||
} else {
|
||||
const colors = {
|
||||
lume: '#f8d62b',
|
||||
nature: '#4CAF50',
|
||||
stone: '#607D8B',
|
||||
ocean: '#039BE5'
|
||||
};
|
||||
return colors[variant];
|
||||
}
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
return auth.signIn(email, password);
|
||||
}
|
||||
|
||||
// Get page background based on theme variant
|
||||
function getPageBackground() {
|
||||
const variant = currentTheme.variant;
|
||||
if (isDark) {
|
||||
const colors = {
|
||||
lume: '#101010',
|
||||
nature: '#121212',
|
||||
stone: '#121212',
|
||||
ocean: '#121212'
|
||||
};
|
||||
return colors[variant];
|
||||
} else {
|
||||
const colors = {
|
||||
lume: '#dddddd',
|
||||
nature: '#FBFDF8',
|
||||
stone: '#F5F7F9',
|
||||
ocean: '#F5FCFF'
|
||||
};
|
||||
return colors[variant];
|
||||
}
|
||||
async function handleSignInWithGoogle(idToken: string) {
|
||||
return auth.signInWithGoogle(idToken);
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
// Validate inputs
|
||||
if (!email) {
|
||||
error = $t('auth.error_email_required');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
error = $t('auth.error_password_required');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await auth.signIn(email, password);
|
||||
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
goto('/dashboard');
|
||||
} else {
|
||||
error = result.error || $t('auth.logging_in');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleForgotPassword() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
// Validate email
|
||||
if (!email) {
|
||||
error = $t('auth.error_email_required');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Send password reset email
|
||||
const result = await authService.forgotPassword(email);
|
||||
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
// Store email for success message and switch to success screen
|
||||
resetEmail = email;
|
||||
resetForm();
|
||||
switchMode('password-reset-success');
|
||||
} else {
|
||||
// Show error with specific handling for rate limiting
|
||||
let errorMessage = result.error || $t('auth.reset_password_error');
|
||||
|
||||
// Check if it's a rate limit error
|
||||
if (result.error?.includes('rate limit') || result.error?.includes('too many')) {
|
||||
errorMessage = $t('auth.reset_password_rate_limit');
|
||||
}
|
||||
|
||||
error = errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
email = '';
|
||||
password = '';
|
||||
error = null;
|
||||
}
|
||||
|
||||
function switchMode(newMode: AuthMode) {
|
||||
mode = newMode;
|
||||
error = null;
|
||||
async function handleForgotPassword(email: string) {
|
||||
return auth.forgotPassword(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Login - Memoro</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen flex-col justify-between"
|
||||
style="background-color: {getPageBackground()};"
|
||||
>
|
||||
<!-- Language Selector and Theme Toggle in top right -->
|
||||
<div class="absolute right-4 top-4 z-50 flex items-center gap-3 opacity-60">
|
||||
<LanguageSelector />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<!-- Top Section - Logo and Welcome -->
|
||||
<div class="flex flex-col items-center justify-center pt-16 pb-8">
|
||||
<div
|
||||
class="flex items-center justify-center rounded-full transition-all mb-4 bg-black"
|
||||
style="width: 120px; height: 120px; border: 3px solid {getPrimaryColor()}; box-shadow: {isDark
|
||||
? '0 6px 12px rgba(0, 0, 0, 0.4)'
|
||||
: '0 6px 12px rgba(0, 0, 0, 0.15)'};"
|
||||
>
|
||||
<MemoroLogo size={55} color={getPrimaryColor()} />
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold" style="color: {isDark ? '#ffffff' : '#000000'};">
|
||||
Memoro
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Middle Section - Auth Buttons Container -->
|
||||
<div class="flex-1 flex items-start justify-center px-5 pt-8 pb-8">
|
||||
<div
|
||||
class="w-full max-w-md rounded-xl p-6"
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.7)'}; backdrop-filter: blur(10px); border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};"
|
||||
>
|
||||
<!-- Title -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-center text-xl font-semibold text-theme flex items-center justify-center gap-2">
|
||||
{#if mode === 'initial'}
|
||||
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z" />
|
||||
</svg>
|
||||
{$t('auth.mana_login')}
|
||||
{:else if mode === 'login'}
|
||||
{$t('auth.sign_in')}
|
||||
{:else if mode === 'forgot-password'}
|
||||
{$t('auth.reset_password')}
|
||||
{:else if mode === 'password-reset-success'}
|
||||
{$t('auth.reset_email_sent_title')}
|
||||
{/if}
|
||||
</h2>
|
||||
{#if mode === 'initial'}
|
||||
<div class="mt-3 flex flex-col items-center justify-center gap-2">
|
||||
<p class="text-sm text-center text-theme-muted">
|
||||
{$t('auth.mana_login_description')}
|
||||
</p>
|
||||
<button
|
||||
onclick={() => showManaInfoModal = true}
|
||||
class="flex items-center gap-2 text-xs font-medium hover:opacity-80 transition-opacity"
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="info" size={16} />
|
||||
Mehr erfahren
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
{#if error}
|
||||
<div class="mb-4 rounded-xl bg-red-500/20 border border-red-500/30 p-3">
|
||||
<p class="text-sm text-red-500">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if oauthError}
|
||||
<div class="mb-4 rounded-xl bg-red-500/20 border border-red-500/30 p-3">
|
||||
<p class="text-sm text-red-500">{oauthError}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Initial Mode -->
|
||||
{#if mode === 'initial'}
|
||||
<div class="mb-2 flex flex-col gap-3">
|
||||
<button
|
||||
onclick={() => goto('/register')}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 border-2"
|
||||
style="background-color: {getPrimaryColor()}60; border-color: {getPrimaryColor()}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="user-plus" size={20} />
|
||||
{$t('auth.create_account')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => switchMode('login')}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all bg-content hover:bg-menu-hover text-theme border border-theme"
|
||||
>
|
||||
<Icon name="sign-in" size={20} />
|
||||
{$t('auth.sign_in')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Terms and Privacy -->
|
||||
<div class="mt-4 text-center px-2">
|
||||
<p class="text-xs text-theme-muted">
|
||||
{@html $t('auth.terms_agreement')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Mode -->
|
||||
{:else if mode === 'login'}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleLogin();
|
||||
}}
|
||||
class="pb-4"
|
||||
>
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder="Email"
|
||||
required
|
||||
class="h-14 w-full rounded-xl border border-theme bg-content px-4 text-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary text-theme placeholder:text-theme-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
bind:value={password}
|
||||
placeholder={$t('auth.password')}
|
||||
required
|
||||
class="h-14 w-full rounded-xl border border-theme bg-content px-4 pr-12 text-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary text-theme placeholder:text-theme-muted"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => showPassword = !showPassword}
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 p-2 rounded-lg hover:bg-menu-hover transition-colors"
|
||||
aria-label={showPassword ? 'Passwort verbergen' : 'Passwort anzeigen'}
|
||||
>
|
||||
<Icon name={showPassword ? 'eye-off' : 'eye'} size={20} class="text-theme-muted" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => switchMode('forgot-password')}
|
||||
class="mb-4 flex h-10 w-full items-center justify-center rounded-xl font-medium transition-all bg-content hover:bg-menu-hover text-theme border border-theme"
|
||||
>
|
||||
{$t('auth.forgot_password')}
|
||||
</button>
|
||||
|
||||
<div class="mb-0 flex flex-col gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 disabled:opacity-50 border-2"
|
||||
style="background-color: {getPrimaryColor()}60; border-color: {getPrimaryColor()}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="sign-in" size={20} />
|
||||
{loading ? $t('auth.logging_in') : $t('auth.sign_in')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="my-4 flex items-center gap-3">
|
||||
<div class="flex-1 border-t border-theme-light"></div>
|
||||
<p class="text-xs text-theme-muted">{$t('common.or')}</p>
|
||||
<div class="flex-1 border-t border-theme-light"></div>
|
||||
</div>
|
||||
|
||||
<!-- Social Sign-In Options -->
|
||||
<div class="mb-4 flex flex-col gap-2">
|
||||
<GoogleSignInButton />
|
||||
<AppleSignInButton />
|
||||
</div>
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="mt-4">
|
||||
<button
|
||||
onclick={() => {
|
||||
resetForm();
|
||||
switchMode('initial');
|
||||
}}
|
||||
class="flex h-10 w-full items-center justify-center gap-2 rounded-xl font-medium transition-all hover:bg-menu-hover text-theme"
|
||||
>
|
||||
<Icon name="arrow-left" size={20} />
|
||||
{$t('auth.back')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Forgot Password Mode -->
|
||||
{:else if mode === 'forgot-password'}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleForgotPassword();
|
||||
}}
|
||||
class="pb-4"
|
||||
>
|
||||
<p class="mb-4 text-sm text-theme-secondary">
|
||||
{$t('auth.reset_password_description')}
|
||||
</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder={$t('auth.email')}
|
||||
required
|
||||
class="h-14 w-full rounded-xl border border-theme bg-content px-4 text-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary text-theme placeholder:text-theme-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 disabled:opacity-50 border-2"
|
||||
style="background-color: {getPrimaryColor()}60; border-color: {getPrimaryColor()}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="key" size={20} />
|
||||
{loading ? $t('auth.sending') : $t('auth.reset_password')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
resetForm();
|
||||
switchMode('login');
|
||||
}}
|
||||
class="flex h-10 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:bg-menu-hover text-theme"
|
||||
>
|
||||
<Icon name="arrow-left" size={20} />
|
||||
{$t('auth.back')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Password Reset Success Mode -->
|
||||
{:else if mode === 'password-reset-success'}
|
||||
<div class="pb-4">
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<div
|
||||
class="w-20 h-20 rounded-full flex items-center justify-center mb-6"
|
||||
style="background-color: {getPrimaryColor()}30;"
|
||||
>
|
||||
<Icon name="mail-open" size={40} color={getPrimaryColor()} />
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-theme-secondary text-center px-2">
|
||||
{$t('auth.reset_email_sent_description').replace('{email}', resetEmail)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
onclick={() => {
|
||||
resetEmail = '';
|
||||
switchMode('login');
|
||||
}}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 border-2"
|
||||
style="background-color: {getPrimaryColor()}60; border-color: {getPrimaryColor()}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="sign-in" size={20} />
|
||||
{$t('auth.back_to_login')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => switchMode('forgot-password')}
|
||||
class="flex h-10 items-center justify-center gap-2 rounded-xl font-medium transition-all bg-content hover:bg-menu-hover text-theme border border-theme"
|
||||
>
|
||||
{$t('auth.resend_email')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Section - App Slider -->
|
||||
{#if mode === 'initial'}
|
||||
<div class="w-full pb-8 px-2 pt-4">
|
||||
<AppSlider />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Mana Login Info Modal -->
|
||||
<Modal
|
||||
visible={showManaInfoModal}
|
||||
onClose={() => showManaInfoModal = false}
|
||||
title="Mana Login"
|
||||
maxWidth="lg"
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg class="h-6 w-6 text-theme" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z" />
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
{#snippet children()}
|
||||
<div class="space-y-4">
|
||||
<p class="text-theme">
|
||||
Mana Login ist dein zentraler Zugang zu allen Apps im Mana-Ökosystem. Mit einem
|
||||
einzigen Account kannst du dich bei allen Mana-Anwendungen anmelden.
|
||||
</p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-theme mb-1">Vorteile:</h4>
|
||||
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex items-start gap-2">
|
||||
<svg class="h-5 w-5 text-theme flex-shrink-0 mt-0.5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z" />
|
||||
</svg>
|
||||
<span class="text-theme">{$t('auth.mana_login_benefit_0')}</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex items-start gap-2">
|
||||
<Icon name="key" size={20} class="text-theme flex-shrink-0 mt-0.5" />
|
||||
<span class="text-theme">Ein Login für alle Mana Apps</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex items-start gap-2">
|
||||
<Icon name="shield-check" size={20} class="text-theme flex-shrink-0 mt-0.5" />
|
||||
<span class="text-theme">Sichere Authentifizierung mit modernen Standards</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex items-start gap-2">
|
||||
<Icon name="arrows-left-right" size={20} class="text-theme flex-shrink-0 mt-0.5" />
|
||||
<span class="text-theme">Synchronisation deiner Einstellungen über alle Apps hinweg</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex items-start gap-2">
|
||||
<Icon name="folder" size={20} class="text-theme flex-shrink-0 mt-0.5" />
|
||||
<span class="text-theme">Einfache Verwaltung deiner Daten an einem zentralen Ort</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-theme">
|
||||
Weitere Mana Apps werden in Zukunft hinzugefügt und können dann ebenfalls mit deinem
|
||||
Mana Login genutzt werden.
|
||||
</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
<button
|
||||
onclick={() => showManaInfoModal = false}
|
||||
class="w-full px-6 py-2 rounded-xl font-medium bg-content hover:bg-menu-hover text-theme border border-theme transition-colors"
|
||||
>
|
||||
Verstanden
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
</div>
|
||||
<LoginPage
|
||||
appName="Memoro"
|
||||
logo={MemoroLogo}
|
||||
primaryColor="#f8d62b"
|
||||
onSignIn={handleSignIn}
|
||||
onSignInWithGoogle={handleSignInWithGoogle}
|
||||
onForgotPassword={handleForgotPassword}
|
||||
goto={goto}
|
||||
enableGoogle={!!env.oauth.googleClientId}
|
||||
enableApple={!!env.oauth.appleClientId}
|
||||
successRedirect="/dashboard"
|
||||
registerPath="/register"
|
||||
lightBackground="#dddddd"
|
||||
darkBackground="#101010"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,408 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||
import ThemeSelector from '$lib/components/ThemeSelector.svelte';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import MemoroLogo from '$lib/components/MemoroLogo.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let success = $state(false);
|
||||
let needsVerification = $state(false);
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let showModal = $state(false);
|
||||
let showPassword = $state(false);
|
||||
let showConfirmPassword = $state(false);
|
||||
|
||||
let currentTheme = $derived($theme);
|
||||
let isDark = $derived(currentTheme.effectiveMode === 'dark');
|
||||
|
||||
// Password validation requirements
|
||||
let passwordRequirements = $derived.by(() => {
|
||||
if (!password) {
|
||||
return {
|
||||
length: false,
|
||||
lowercase: false,
|
||||
uppercase: false,
|
||||
digit: false,
|
||||
special: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
length: password.length >= 8,
|
||||
lowercase: /[a-z]/.test(password),
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
digit: /[0-9]/.test(password),
|
||||
special: /[^a-zA-Z0-9]/.test(password)
|
||||
};
|
||||
});
|
||||
|
||||
// Display OAuth error from URL if present
|
||||
let oauthError = $derived($page.url.searchParams.get('error'));
|
||||
|
||||
// Get primary color based on theme variant
|
||||
function getPrimaryColor() {
|
||||
const variant = currentTheme.variant;
|
||||
if (isDark) {
|
||||
const colors = {
|
||||
lume: '#f8d62b',
|
||||
nature: '#4CAF50',
|
||||
stone: '#78909C',
|
||||
ocean: '#039BE5'
|
||||
};
|
||||
return colors[variant];
|
||||
} else {
|
||||
const colors = {
|
||||
lume: '#f8d62b',
|
||||
nature: '#4CAF50',
|
||||
stone: '#607D8B',
|
||||
ocean: '#039BE5'
|
||||
};
|
||||
return colors[variant];
|
||||
}
|
||||
}
|
||||
|
||||
// Get page background based on theme variant
|
||||
function getPageBackground() {
|
||||
const variant = currentTheme.variant;
|
||||
if (isDark) {
|
||||
const colors = {
|
||||
lume: '#101010',
|
||||
nature: '#121212',
|
||||
stone: '#121212',
|
||||
ocean: '#121212'
|
||||
};
|
||||
return colors[variant];
|
||||
} else {
|
||||
const colors = {
|
||||
lume: '#dddddd',
|
||||
nature: '#FBFDF8',
|
||||
stone: '#F5F7F9',
|
||||
ocean: '#F5FCFF'
|
||||
};
|
||||
return colors[variant];
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister() {
|
||||
loading = true;
|
||||
error = null;
|
||||
success = false;
|
||||
|
||||
// Client-side validation
|
||||
if (!email) {
|
||||
error = $t('auth.error_email_required');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
error = $t('auth.error_password_required');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmPassword) {
|
||||
error = $t('auth.error_confirm_password');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
error = $t('auth.error_passwords_not_match');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
error = $t('auth.error_password_too_short');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check password strength
|
||||
const hasLowercase = /[a-z]/.test(password);
|
||||
const hasUppercase = /[A-Z]/.test(password);
|
||||
const hasDigit = /[0-9]/.test(password);
|
||||
const hasSymbol = /[^a-zA-Z0-9]/.test(password);
|
||||
|
||||
if (!hasLowercase || !hasUppercase || !hasDigit || !hasSymbol) {
|
||||
error = $t('auth.error_password_requirements');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await auth.signUp(email, password);
|
||||
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
if (result.needsVerification) {
|
||||
needsVerification = true;
|
||||
success = true;
|
||||
// Clear sensitive fields but keep email
|
||||
password = '';
|
||||
confirmPassword = '';
|
||||
} else {
|
||||
goto('/dashboard');
|
||||
}
|
||||
} else {
|
||||
error = result.error || $t('auth.error_registration_failed');
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
email = '';
|
||||
password = '';
|
||||
confirmPassword = '';
|
||||
error = null;
|
||||
success = false;
|
||||
needsVerification = false;
|
||||
async function handleSignUp(email: string, password: string) {
|
||||
return auth.signUp(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('auth.create_account')} - Memoro</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen flex-col justify-between"
|
||||
style="background-color: {getPageBackground()};"
|
||||
>
|
||||
<!-- Language Selector and Theme Toggle in top right -->
|
||||
<div class="absolute right-4 top-4 z-50 flex items-center gap-3 opacity-60">
|
||||
<LanguageSelector />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<!-- Top Section - Logo and Welcome -->
|
||||
<div class="flex flex-col items-center justify-center pt-16 pb-8">
|
||||
<div
|
||||
class="flex items-center justify-center rounded-full transition-all mb-4 bg-black"
|
||||
style="width: 120px; height: 120px; border: 3px solid {getPrimaryColor()}; box-shadow: {isDark
|
||||
? '0 6px 12px rgba(0, 0, 0, 0.4)'
|
||||
: '0 6px 12px rgba(0, 0, 0, 0.15)'};"
|
||||
>
|
||||
<MemoroLogo size={55} color={getPrimaryColor()} />
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold" style="color: {isDark ? '#ffffff' : '#000000'};">
|
||||
Memoro
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Middle Section - Auth Buttons Container -->
|
||||
<div class="flex-1 flex items-start justify-center px-5 pt-8 pb-8">
|
||||
<div
|
||||
class="w-full max-w-md rounded-xl p-6"
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.7)'}; backdrop-filter: blur(10px); border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};"
|
||||
>
|
||||
<!-- Title -->
|
||||
<h2
|
||||
class="mb-6 text-center text-xl font-semibold"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)'};"
|
||||
>
|
||||
{$t('auth.create_account')}
|
||||
</h2>
|
||||
|
||||
<!-- Error Messages -->
|
||||
{#if error}
|
||||
<div class="mb-4 rounded-lg bg-red-500/20 p-3">
|
||||
<p class="text-sm text-red-500">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if success && needsVerification}
|
||||
<div class="mb-4 rounded-lg bg-green-500/20 p-3">
|
||||
<p class="text-sm text-green-500">
|
||||
{$t('auth.registration_success')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if oauthError}
|
||||
<div class="mb-4 rounded-lg bg-red-500/20 p-3">
|
||||
<p class="text-sm text-red-500">{oauthError}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Email Register Form -->
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleRegister();
|
||||
}}
|
||||
class="pb-4"
|
||||
>
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder={$t('auth.email')}
|
||||
required
|
||||
class="h-14 w-full rounded-xl border px-4 text-lg transition-colors"
|
||||
style="background-color: {isDark
|
||||
? 'rgba(0, 0, 0, 0.2)'
|
||||
: 'rgba(241, 248, 233, 0.5)'}; border-color: {isDark
|
||||
? '#424242'
|
||||
: '#C8E6C9'}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="password"
|
||||
bind:value={password}
|
||||
placeholder={$t('auth.password')}
|
||||
required
|
||||
minlength="8"
|
||||
class="h-14 w-full rounded-xl border px-4 text-lg transition-colors"
|
||||
style="background-color: {isDark
|
||||
? 'rgba(0, 0, 0, 0.2)'
|
||||
: 'rgba(241, 248, 233, 0.5)'}; border-color: {isDark
|
||||
? '#424242'
|
||||
: '#C8E6C9'}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="password"
|
||||
bind:value={confirmPassword}
|
||||
placeholder={$t('auth.confirm_password')}
|
||||
required
|
||||
minlength="8"
|
||||
class="h-14 w-full rounded-xl border px-4 text-lg transition-colors"
|
||||
style="background-color: {isDark
|
||||
? 'rgba(0, 0, 0, 0.2)'
|
||||
: 'rgba(241, 248, 233, 0.5)'}; border-color: {isDark
|
||||
? '#424242'
|
||||
: '#C8E6C9'}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="mb-4 mt-2 text-xs"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'};"
|
||||
>
|
||||
{$t('auth.password_requirement')}
|
||||
</p>
|
||||
|
||||
<div class="mb-0 flex flex-col gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 disabled:opacity-50 border-2"
|
||||
style="background-color: {getPrimaryColor()}60; border-color: {getPrimaryColor()}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="user-plus" size={20} />
|
||||
{loading ? $t('auth.creating_account') : $t('auth.create_account')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Info about email-only authentication -->
|
||||
<div class="mt-4 mb-2 rounded-xl p-4 flex flex-col items-center gap-2" style="background-color: {isDark ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'};">
|
||||
<p class="text-xs text-center" style="color: {isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'};">
|
||||
{$t('auth.email_only_info')}
|
||||
</p>
|
||||
<button
|
||||
onclick={() => showModal = true}
|
||||
class="flex items-center gap-2 text-xs font-medium hover:opacity-80 transition-opacity"
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="info" size={16} />
|
||||
{$t('auth.email_only_learn_more')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="mt-4">
|
||||
<button
|
||||
onclick={() => goto('/login')}
|
||||
class="flex h-10 w-full items-center justify-center gap-2 rounded-xl font-medium transition-all"
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="arrow-left" size={20} />
|
||||
{$t('common.back')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modal for "Learn More" -->
|
||||
<Modal visible={showModal} onClose={() => showModal = false} title={$t('auth.email_only_title')} maxWidth="lg">
|
||||
{#snippet children()}
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm leading-relaxed text-theme">
|
||||
{$t('auth.email_only_intro')}
|
||||
</p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex gap-3">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<Icon name="lock" size={20} class="text-theme" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium mb-1 text-theme">{$t('auth.email_only_benefit_1_title')}</h3>
|
||||
<p class="text-sm text-theme">{$t('auth.email_only_benefit_1_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex gap-3">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<Icon name="shield-check" size={20} class="text-theme" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium mb-1 text-theme">{$t('auth.email_only_benefit_2_title')}</h3>
|
||||
<p class="text-sm text-theme">{$t('auth.email_only_benefit_2_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex gap-3">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<Icon name="arrows-left-right" size={20} class="text-theme" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium mb-1 text-theme">{$t('auth.email_only_benefit_3_title')}</h3>
|
||||
<p class="text-sm text-theme">{$t('auth.email_only_benefit_3_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-content p-3 border border-theme flex gap-3">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<Icon name="envelope" size={20} class="text-theme" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium mb-1 text-theme">{$t('auth.email_only_benefit_4_title')}</h3>
|
||||
<p class="text-sm text-theme">{$t('auth.email_only_benefit_4_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm leading-relaxed text-theme">
|
||||
{$t('auth.email_only_modal_footer')}
|
||||
</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
<button
|
||||
onclick={() => showModal = false}
|
||||
class="w-full px-6 py-2 rounded-xl font-medium bg-content hover:bg-menu-hover text-theme border border-theme transition-colors"
|
||||
>
|
||||
{$t('auth.got_it')}
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
<RegisterPage
|
||||
appName="Memoro"
|
||||
logo={MemoroLogo}
|
||||
primaryColor="#f8d62b"
|
||||
onSignUp={handleSignUp}
|
||||
goto={goto}
|
||||
successRedirect="/dashboard"
|
||||
loginPath="/login"
|
||||
lightBackground="#dddddd"
|
||||
darkBackground="#101010"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
|
||||
let { data } = $props();
|
||||
interface PageData {
|
||||
session?: Session | null;
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (data.session) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
parseAppleAuthorizationResponse,
|
||||
getStoredReturnUrl,
|
||||
clearAppleSignInSession
|
||||
} from '$lib/utils/appleAuth';
|
||||
} from '@manacore/shared-auth-ui';
|
||||
|
||||
let error = $state<string | null>(null);
|
||||
let processing = $state(true);
|
||||
|
|
|
|||
|
|
@ -1,165 +1,12 @@
|
|||
import preset from '@manacore/shared-tailwind/preset';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Mana - Consistent color for subscription page
|
||||
mana: '#4287f5',
|
||||
// Lume Theme - Modern Gold & Dark
|
||||
lume: {
|
||||
primary: '#f8d62b',
|
||||
primaryButton: '#f8d62b',
|
||||
primaryButtonText: '#000000',
|
||||
secondary: '#D4B200',
|
||||
secondaryButton: '#FFE9A3',
|
||||
contentBackground: '#ffffff',
|
||||
contentBackgroundHover: '#f5f5f5',
|
||||
contentPageBackground: '#ffffff',
|
||||
menuBackground: '#dddddd',
|
||||
menuBackgroundHover: '#cccccc',
|
||||
pageBackground: '#dddddd',
|
||||
text: '#2c2c2c',
|
||||
borderLight: '#f2f2f2',
|
||||
border: '#e6e6e6',
|
||||
borderStrong: '#cccccc',
|
||||
error: '#e74c3c'
|
||||
},
|
||||
// Nature Theme - Soothing Green
|
||||
nature: {
|
||||
primary: '#4CAF50',
|
||||
primaryButton: '#A08500',
|
||||
primaryButtonText: '#ffffff',
|
||||
secondary: '#81C784',
|
||||
secondaryButton: '#F1F8E9',
|
||||
contentBackground: '#F1F8E9',
|
||||
contentBackgroundHover: '#E8F5E9',
|
||||
contentPageBackground: '#ffffff',
|
||||
menuBackground: '#E8F5E9',
|
||||
menuBackgroundHover: '#C8E6C9',
|
||||
pageBackground: '#FBFDF8',
|
||||
text: '#1B5E20',
|
||||
borderLight: '#E8F5E9',
|
||||
border: '#C8E6C9',
|
||||
borderStrong: '#A5D6A7',
|
||||
error: '#E57373'
|
||||
},
|
||||
// Stone Theme - Elegant Slate
|
||||
stone: {
|
||||
primary: '#607D8B',
|
||||
primaryButton: '#FF9500',
|
||||
primaryButtonText: '#000000',
|
||||
secondary: '#90A4AE',
|
||||
secondaryButton: '#ECEFF1',
|
||||
contentBackground: '#ECEFF1',
|
||||
contentBackgroundHover: '#E0E6EA',
|
||||
contentPageBackground: '#ffffff',
|
||||
menuBackground: '#E0E6EA',
|
||||
menuBackgroundHover: '#CFD8DC',
|
||||
pageBackground: '#F5F7F9',
|
||||
text: '#263238',
|
||||
borderLight: '#ECEFF1',
|
||||
border: '#CFD8DC',
|
||||
borderStrong: '#B0BEC5',
|
||||
error: '#EF5350'
|
||||
},
|
||||
// Ocean Theme - Tranquil Blue
|
||||
ocean: {
|
||||
primary: '#039BE5',
|
||||
primaryButton: '#FF9500',
|
||||
primaryButtonText: '#000000',
|
||||
secondary: '#4FC3F7',
|
||||
secondaryButton: '#E1F5FE',
|
||||
contentBackground: '#E1F5FE',
|
||||
contentBackgroundHover: '#B3E5FC',
|
||||
contentPageBackground: '#ffffff',
|
||||
menuBackground: '#E1F5FE',
|
||||
menuBackgroundHover: '#B3E5FC',
|
||||
pageBackground: '#F5FCFF',
|
||||
text: '#01579B',
|
||||
borderLight: '#E1F5FE',
|
||||
border: '#B3E5FC',
|
||||
borderStrong: '#81D4FA',
|
||||
error: '#EF5350'
|
||||
},
|
||||
// Dark Mode Variants
|
||||
dark: {
|
||||
lume: {
|
||||
primary: '#f8d62b',
|
||||
primaryButton: '#7C6B16',
|
||||
primaryButtonText: '#ffffff',
|
||||
secondary: '#D4B200',
|
||||
secondaryButton: '#1E1E1E',
|
||||
contentBackground: '#1E1E1E',
|
||||
contentBackgroundHover: '#333333',
|
||||
contentPageBackground: '#121212',
|
||||
menuBackground: '#101010',
|
||||
menuBackgroundHover: '#333333',
|
||||
pageBackground: '#101010',
|
||||
text: '#ffffff',
|
||||
borderLight: '#333333',
|
||||
border: '#424242',
|
||||
borderStrong: '#616161',
|
||||
error: '#e74c3c'
|
||||
},
|
||||
nature: {
|
||||
primary: '#4CAF50',
|
||||
primaryButton: '#FF9500',
|
||||
primaryButtonText: '#000000',
|
||||
secondary: '#81C784',
|
||||
secondaryButton: '#1E1E1E',
|
||||
contentBackground: '#1E1E1E',
|
||||
contentBackgroundHover: '#2E7D32',
|
||||
contentPageBackground: '#121212',
|
||||
menuBackground: '#252525',
|
||||
menuBackgroundHover: '#2E7D32',
|
||||
pageBackground: '#121212',
|
||||
text: '#FFFFFF',
|
||||
borderLight: '#1B5E20',
|
||||
border: '#2E7D32',
|
||||
borderStrong: '#388E3C',
|
||||
error: '#CF6679'
|
||||
},
|
||||
stone: {
|
||||
primary: '#78909C',
|
||||
primaryButton: '#FF9500',
|
||||
primaryButtonText: '#000000',
|
||||
secondary: '#90A4AE',
|
||||
secondaryButton: '#1E1E1E',
|
||||
contentBackground: '#1E1E1E',
|
||||
contentBackgroundHover: '#37474F',
|
||||
contentPageBackground: '#121212',
|
||||
menuBackground: '#252525',
|
||||
menuBackgroundHover: '#37474F',
|
||||
pageBackground: '#121212',
|
||||
text: '#FFFFFF',
|
||||
borderLight: '#37474F',
|
||||
border: '#455A64',
|
||||
borderStrong: '#546E7A',
|
||||
error: '#CF6679'
|
||||
},
|
||||
ocean: {
|
||||
primary: '#039BE5',
|
||||
primaryButton: '#FF9500',
|
||||
primaryButtonText: '#000000',
|
||||
secondary: '#4FC3F7',
|
||||
secondaryButton: '#1E1E1E',
|
||||
contentBackground: '#1E1E1E',
|
||||
contentBackgroundHover: '#0277BD',
|
||||
contentPageBackground: '#121212',
|
||||
menuBackground: '#252525',
|
||||
menuBackgroundHover: '#0277BD',
|
||||
pageBackground: '#121212',
|
||||
text: '#FFFFFF',
|
||||
borderLight: '#01579B',
|
||||
border: '#0277BD',
|
||||
borderStrong: '#0288D1',
|
||||
error: '#CF6679'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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}'
|
||||
],
|
||||
plugins: [require('@tailwindcss/typography')]
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue