Skip to content

Commit 154b28e

Browse files
feat: unified create-school flow with cross-subdomain auth
- Combined signup + school creation into single UI flow at /create-school - Fixed stale cookie bug by using client-side create_school RPC instead of server action - Added cross-subdomain cookie sharing (domain=.lvh.me / .lmsplatform.com) - Added password visibility toggles with Eye/EyeOff icons - Protected proxy.ts against invalid JWT errors with try-catch - Added theme/appearance system with presets and onboarding step Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 29d60a3 commit 154b28e

29 files changed

Lines changed: 2855 additions & 213 deletions

app/[locale]/create-school/page.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import { createClient } from '@/lib/supabase/server'
2-
import { redirect } from 'next/navigation'
3-
import { CreateSchoolForm } from '@/components/tenant/create-school-form'
2+
import { CreateSchoolFlow } from '@/components/tenant/create-school-flow'
43

54
export default async function CreateSchoolPage() {
65
const supabase = await createClient()
7-
const { data: { user } } = await supabase.auth.getUser()
8-
9-
if (!user) {
10-
redirect('/auth/login?redirectTo=/create-school')
11-
}
6+
const { data: { user }, error } = await supabase.auth.getUser()
7+
// If JWT is invalid/expired, treat as unauthenticated
8+
const validUser = error ? null : user
129

1310
return (
1411
<div className="min-h-screen bg-[#0A0A0A] flex items-center justify-center p-4">
1512
<div className="w-full max-w-lg">
16-
<div className="text-center mb-8">
17-
<h1 className="text-3xl font-bold text-white">Create Your School</h1>
18-
<p className="text-zinc-400 mt-2">
19-
Set up your own learning platform in seconds
20-
</p>
21-
</div>
22-
<CreateSchoolForm userId={user.id} />
13+
<CreateSchoolFlow
14+
user={validUser ? { id: validUser.id, email: validUser.email || '' } : null}
15+
/>
2316
</div>
2417
</div>
2518
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { getUserRole } from '@/lib/supabase/get-user-role'
2+
import { redirect } from 'next/navigation'
3+
import { getTranslations } from 'next-intl/server'
4+
import { AdminBreadcrumb } from '@/components/admin/admin-breadcrumb'
5+
import { getActivePreset } from '@/app/actions/admin/theme'
6+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7+
import { ThemePresetSelector } from '@/components/admin/theme-preset-selector'
8+
import { ThemePreview } from '@/components/admin/theme-preview'
9+
10+
export default async function AppearancePage() {
11+
const role = await getUserRole()
12+
if (role !== 'admin') {
13+
redirect('/dashboard/admin')
14+
}
15+
16+
const t = await getTranslations('dashboard.admin.appearance')
17+
const tBreadcrumbs = await getTranslations('dashboard.admin.breadcrumbs')
18+
19+
const activePreset = await getActivePreset()
20+
21+
return (
22+
<div className="min-h-screen bg-background">
23+
<header className="border-b bg-card">
24+
<div className="mx-auto max-w-7xl px-4 py-5 sm:px-6 lg:px-8">
25+
<div className="mb-4">
26+
<AdminBreadcrumb
27+
items={[
28+
{ label: tBreadcrumbs('admin'), href: '/dashboard/admin' },
29+
{ label: tBreadcrumbs('settings'), href: '/dashboard/admin/settings' },
30+
{ label: tBreadcrumbs('appearance') },
31+
]}
32+
/>
33+
</div>
34+
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
35+
<p className="mt-0.5 text-sm text-muted-foreground">
36+
{t('description')}
37+
</p>
38+
</div>
39+
</header>
40+
41+
<main className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
42+
<div className="grid gap-6 lg:grid-cols-[1fr,400px]">
43+
{/* Left: Preview */}
44+
<div className="order-2 lg:order-1">
45+
<ThemePreview />
46+
</div>
47+
48+
{/* Right: Theme controls */}
49+
<div className="order-1 lg:order-2 space-y-6">
50+
<Card>
51+
<CardHeader>
52+
<CardTitle>{t('theme.title')}</CardTitle>
53+
<CardDescription>
54+
{t('theme.description')}
55+
</CardDescription>
56+
</CardHeader>
57+
<CardContent>
58+
<ThemePresetSelector activePreset={activePreset} />
59+
</CardContent>
60+
</Card>
61+
</div>
62+
</div>
63+
</main>
64+
</div>
65+
)
66+
}

app/[locale]/dashboard/admin/settings/page.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import PaymentSettingsForm from '@/components/admin/payment-settings-form'
1212
import EnrollmentSettingsForm from '@/components/admin/enrollment-settings-form'
1313
import BrandingSettingsForm from '@/components/admin/branding-settings-form'
1414
import { ReferralLinkCard } from '@/components/admin/referral-link-card'
15+
import Link from 'next/link'
16+
import { IconPalette } from '@tabler/icons-react'
1517

1618
export default async function SettingsPage() {
1719
const t = await getTranslations('dashboard.admin.settings')
1820
const tBreadcrumbs = await getTranslations('dashboard.admin.breadcrumbs')
21+
const tAppearance = await getTranslations('dashboard.admin.appearance')
1922
// Verify admin role
2023
const role = await getUserRole()
2124
if (role !== 'admin') {
@@ -71,6 +74,7 @@ export default async function SettingsPage() {
7174
<TabsList className="flex w-full overflow-x-auto lg:w-auto">
7275
<TabsTrigger value="general">{t('tabs.general')}</TabsTrigger>
7376
<TabsTrigger value="branding">{t('tabs.branding')}</TabsTrigger>
77+
<TabsTrigger value="appearance">{t('tabs.appearance')}</TabsTrigger>
7478
<TabsTrigger value="email">{t('tabs.email')}</TabsTrigger>
7579
<TabsTrigger value="payment">{t('tabs.payment')}</TabsTrigger>
7680
<TabsTrigger value="enrollment">{t('tabs.enrollment')}</TabsTrigger>
@@ -106,6 +110,35 @@ export default async function SettingsPage() {
106110
</Card>
107111
</TabsContent>
108112

113+
{/* Appearance — links to dedicated page */}
114+
<TabsContent value="appearance">
115+
<Card>
116+
<CardHeader>
117+
<CardTitle>{tAppearance('title')}</CardTitle>
118+
<CardDescription>
119+
{tAppearance('description')}
120+
</CardDescription>
121+
</CardHeader>
122+
<CardContent>
123+
<Link
124+
href="/dashboard/admin/appearance"
125+
className="flex items-center gap-3 rounded-lg border border-border p-4 transition-colors hover:border-primary/40 hover:bg-primary/5"
126+
>
127+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10">
128+
<IconPalette className="h-[18px] w-[18px] text-primary" strokeWidth={1.75} />
129+
</div>
130+
<div className="flex-1">
131+
<p className="text-sm font-medium">{tAppearance('settingsLink.title')}</p>
132+
<p className="text-xs text-muted-foreground">
133+
{tAppearance('settingsLink.description')}
134+
</p>
135+
</div>
136+
<span className="text-sm text-muted-foreground">&rarr;</span>
137+
</Link>
138+
</CardContent>
139+
</Card>
140+
</TabsContent>
141+
109142
{/* Email Settings */}
110143
<TabsContent value="email">
111144
<Card>

app/[locale]/layout.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ThemeProvider } from "@/components/theme-provider";
66
import { TenantProvider } from "@/components/tenant/tenant-provider"
77
import { TenantCssVars } from "@/components/tenant/tenant-css-vars";
88
import { getCurrentTenant } from "@/lib/supabase/tenant";
9+
import { createAdminClient } from "@/lib/supabase/admin";
910
import { NextIntlClientProvider } from 'next-intl';
1011
import { getMessages, setRequestLocale } from 'next-intl/server';
1112
import { notFound } from 'next/navigation';
@@ -52,16 +53,16 @@ export default async function RootLayout({
5253
const messages = await getMessages();
5354
const tenant = await getCurrentTenant();
5455

55-
// Load tenant settings for branding overrides
56+
// Load tenant settings for branding overrides (use admin client to bypass RLS
57+
// since these are public tenant configuration, not user-specific data)
5658
let tenantSettings: Record<string, any> = {};
5759
if (tenant) {
58-
const { createClient: createSC } = await import('@/lib/supabase/server');
59-
const sb = await createSC();
60+
const sb = createAdminClient();
6061
const { data: settings } = await sb
6162
.from('tenant_settings')
6263
.select('setting_key, setting_value')
6364
.eq('tenant_id', tenant.id)
64-
.in('setting_key', ['site_name', 'logo_url', 'primary_color', 'secondary_color', 'favicon_url']);
65+
.in('setting_key', ['site_name', 'logo_url', 'primary_color', 'secondary_color', 'favicon_url', 'theme_preset']);
6566
if (settings) {
6667
tenantSettings = settings.reduce((acc: Record<string, any>, s) => {
6768
acc[s.setting_key] = s.setting_value;
@@ -79,6 +80,7 @@ export default async function RootLayout({
7980
secondary_color: tenantSettings.secondary_color?.value || tenant.secondary_color,
8081
plan: tenant.plan,
8182
settings: tenantSettings,
83+
theme_preset: tenantSettings.theme_preset ?? null,
8284
} : null;
8385

8486
return (

0 commit comments

Comments
 (0)