add platform demo fam
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
famStore.initialized ? famStore.rewards.filter((r: any) => r.status === 'requested').length : 0
|
||||
);
|
||||
|
||||
let { famName = '', session = null, isParent = false, role = 'child' } = $props();
|
||||
let { famName = '', session = null, isParent = false, role = 'child', isDemo = false } = $props();
|
||||
|
||||
let famSlug = $derived(page.data.famSlug ?? page.params.fam);
|
||||
let memberName = $derived(session?.memberName || page.params.username || '');
|
||||
@@ -43,10 +43,10 @@
|
||||
);
|
||||
|
||||
let footerItems = $derived([
|
||||
...(memberName
|
||||
...(memberName && !isDemo
|
||||
? [{ href: `/${famSlug}/${memberName}/preferences`, label: 'Preferences', icon: prefsIcon }]
|
||||
: []),
|
||||
...(isParent && memberName
|
||||
...(isParent && memberName && !isDemo
|
||||
? [{ href: `/${famSlug}/${memberName}/settings`, label: 'Settings', icon: settingsIcon }]
|
||||
: []),
|
||||
{
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
import { Resend } from 'resend';
|
||||
import { RESEND_API } from '$app/env/private';
|
||||
import { getAccessEmailTemplate } from './email-templates';
|
||||
import { getPlatformFlags } from '$lib/server/platform';
|
||||
|
||||
const resend = new Resend(String(RESEND_API));
|
||||
|
||||
let demoCache: { value: boolean; at: number } | null = null;
|
||||
const DEMO_TTL_MS = 10_000;
|
||||
|
||||
async function isDemoMode(): Promise<boolean> {
|
||||
if (demoCache && Date.now() - demoCache.at < DEMO_TTL_MS) return demoCache.value;
|
||||
try {
|
||||
const flags = await getPlatformFlags();
|
||||
demoCache = { value: flags.demo === true, at: Date.now() };
|
||||
return flags.demo === true;
|
||||
} catch {
|
||||
demoCache = { value: false, at: Date.now() };
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendPasswordResetEmail(opts: {
|
||||
to: string;
|
||||
resetLink: string;
|
||||
famName: string;
|
||||
}) {
|
||||
if (await isDemoMode()) return;
|
||||
const { to, resetLink, famName } = opts;
|
||||
|
||||
await resend.emails.send({
|
||||
@@ -30,6 +47,7 @@ export async function sendParentInviteEmail(opts: {
|
||||
famName: string;
|
||||
otp: string;
|
||||
}) {
|
||||
if (await isDemoMode()) return;
|
||||
const { to, inviteLink, famName, otp } = opts;
|
||||
|
||||
await resend.emails.send({
|
||||
@@ -52,6 +70,7 @@ export async function sendAccessUnlockedEmail(opts: {
|
||||
codeValue: string;
|
||||
dashboardUrl: string;
|
||||
}) {
|
||||
if (await isDemoMode()) return;
|
||||
const { to, famName, codeName, codeValue, dashboardUrl } = opts;
|
||||
|
||||
const template = getAccessEmailTemplate(codeValue);
|
||||
@@ -70,6 +89,7 @@ export async function sendWelcomeEmail(opts: {
|
||||
famName: string;
|
||||
dashboardUrl: string;
|
||||
}) {
|
||||
if (await isDemoMode()) return;
|
||||
const { to, famName, dashboardUrl } = opts;
|
||||
|
||||
await resend.emails.send({
|
||||
@@ -112,6 +132,7 @@ export async function sendMentionEmail(opts: {
|
||||
message: string;
|
||||
dashboardUrl: string;
|
||||
}) {
|
||||
if (await isDemoMode()) return;
|
||||
const { to, parentName, authorName, message, dashboardUrl } = opts;
|
||||
|
||||
await resend.emails.send({
|
||||
@@ -138,6 +159,7 @@ export async function sendClaimRequestEmail(opts: {
|
||||
rewardValue: string;
|
||||
dashboardUrl: string;
|
||||
}) {
|
||||
if (await isDemoMode()) return;
|
||||
const { to, parentName, childName, rewardName, rewardValue, dashboardUrl } = opts;
|
||||
|
||||
await resend.emails.send({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SCHEMA_PLAN } from '@shared/pb/schema';
|
||||
import { PB_ENDPOINT } from '$lib/server/pocketbase';
|
||||
import { PB_EMAIL, PB_PASSWORD, MEMBER_SECRET } from '$app/env/private';
|
||||
import { getPlatformFlags } from '$lib/server/platform';
|
||||
|
||||
let token: string | null = null;
|
||||
let tokenExpiry = 0;
|
||||
@@ -20,6 +21,11 @@ async function auth(): Promise<string> {
|
||||
return token!;
|
||||
}
|
||||
|
||||
async function isDemo(): Promise<boolean> {
|
||||
const flags = await getPlatformFlags();
|
||||
return flags.demo === true;
|
||||
}
|
||||
|
||||
async function getCollection(name: string): Promise<any | null> {
|
||||
const t = await auth();
|
||||
const res = await fetch(`${PB_ENDPOINT}/api/collections?filter=name='${name}'`, {
|
||||
@@ -444,8 +450,10 @@ export async function migrate(): Promise<void> {
|
||||
await ensureAccessCodes();
|
||||
await ensurePlatform();
|
||||
await ensureDefaultSeasons();
|
||||
if (await isDemo()) {
|
||||
await seedDemoFamily();
|
||||
await seedDemoCompletions();
|
||||
}
|
||||
console.log('[migrate] Done');
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPbClient } from '$lib/server/pocketbase';
|
||||
import { createServices, type ChatActor } from '$lib/server/services';
|
||||
import { ensureFamAccess } from '$lib/server/access';
|
||||
import { seedDemoCompletions } from '$lib/server/migrate';
|
||||
import { getPlatformFlags } from '$lib/server/platform';
|
||||
|
||||
async function paydayCheck(famId: string, pbToken: string) {
|
||||
try {
|
||||
@@ -47,8 +48,8 @@ async function resolveChatIdentity(
|
||||
}
|
||||
|
||||
export async function load(event) {
|
||||
// Demo family: rotate 3-week completion data on every visit (idempotent).
|
||||
if (event.params.fam === 'showboaters') {
|
||||
// Demo family: rotate 3-week completion data on every visit when demo mode is on (idempotent).
|
||||
if (event.params.fam === 'showboaters' && (await getPlatformFlags()).demo) {
|
||||
seedDemoCompletions().catch(() => {});
|
||||
}
|
||||
|
||||
@@ -94,6 +95,8 @@ export async function load(event) {
|
||||
}
|
||||
}
|
||||
|
||||
const demoMode = (await getPlatformFlags()).demo;
|
||||
|
||||
return {
|
||||
// Canonical fam slug — from the URL param ([fam] routes). Client code
|
||||
// reads page.data.famSlug; never copy it into local $state.
|
||||
@@ -117,6 +120,7 @@ export async function load(event) {
|
||||
chat,
|
||||
pbToken,
|
||||
fam,
|
||||
famAccess
|
||||
famAccess,
|
||||
demoMode
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
let session = $state<Session | null>(data.session);
|
||||
let isParent = $state(data.isParent);
|
||||
let role = $state(data.role);
|
||||
let isDemo = $state(data.demoMode);
|
||||
let famName = $derived(
|
||||
famStore.initialized ? (famStore.fam as any)?.name || page.params.fam : page.params.fam
|
||||
);
|
||||
@@ -198,7 +199,7 @@
|
||||
class="app-shell {bgPattern ? `pattern-${bgPattern}` : ''}"
|
||||
style={`${bgSizeVw ? `--s:calc(${bgSizeVw} * 1vw);` : ''}--c1:${bgShades.c1};--c2:${bgShades.c2};--c3:${bgShades.c3};--c4:${bgShades.c4}`}
|
||||
>
|
||||
<Sidebar {famName} session={data.session} {isParent} {role} />
|
||||
<Sidebar {famName} session={data.session} {isParent} {role} {isDemo} />
|
||||
<TopNav
|
||||
role={data.session ? role : ''}
|
||||
seasons={famStore.seasons.filter((s) => s.active !== false)}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { pbUser, createPbClient } from '$lib/server/pocketbase';
|
||||
import { createServices } from '$lib/server/services';
|
||||
import { getPlatformFlags } from '$lib/server/platform';
|
||||
import { pbAdmin } from '$lib/server/pocketbase';
|
||||
|
||||
export async function load(event) {
|
||||
const session = event.locals.user;
|
||||
const demoMode = (await getPlatformFlags()).demo;
|
||||
const isDemoParent = session?.username === 'demo';
|
||||
const famSlug = event.params.fam;
|
||||
const username = event.params.username;
|
||||
|
||||
// Demo parent visiting a child's route → load child kanban view.
|
||||
const demoParentVisitingChild = demoMode && isDemoParent && session?.username !== username;
|
||||
|
||||
// Parent (role=parent, session auth) → admin overview.
|
||||
// Branch on role, NOT presence: a child who OTP-logged-in also has a
|
||||
// `users` session (locals.user) and must not hit the parent path.
|
||||
if (session && session.role === 'parent') {
|
||||
const famId = session.famId;
|
||||
const famSlug = event.params.fam;
|
||||
const username = event.params.username;
|
||||
if (session && session.role === 'parent' && !demoParentVisitingChild) {
|
||||
if (!(demoMode && isDemoParent)) {
|
||||
if (session.name && session.username && session.username !== username) {
|
||||
throw redirect(303, `/${famSlug}/${session.username}`);
|
||||
}
|
||||
}
|
||||
const s = createServices(pbUser(event), session);
|
||||
const [
|
||||
members,
|
||||
@@ -27,15 +35,15 @@ export async function load(event) {
|
||||
completions,
|
||||
settings
|
||||
] = await Promise.all([
|
||||
s.crud.list('members', famId),
|
||||
s.crud.list('chore-templates', famId),
|
||||
s.crud.list('assigned-chores', famId),
|
||||
s.fam.weeklySummary(famId),
|
||||
s.fam.get(famId),
|
||||
s.crud.list('rewards', famId),
|
||||
s.crud.list('bonus-configs', famId),
|
||||
s.crud.list('completions', famId),
|
||||
s.settings.get(famId)
|
||||
s.crud.list('members', session.famId),
|
||||
s.crud.list('chore-templates', session.famId),
|
||||
s.crud.list('assigned-chores', session.famId),
|
||||
s.fam.weeklySummary(session.famId),
|
||||
s.fam.get(session.famId),
|
||||
s.crud.list('rewards', session.famId),
|
||||
s.crud.list('bonus-configs', session.famId),
|
||||
s.crud.list('completions', session.famId),
|
||||
s.settings.get(session.famId)
|
||||
]);
|
||||
return {
|
||||
role: 'parent',
|
||||
@@ -52,18 +60,23 @@ export async function load(event) {
|
||||
};
|
||||
}
|
||||
|
||||
// Child (users auth, role=child) → kanban
|
||||
const famSlug = event.params.fam;
|
||||
const username = event.params.username;
|
||||
// Child (users auth, role=child) → kanban.
|
||||
// Also demo parent visiting a child's route.
|
||||
|
||||
// A child should always land on their own kanban route.
|
||||
if (!(demoMode && isDemoParent)) {
|
||||
if (session?.username && session.username !== username) {
|
||||
throw redirect(303, `/${famSlug}/${session.username}`);
|
||||
}
|
||||
}
|
||||
|
||||
const pbToken = event.cookies.get('pb_token') || '';
|
||||
const famId = session?.famId || '';
|
||||
const childId = session?.id || '';
|
||||
// When a demo parent visits a child's route, session.id is the parent's PB
|
||||
// record. Look up the actual child user by username so chores load correctly.
|
||||
const childId = demoParentVisitingChild
|
||||
? (await pbAdmin.getList('users', `famId='${famId}' && username='${famSlug}:${username}'`))?.[0]?.id || ''
|
||||
: session?.id || '';
|
||||
|
||||
const empty = {
|
||||
role: 'child' as const,
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { servicesFor } from '$lib/server/servicesFor';
|
||||
import { getPlatformFlags } from '$lib/server/platform';
|
||||
|
||||
async function isDemoMode(): Promise<boolean> {
|
||||
return (await getPlatformFlags()).demo;
|
||||
}
|
||||
|
||||
export async function load(event) {
|
||||
if (await isDemoMode()) {
|
||||
return { verified: false, role: '', token: '', memberId: '', famId: '', memberName: '', memberColor: '', pattern: '', themeSize: '', themeOpacity: '', email: '', demoMode: true };
|
||||
}
|
||||
|
||||
const session = event.locals.user;
|
||||
const famSlug = event.params.fam;
|
||||
const username = event.params.username;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { issueAccess, createChild, inviteParent } from '$lib/server/member-otp';
|
||||
import { sendParentInviteEmail } from '$lib/server/email';
|
||||
import { slugify, handle, famUsername } from '@shared/slugify';
|
||||
import { applyAccessCode } from '$lib/server/access';
|
||||
import { getPlatformFlags } from '$lib/server/platform';
|
||||
import {
|
||||
createBillingPortalSession,
|
||||
cancelSubscriptionAtPeriodEnd,
|
||||
@@ -18,7 +19,15 @@ function famIdOf(event: RequestEvent): string {
|
||||
return event.locals.user.famId;
|
||||
}
|
||||
|
||||
// In demo mode, settings is disabled for all users.
|
||||
async function isDemoMode(): Promise<boolean> {
|
||||
return (await getPlatformFlags()).demo;
|
||||
}
|
||||
|
||||
export async function load(event: RequestEvent) {
|
||||
if (await isDemoMode()) {
|
||||
return { members: [], fam: null, seasons: [], accessCode: null, subStatus: null, demoMode: true };
|
||||
}
|
||||
const famId = famIdOf(event);
|
||||
const pb = pbUser(event);
|
||||
const [members, fam, seasons] = await Promise.all([
|
||||
|
||||
@@ -89,12 +89,15 @@ export const load: PageServerLoad = async (event) => {
|
||||
pbAdmin.getList('bonus_templates', 'global = true')
|
||||
]);
|
||||
|
||||
const platformFlags = await getPlatformFlags();
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
fams: famsWithStats,
|
||||
codes: codeList,
|
||||
choreTemplates: choreTemplates as any[],
|
||||
bonusTemplates: bonusTemplates as any[],
|
||||
platformFlags,
|
||||
totalFams: fams.length,
|
||||
totalMembers: famsWithStats.reduce((s, f) => s + f.memberCount, 0),
|
||||
totalRewards: famsWithStats.reduce((s, f) => s + f.totalRewards, 0),
|
||||
@@ -121,6 +124,7 @@ export const load: PageServerLoad = async (event) => {
|
||||
codeCount: 0,
|
||||
choreTemplates: [],
|
||||
bonusTemplates: [],
|
||||
platformFlags: {},
|
||||
loadError: e instanceof Error ? e.message : 'Failed to load platform data'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,6 +188,20 @@
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flag-row">
|
||||
<span class="flag-name">demo</span>
|
||||
<span class="flag-desc">Showboaters demo family with sample data for prospectus viewers</span>
|
||||
<form method="POST" action="?/togglePlatformFlag" use:enhance>
|
||||
<input type="hidden" name="flag" value="demo" />
|
||||
<button
|
||||
type="submit"
|
||||
class="toggle-btn"
|
||||
class:active={page.data.platformFlags?.demo}
|
||||
>
|
||||
{page.data.platformFlags?.demo ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flag-row">
|
||||
<span class="flag-name">rewardsTargets</span>
|
||||
<span class="flag-desc">Multi-target reward rules (Competition / Collaboration)</span>
|
||||
|
||||
Reference in New Issue
Block a user