diff --git a/apps/skilltree/apps/web/src/lib/data/guest-seed.ts b/apps/skilltree/apps/web/src/lib/data/guest-seed.ts
index 33f5d7ef2..8dca1792e 100644
--- a/apps/skilltree/apps/web/src/lib/data/guest-seed.ts
+++ b/apps/skilltree/apps/web/src/lib/data/guest-seed.ts
@@ -1,32 +1,45 @@
/**
* Guest seed data for the SkilltTree app.
*
- * Provides a demo skill with an activity to showcase the leveling system.
+ * Provides demo skills across multiple branches with activities and an achievement
+ * to showcase the leveling system, XP progression, and activity feed.
*/
-import type { LocalSkill, LocalActivity } from './local-store';
+import type { LocalSkill, LocalActivity, LocalAchievement } from './local-store';
-const DEMO_SKILL_ID = 'demo-coding';
+const DEMO_CODING_ID = 'demo-coding';
+const DEMO_FITNESS_ID = 'demo-fitness';
+const DEMO_CREATIVE_ID = 'demo-creative';
export const guestSkills: LocalSkill[] = [
{
- id: DEMO_SKILL_ID,
+ id: DEMO_CODING_ID,
name: 'Programmieren',
description: 'Software-Entwicklung und Coding-Skills',
branch: 'intellect',
icon: 'đź’»',
- currentXp: 150,
- totalXp: 150,
+ currentXp: 250,
+ totalXp: 250,
level: 1,
},
{
- id: 'demo-fitness',
+ id: DEMO_FITNESS_ID,
name: 'Fitness',
description: 'Körperliche Fitness und Training',
branch: 'body',
icon: 'đź’Ş',
- currentXp: 50,
- totalXp: 50,
+ currentXp: 120,
+ totalXp: 120,
+ level: 1,
+ },
+ {
+ id: DEMO_CREATIVE_ID,
+ name: 'Zeichnen',
+ description: 'Illustration, Skizzen und visuelles Denken',
+ branch: 'creativity',
+ icon: '🎨',
+ currentXp: 60,
+ totalXp: 60,
level: 0,
},
];
@@ -34,18 +47,61 @@ export const guestSkills: LocalSkill[] = [
export const guestActivities: LocalActivity[] = [
{
id: 'activity-1',
- skillId: DEMO_SKILL_ID,
+ skillId: DEMO_CODING_ID,
xpEarned: 100,
description: 'TypeScript-Projekt aufgesetzt',
duration: 60,
- timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
+ timestamp: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString(),
},
{
id: 'activity-2',
- skillId: DEMO_SKILL_ID,
+ skillId: DEMO_FITNESS_ID,
+ xpEarned: 50,
+ description: '5 km Joggen im Park',
+ duration: 35,
+ timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
+ },
+ {
+ id: 'activity-3',
+ skillId: DEMO_CODING_ID,
+ xpEarned: 100,
+ description: 'REST API mit Hono gebaut',
+ duration: 90,
+ timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
+ },
+ {
+ id: 'activity-4',
+ skillId: DEMO_CREATIVE_ID,
+ xpEarned: 60,
+ description: 'Erste Skizzen mit Procreate',
+ duration: 45,
+ timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
+ },
+ {
+ id: 'activity-5',
+ skillId: DEMO_FITNESS_ID,
+ xpEarned: 70,
+ description: 'Krafttraining — Oberkörper',
+ duration: 50,
+ timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
+ },
+ {
+ id: 'activity-6',
+ skillId: DEMO_CODING_ID,
xpEarned: 50,
description: 'Unit Tests geschrieben',
duration: 30,
- timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
+ timestamp: new Date(Date.now() - 12 * 60 * 60 * 1000).toISOString(),
+ },
+];
+
+export const guestAchievements: LocalAchievement[] = [
+ {
+ id: 'achievement-1',
+ key: 'first-skill',
+ name: 'Erste Schritte',
+ description: 'Deinen ersten Skill erstellt',
+ icon: '🌱',
+ unlockedAt: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString(),
},
];
diff --git a/apps/skilltree/apps/web/src/lib/data/local-store.ts b/apps/skilltree/apps/web/src/lib/data/local-store.ts
index 55ae43126..1381ecdcf 100644
--- a/apps/skilltree/apps/web/src/lib/data/local-store.ts
+++ b/apps/skilltree/apps/web/src/lib/data/local-store.ts
@@ -6,7 +6,7 @@
*/
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
-import { guestSkills, guestActivities } from './guest-seed';
+import { guestSkills, guestActivities, guestAchievements } from './guest-seed';
// ─── Types ──────────────────────────────────────────────────
@@ -58,6 +58,7 @@ export const skilltreeStore = createLocalStore({
{
name: 'achievements',
indexes: ['key', 'unlockedAt'],
+ guestSeed: guestAchievements,
},
],
sync: {
diff --git a/apps/todo/apps/web/src/routes/(app)/+layout.svelte b/apps/todo/apps/web/src/routes/(app)/+layout.svelte
index 5ac610c45..95a74915c 100644
--- a/apps/todo/apps/web/src/routes/(app)/+layout.svelte
+++ b/apps/todo/apps/web/src/routes/(app)/+layout.svelte
@@ -43,7 +43,12 @@
import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from '$lib/utils/task-parser';
import { todoOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
- import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
+ import {
+ SessionExpiredBanner,
+ AuthGate,
+ GuestWelcomeModal,
+ GuestRegistrationNudge,
+ } from '@manacore/shared-auth-ui';
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import { TodoEvents } from '@manacore/shared-utils/analytics';
import { todoStore, taskCollection } from '$lib/data/local-store';
@@ -554,6 +559,13 @@
{#if authStore.isAuthenticated}
+ {:else}
+ goto('/register')}
+ locale={($locale || 'de') === 'de' ? 'de' : 'en'}
+ delayMinutes={3}
+ />
{/if}
diff --git a/apps/zitare/apps/web/src/lib/data/guest-seed.ts b/apps/zitare/apps/web/src/lib/data/guest-seed.ts
index a7a5e6805..479b9d159 100644
--- a/apps/zitare/apps/web/src/lib/data/guest-seed.ts
+++ b/apps/zitare/apps/web/src/lib/data/guest-seed.ts
@@ -5,18 +5,26 @@
import type { LocalFavorite, LocalQuoteList } from './local-store';
-// Some well-known quote IDs from the content package
+// Well-known quote IDs from @zitare/content
export const guestFavorites: LocalFavorite[] = [
{ id: 'fav-1', quoteId: 'mot-1' },
{ id: 'fav-2', quoteId: 'weis-3' },
{ id: 'fav-3', quoteId: 'mot-7' },
+ { id: 'fav-4', quoteId: 'weis-1' },
+ { id: 'fav-5', quoteId: 'liebe-1' },
];
export const guestLists: LocalQuoteList[] = [
{
- id: 'list-onboarding',
- name: 'Meine Lieblingszitate',
- description: 'Eine Beispiel-Sammlung zum Ausprobieren',
- quoteIds: ['mot-1', 'weis-3'],
+ id: 'list-motivation',
+ name: 'Motivation & Antrieb',
+ description: 'Zitate die dich voranbringen',
+ quoteIds: ['mot-1', 'mot-7', 'mot-3'],
+ },
+ {
+ id: 'list-weisheit',
+ name: 'Zeitlose Weisheiten',
+ description: 'Die groĂźen Denker und Dichter',
+ quoteIds: ['weis-1', 'weis-3', 'weis-5'],
},
];
diff --git a/packages/shared-auth-ui/src/components/GuestRegistrationNudge.svelte b/packages/shared-auth-ui/src/components/GuestRegistrationNudge.svelte
new file mode 100644
index 000000000..e06cd3b05
--- /dev/null
+++ b/packages/shared-auth-ui/src/components/GuestRegistrationNudge.svelte
@@ -0,0 +1,225 @@
+
+
+{#if visible}
+
+
+
{texts.message}
+
+
+
+
+
+
+{/if}
+
+
diff --git a/packages/shared-auth-ui/src/components/GuestWelcomeModal.svelte b/packages/shared-auth-ui/src/components/GuestWelcomeModal.svelte
index 590d44772..79ec2f077 100644
--- a/packages/shared-auth-ui/src/components/GuestWelcomeModal.svelte
+++ b/packages/shared-auth-ui/src/components/GuestWelcomeModal.svelte
@@ -48,6 +48,26 @@
zitare: ['Tägliche Inspiration für dich', 'Quelloffen & unabhängig', 'Privat by Design'],
picture: ['Kreativität trifft KI', 'Quelloffen & unabhängig', 'Privat by Design'],
cards: ['Lernen leicht gemacht', 'Quelloffen & unabhängig', 'Privat by Design'],
+ moodlit: ['Dein Raum, deine Stimmung', 'Quelloffen & unabhängig', 'Privat by Design'],
+ calc: ['Rechnen ohne Ablenkung', 'Quelloffen & unabhängig', 'Privat by Design'],
+ guides: ['Anleitungen, die funktionieren', 'Quelloffen & unabhängig', 'Privat by Design'],
+ citycorners: ['Entdecke deine Stadt', 'Quelloffen & unabhängig', 'Privat by Design'],
+ planta: ['Pflanzenpflege leicht gemacht', 'Quelloffen & unabhängig', 'Privat by Design'],
+ photos: ['Deine Fotos, deine Galerie', 'Quelloffen & unabhängig', 'Privat by Design'],
+ questions: ['Recherche mit System', 'Quelloffen & unabhängig', 'Privat by Design'],
+ context: ['Dein Wissen, strukturiert', 'Quelloffen & unabhängig', 'Privat by Design'],
+ presi: ['Präsentationen neu gedacht', 'Quelloffen & unabhängig', 'Privat by Design'],
+ mukke: ['Musik machen, einfach so', 'Quelloffen & unabhängig', 'Privat by Design'],
+ storage: ['Deine Dateien, dein Tresor', 'Quelloffen & unabhängig', 'Privat by Design'],
+ times: ['Zeiterfassung ohne Overhead', 'Quelloffen & unabhängig', 'Privat by Design'],
+ inventar: ['Alles im Überblick behalten', 'Quelloffen & unabhängig', 'Privat by Design'],
+ uload: ['Links kürzen & verwalten', 'Quelloffen & unabhängig', 'Privat by Design'],
+ news: ['Nachrichten, kuratiert für dich', 'Quelloffen & unabhängig', 'Privat by Design'],
+ arcade: ['Spiele direkt im Browser', 'Quelloffen & unabhängig', 'Privat by Design'],
+ skilltree: ['Dein Fortschritt, sichtbar', 'Quelloffen & unabhängig', 'Privat by Design'],
+ nutriphi: ['Ernährung bewusst leben', 'Quelloffen & unabhängig', 'Privat by Design'],
+ wisekeep: ['Wissen bewahren & teilen', 'Quelloffen & unabhängig', 'Privat by Design'],
+ memoro: ['Sprache wird zu Wissen', 'Quelloffen & unabhängig', 'Privat by Design'],
};
/** Default features per app (English) */
@@ -60,6 +80,26 @@
zitare: ['Daily inspiration for you', 'Open-source & independent', 'Private by design'],
picture: ['Where creativity meets AI', 'Open-source & independent', 'Private by design'],
cards: ['Learning made easy', 'Open-source & independent', 'Private by design'],
+ moodlit: ['Your space, your mood', 'Open-source & independent', 'Private by design'],
+ calc: ['Calculate without distraction', 'Open-source & independent', 'Private by design'],
+ guides: ['Guides that actually work', 'Open-source & independent', 'Private by design'],
+ citycorners: ['Discover your city', 'Open-source & independent', 'Private by design'],
+ planta: ['Plant care made simple', 'Open-source & independent', 'Private by design'],
+ photos: ['Your photos, your gallery', 'Open-source & independent', 'Private by design'],
+ questions: ['Research with structure', 'Open-source & independent', 'Private by design'],
+ context: ['Your knowledge, organized', 'Open-source & independent', 'Private by design'],
+ presi: ['Presentations reimagined', 'Open-source & independent', 'Private by design'],
+ mukke: ['Make music, just like that', 'Open-source & independent', 'Private by design'],
+ storage: ['Your files, your vault', 'Open-source & independent', 'Private by design'],
+ times: ['Time tracking without overhead', 'Open-source & independent', 'Private by design'],
+ inventar: ['Keep track of everything', 'Open-source & independent', 'Private by design'],
+ uload: ['Shorten & manage links', 'Open-source & independent', 'Private by design'],
+ news: ['News, curated for you', 'Open-source & independent', 'Private by design'],
+ arcade: ['Games right in your browser', 'Open-source & independent', 'Private by design'],
+ skilltree: ['Your progress, visualized', 'Open-source & independent', 'Private by design'],
+ nutriphi: ['Mindful nutrition tracking', 'Open-source & independent', 'Private by design'],
+ wisekeep: ['Preserve & share knowledge', 'Open-source & independent', 'Private by design'],
+ memoro: ['Voice becomes knowledge', 'Open-source & independent', 'Private by design'],
};
interface Props {
diff --git a/packages/shared-auth-ui/src/index.ts b/packages/shared-auth-ui/src/index.ts
index 9e7000db2..cc159c6e0 100644
--- a/packages/shared-auth-ui/src/index.ts
+++ b/packages/shared-auth-ui/src/index.ts
@@ -5,6 +5,7 @@ export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.svelte
// Components
export { default as GuestWelcomeModal } from './components/GuestWelcomeModal.svelte';
+export { default as GuestRegistrationNudge } from './components/GuestRegistrationNudge.svelte';
export { default as AuthGateModal } from './components/AuthGateModal.svelte';
export { default as SessionExpiredBanner } from './components/SessionExpiredBanner.svelte';
export { default as AuthGate } from './components/AuthGate.svelte';
@@ -23,6 +24,13 @@ export {
resetGuestWelcome,
resetAllGuestWelcome,
} from './utils/guestWelcome';
+export {
+ startGuestSession,
+ shouldShowGuestNudge,
+ dismissGuestNudge,
+ resetGuestNudge,
+ resetAllGuestNudges,
+} from './utils/guestNudge';
export { parseUserAgent, getDeviceType, formatUserAgent } from './utils/userAgent';
// Types
diff --git a/packages/shared-auth-ui/src/utils/guestNudge.ts b/packages/shared-auth-ui/src/utils/guestNudge.ts
new file mode 100644
index 000000000..6c5b1e9b5
--- /dev/null
+++ b/packages/shared-auth-ui/src/utils/guestNudge.ts
@@ -0,0 +1,69 @@
+/**
+ * Utility functions for managing guest registration nudge state.
+ * Shows a nudge after X minutes of guest usage to encourage sign-up.
+ */
+
+const SESSION_PREFIX = 'guest-nudge-session';
+const DISMISSED_PREFIX = 'guest-nudge-dismissed';
+
+/**
+ * Record the start of a guest session for an app (call once on mount).
+ * Only sets the timestamp if one doesn't already exist.
+ */
+export function startGuestSession(appId: string): void {
+ if (typeof localStorage === 'undefined') return;
+ const key = `${SESSION_PREFIX}-${appId}`;
+ if (!localStorage.getItem(key)) {
+ localStorage.setItem(key, Date.now().toString());
+ }
+}
+
+/**
+ * Check if enough time has passed to show the registration nudge.
+ * Returns false if already dismissed or not enough time elapsed.
+ */
+export function shouldShowGuestNudge(appId: string, delayMinutes = 5): boolean {
+ if (typeof localStorage === 'undefined') return false;
+
+ // Already dismissed?
+ if (localStorage.getItem(`${DISMISSED_PREFIX}-${appId}`) === 'true') return false;
+
+ // Check elapsed time
+ const sessionStart = localStorage.getItem(`${SESSION_PREFIX}-${appId}`);
+ if (!sessionStart) return false;
+
+ const elapsed = Date.now() - parseInt(sessionStart, 10);
+ return elapsed >= delayMinutes * 60 * 1000;
+}
+
+/**
+ * Permanently dismiss the nudge for an app.
+ */
+export function dismissGuestNudge(appId: string): void {
+ if (typeof localStorage === 'undefined') return;
+ localStorage.setItem(`${DISMISSED_PREFIX}-${appId}`, 'true');
+}
+
+/**
+ * Reset nudge state for an app (will show again after delay).
+ */
+export function resetGuestNudge(appId: string): void {
+ if (typeof localStorage === 'undefined') return;
+ localStorage.removeItem(`${SESSION_PREFIX}-${appId}`);
+ localStorage.removeItem(`${DISMISSED_PREFIX}-${appId}`);
+}
+
+/**
+ * Reset nudge state for all apps.
+ */
+export function resetAllGuestNudges(): void {
+ if (typeof localStorage === 'undefined') return;
+ const keysToRemove: string[] = [];
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key?.startsWith(SESSION_PREFIX) || key?.startsWith(DISMISSED_PREFIX)) {
+ keysToRemove.push(key);
+ }
+ }
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
+}