From 6d0d9d4f67c2d9c6b488a1690f218867b66aec91 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:57:20 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(questions):=20add=20demo=20mod?= =?UTF-8?q?e=20for=20unauthenticated=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add demo-questions.ts with sample questions and collection - Update questionsStore to show demo data when not authenticated - Update collectionsStore with demo mode support - Add demo banner and GuestWelcomeModal to layout - Block create/update/delete operations in demo mode --- .../apps/web/src/lib/data/demo-questions.ts | 118 +++++++++++++++ .../web/src/lib/stores/collections.svelte.ts | 68 +++++++++ .../web/src/lib/stores/questions.svelte.ts | 90 ++++++++++++ .../apps/web/src/routes/(app)/+layout.svelte | 135 ++++++++++++++++-- 4 files changed, 396 insertions(+), 15 deletions(-) create mode 100644 apps/questions/apps/web/src/lib/data/demo-questions.ts diff --git a/apps/questions/apps/web/src/lib/data/demo-questions.ts b/apps/questions/apps/web/src/lib/data/demo-questions.ts new file mode 100644 index 000000000..617a013f3 --- /dev/null +++ b/apps/questions/apps/web/src/lib/data/demo-questions.ts @@ -0,0 +1,118 @@ +/** + * Demo Questions - Static sample questions for unauthenticated users + * + * Shows a realistic set of research questions to demonstrate + * the app's capabilities without requiring login. + */ + +import type { Question, Collection } from '$lib/types'; + +/** + * Demo collection for sample questions + */ +export const DEMO_COLLECTION: Collection = { + id: 'demo-collection', + userId: 'demo', + name: 'Tech Research', + description: 'Sample technology research questions', + color: '#8b5cf6', + icon: 'folder', + isDefault: true, + sortOrder: 0, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + questionCount: 5, +}; + +/** + * Generate demo questions for unauthenticated users + */ +export function generateDemoQuestions(): Question[] { + const now = new Date(); + + return [ + { + id: 'demo_1', + userId: 'demo', + collectionId: DEMO_COLLECTION.id, + title: 'What are the key differences between React Server Components and traditional SSR?', + description: + 'I want to understand how RSC differs from traditional server-side rendering approaches and when to use each.', + status: 'answered', + priority: 'high', + tags: ['react', 'ssr', 'web-development'], + researchDepth: 'deep', + createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 'demo_2', + userId: 'demo', + collectionId: DEMO_COLLECTION.id, + title: 'How does vector database similarity search work under the hood?', + description: + 'Exploring the algorithms and data structures used in vector databases for semantic search.', + status: 'researching', + priority: 'normal', + tags: ['ai', 'databases', 'embeddings'], + researchDepth: 'standard', + createdAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(), + }, + { + id: 'demo_3', + userId: 'demo', + collectionId: DEMO_COLLECTION.id, + title: 'What are best practices for implementing OAuth 2.0 with PKCE in mobile apps?', + description: 'Security considerations and implementation patterns for mobile authentication.', + status: 'open', + priority: 'urgent', + tags: ['security', 'oauth', 'mobile'], + researchDepth: 'deep', + createdAt: new Date(now.getTime() - 6 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(now.getTime() - 6 * 60 * 60 * 1000).toISOString(), + }, + { + id: 'demo_4', + userId: 'demo', + collectionId: DEMO_COLLECTION.id, + title: 'How do transformer models handle context length limitations?', + description: + 'Understanding techniques like sliding window attention, sparse attention, and context compression.', + status: 'answered', + priority: 'normal', + tags: ['ai', 'transformers', 'llm'], + researchDepth: 'standard', + createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 'demo_5', + userId: 'demo', + collectionId: DEMO_COLLECTION.id, + title: 'What are the trade-offs between monorepo and polyrepo architectures?', + description: + 'Comparing build systems, dependency management, and team collaboration patterns.', + status: 'open', + priority: 'low', + tags: ['architecture', 'devops', 'tooling'], + researchDepth: 'quick', + createdAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), + }, + ]; +} + +/** + * Check if an ID belongs to a demo question + */ +export function isDemoQuestion(id: string): boolean { + return id.startsWith('demo_'); +} + +/** + * Check if an ID belongs to the demo collection + */ +export function isDemoCollection(id: string): boolean { + return id === DEMO_COLLECTION.id; +} diff --git a/apps/questions/apps/web/src/lib/stores/collections.svelte.ts b/apps/questions/apps/web/src/lib/stores/collections.svelte.ts index 71f4797b0..17326f85d 100644 --- a/apps/questions/apps/web/src/lib/stores/collections.svelte.ts +++ b/apps/questions/apps/web/src/lib/stores/collections.svelte.ts @@ -1,9 +1,13 @@ /** * Collections Store - Manages collections state using Svelte 5 runes + * Authenticated users: collections from API + * Demo mode: static sample collection to showcase the app */ import { collectionsApi } from '$lib/api/collections'; import type { Collection, CreateCollectionDto, UpdateCollectionDto } from '$lib/types'; +import { authStore } from './auth.svelte'; +import { DEMO_COLLECTION, isDemoCollection } from '$lib/data/demo-questions'; let collections = $state([]); let loading = $state(false); @@ -27,10 +31,23 @@ export const collectionsStore = { return selectedId ? collections.find((c) => c.id === selectedId) : null; }, + /** + * Load collections + * Demo mode: shows static sample collection + * Authenticated: fetches from API + */ async load() { loading = true; error = null; + // Demo mode: load demo collection + if (!authStore.isAuthenticated) { + collections = [DEMO_COLLECTION]; + loading = false; + return; + } + + // Authenticated: fetch from API try { collections = await collectionsApi.getAll(); } catch (e) { @@ -41,7 +58,18 @@ export const collectionsStore = { } }, + /** + * Create a new collection + * Demo mode: returns auth_required error + * Authenticated: creates via API + */ async create(data: CreateCollectionDto): Promise { + // Demo mode: require authentication + if (!authStore.isAuthenticated) { + error = 'Login required to create collections'; + return null; + } + loading = true; error = null; @@ -57,7 +85,18 @@ export const collectionsStore = { } }, + /** + * Update a collection + * Demo mode: returns auth_required error + * Authenticated: updates via API + */ async update(id: string, data: UpdateCollectionDto): Promise { + // Demo collection or not authenticated: require authentication + if (isDemoCollection(id) || !authStore.isAuthenticated) { + error = 'Login required to update collections'; + return null; + } + error = null; try { @@ -70,7 +109,18 @@ export const collectionsStore = { } }, + /** + * Delete a collection + * Demo mode: returns auth_required error + * Authenticated: deletes via API + */ async delete(id: string): Promise { + // Demo collection or not authenticated: require authentication + if (isDemoCollection(id) || !authStore.isAuthenticated) { + error = 'Login required to delete collections'; + return false; + } + error = null; try { @@ -86,7 +136,18 @@ export const collectionsStore = { } }, + /** + * Reorder collections + * Demo mode: returns auth_required error + * Authenticated: reorders via API + */ async reorder(orderedIds: string[]): Promise { + // Demo mode: require authentication + if (!authStore.isAuthenticated) { + error = 'Login required to reorder collections'; + return false; + } + error = null; try { @@ -111,6 +172,13 @@ export const collectionsStore = { return collections.find((c) => c.id === id); }, + /** + * Check if a collection is a demo collection + */ + isDemoCollection(id: string): boolean { + return isDemoCollection(id); + }, + clear() { collections = []; error = null; diff --git a/apps/questions/apps/web/src/lib/stores/questions.svelte.ts b/apps/questions/apps/web/src/lib/stores/questions.svelte.ts index b1b8466e1..441c1f833 100644 --- a/apps/questions/apps/web/src/lib/stores/questions.svelte.ts +++ b/apps/questions/apps/web/src/lib/stores/questions.svelte.ts @@ -1,9 +1,13 @@ /** * Questions Store - Manages questions state using Svelte 5 runes + * Authenticated users: questions from API + * Demo mode: static sample questions to showcase the app */ import { questionsApi, type QuestionFilters } from '$lib/api/questions'; import type { Question, CreateQuestionDto, UpdateQuestionDto } from '$lib/types'; +import { authStore } from './auth.svelte'; +import { generateDemoQuestions, isDemoQuestion } from '$lib/data/demo-questions'; let questions = $state([]); let loading = $state(false); @@ -28,11 +32,46 @@ export const questionsStore = { return currentFilters; }, + /** + * Load questions + * Demo mode: shows static sample questions + * Authenticated: fetches from API + */ async load(filters?: QuestionFilters) { loading = true; error = null; currentFilters = filters || {}; + // Demo mode: load demo questions + if (!authStore.isAuthenticated) { + let demoQuestions = generateDemoQuestions(); + + // Apply filters + if (filters?.collectionId) { + demoQuestions = demoQuestions.filter( + (q: Question) => q.collectionId === filters.collectionId + ); + } + if (filters?.status) { + demoQuestions = demoQuestions.filter((q: Question) => q.status === filters.status); + } + if (filters?.search) { + const search = filters.search.toLowerCase(); + demoQuestions = demoQuestions.filter( + (q: Question) => + q.title.toLowerCase().includes(search) || + q.description?.toLowerCase().includes(search) || + q.tags?.some((t: string) => t.toLowerCase().includes(search)) + ); + } + + questions = demoQuestions; + total = demoQuestions.length; + loading = false; + return; + } + + // Authenticated: fetch from API try { const response = await questionsApi.getAll(filters); questions = response.data; @@ -46,7 +85,18 @@ export const questionsStore = { } }, + /** + * Create a new question + * Demo mode: returns auth_required error + * Authenticated: creates via API + */ async create(data: CreateQuestionDto): Promise { + // Demo mode: require authentication + if (!authStore.isAuthenticated) { + error = 'Login required to create questions'; + return null; + } + loading = true; error = null; @@ -63,7 +113,18 @@ export const questionsStore = { } }, + /** + * Update a question + * Demo mode: returns auth_required error + * Authenticated: updates via API + */ async update(id: string, data: UpdateQuestionDto): Promise { + // Demo question or not authenticated: require authentication + if (isDemoQuestion(id) || !authStore.isAuthenticated) { + error = 'Login required to update questions'; + return null; + } + error = null; try { @@ -76,7 +137,18 @@ export const questionsStore = { } }, + /** + * Delete a question + * Demo mode: returns auth_required error + * Authenticated: deletes via API + */ async delete(id: string): Promise { + // Demo question or not authenticated: require authentication + if (isDemoQuestion(id) || !authStore.isAuthenticated) { + error = 'Login required to delete questions'; + return false; + } + error = null; try { @@ -90,7 +162,18 @@ export const questionsStore = { } }, + /** + * Update question status + * Demo mode: returns auth_required error + * Authenticated: updates via API + */ async updateStatus(id: string, status: string): Promise { + // Demo question or not authenticated: require authentication + if (isDemoQuestion(id) || !authStore.isAuthenticated) { + error = 'Login required to update question status'; + return null; + } + error = null; try { @@ -107,6 +190,13 @@ export const questionsStore = { return questions.find((q) => q.id === id); }, + /** + * Check if a question is a demo question + */ + isDemoQuestion(id: string): boolean { + return isDemoQuestion(id); + }, + clear() { questions = []; total = 0; diff --git a/apps/questions/apps/web/src/routes/(app)/+layout.svelte b/apps/questions/apps/web/src/routes/(app)/+layout.svelte index 6d01958a3..a2cddd607 100644 --- a/apps/questions/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/questions/apps/web/src/routes/(app)/+layout.svelte @@ -15,6 +15,7 @@ CreatePreview, } from '@manacore/shared-ui'; import { getPillAppItems } from '@manacore/shared-branding'; + import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui'; let { children } = $props(); @@ -40,16 +41,22 @@ // User email for nav let userEmail = $derived(authStore.user?.email || 'Menu'); + // Guest welcome modal state + let showGuestWelcome = $state(false); + onMount(async () => { - if (!authStore.isAuthenticated) { - goto('/login'); - return; + // Set API token if authenticated + if (authStore.isAuthenticated) { + const token = await authStore.getValidToken(); + apiClient.setAccessToken(token); + } else { + // Show guest welcome modal for unauthenticated users + if (shouldShowGuestWelcome('questions')) { + showGuestWelcome = true; + } } - const token = await authStore.getValidToken(); - apiClient.setAccessToken(token); - - // Load initial data + // Load initial data (works in both guest and authenticated mode) await collectionsStore.load(); await questionsStore.load(); @@ -57,14 +64,16 @@ updateMobileState(); // Restore nav mode from localStorage - const savedSidebar = localStorage.getItem('questions-nav-sidebar'); - if (savedSidebar === 'true') { - isSidebarMode = true; - } + if (browser) { + const savedSidebar = localStorage.getItem('questions-nav-sidebar'); + if (savedSidebar === 'true') { + isSidebarMode = true; + } - const savedCollapsed = localStorage.getItem('questions-nav-collapsed'); - if (savedCollapsed === 'true') { - isCollapsed = true; + const savedCollapsed = localStorage.getItem('questions-nav-collapsed'); + if (savedCollapsed === 'true') { + isCollapsed = true; + } } }); @@ -96,6 +105,17 @@ async function handleSearch(query: string): Promise { if (!query.trim()) return []; + // Demo mode: search from store + if (!authStore.isAuthenticated) { + await questionsStore.load({ search: query }); + return questionsStore.questions.slice(0, 10).map((q) => ({ + id: q.id, + title: q.title, + subtitle: q.status || 'pending', + })); + } + + // Authenticated: search via API try { const response = await questionsApi.getAll({ search: query, limit: 10 }); return response.data.map((q) => ({ @@ -126,6 +146,12 @@ async function handleCreate(query: string): Promise { if (!query.trim()) return; + // Demo mode: show login prompt + if (!authStore.isAuthenticated) { + showGuestWelcome = true; + return; + } + const question = await questionsStore.create({ title: query, collectionId: collectionsStore.selectedId || undefined, @@ -176,11 +202,54 @@ { href: '/collections', label: 'Collections', icon: 'folder' }, { href: '/settings', label: 'Settings', icon: 'settings' }, ]); + + // Guest features for welcome modal + const guestFeatures = [ + 'Browse sample research questions', + 'Explore the app interface', + 'See how AI research works', + ];
+ + {#if !authStore.isAuthenticated} +
+
+ + + + + + Demo Mode + + +
+ +
+ {/if} + -
+
{@render children()}
+ + (showGuestWelcome = false)} + onLogin={() => { + showGuestWelcome = false; + goto('/login'); + }} + onRegister={() => { + showGuestWelcome = false; + goto('/register'); + }} + helpHref="/help" + locale="en" + features={guestFeatures} +/> +