11 seo pages
This commit is contained in:
@@ -1,171 +1,171 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { appendRedirectParam } from '@/lib/auth-flow';
|
||||
import {
|
||||
getChecklistItems,
|
||||
ONBOARDING_CHECKLIST_DISMISS_KEY,
|
||||
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
|
||||
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
|
||||
} from '@/lib/revops';
|
||||
|
||||
type ChecklistState = {
|
||||
id?: string;
|
||||
signupSourceSelfReported?: string | null;
|
||||
primaryUseCase?: string | null;
|
||||
firstQrCreatedAt?: string | null;
|
||||
firstDynamicQrAt?: string | null;
|
||||
firstScanAt?: string | null;
|
||||
activationAt?: string | null;
|
||||
onboardingCompletedAt?: string | null;
|
||||
};
|
||||
|
||||
type OnboardingChecklistProps = {
|
||||
state: ChecklistState | null;
|
||||
};
|
||||
|
||||
function buildChecklistItems(state: ChecklistState, downloadDone: boolean) {
|
||||
return getChecklistItems(state).map((item) =>
|
||||
item.id === 'download'
|
||||
? {
|
||||
...item,
|
||||
done: downloadDone || Boolean(state.firstScanAt),
|
||||
}
|
||||
: item
|
||||
);
|
||||
}
|
||||
|
||||
export function OnboardingChecklist({ state }: OnboardingChecklistProps) {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [downloadDone, setDownloadDone] = useState(false);
|
||||
const [storageReady, setStorageReady] = useState(false);
|
||||
|
||||
const stepMap: Record<string, number> = {
|
||||
source: 1,
|
||||
'use-case': 2,
|
||||
'first-qr': 7,
|
||||
'first-dynamic': 7,
|
||||
download: 8,
|
||||
scan: 8,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDismissed(localStorage.getItem(ONBOARDING_CHECKLIST_DISMISS_KEY) === '1');
|
||||
setDownloadDone(localStorage.getItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY) === '1');
|
||||
setStorageReady(true);
|
||||
|
||||
const syncDownloadState = () => {
|
||||
setDownloadDone(localStorage.getItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY) === '1');
|
||||
};
|
||||
|
||||
window.addEventListener(ONBOARDING_DOWNLOAD_COMPLETE_EVENT, syncDownloadState);
|
||||
window.addEventListener('storage', syncDownloadState);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(ONBOARDING_DOWNLOAD_COMPLETE_EVENT, syncDownloadState);
|
||||
window.removeEventListener('storage', syncDownloadState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const items = state ? buildChecklistItems(state, downloadDone) : [];
|
||||
const completed = items.filter((item) => item.done).length;
|
||||
const allDone = items.length > 0 && completed === items.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageReady || !state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.onboardingCompletedAt || allDone) {
|
||||
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
|
||||
setDismissed(true);
|
||||
}
|
||||
}, [allDone, state, storageReady]);
|
||||
|
||||
if (!state || !storageReady || dismissed || state.onboardingCompletedAt || allDone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const progress = Math.round((completed / items.length) * 100);
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden rounded-[28px] border border-slate-200 bg-white p-0 shadow-none">
|
||||
<CardContent className="space-y-5 p-5 sm:p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||
{allDone ? 'Onboarding complete' : 'Onboarding progress'}
|
||||
</p>
|
||||
<h3 className="mt-2 text-xl font-semibold tracking-tight text-slate-950">
|
||||
{allDone ? 'Your first setup is complete' : 'Finish the first-run checklist'}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
{allDone
|
||||
? 'You created, downloaded, and tested your first code. You can keep this visible or dismiss it.'
|
||||
: 'Keep this minimal checklist visible until the first QR workflow is fully done.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-semibold text-primary-700">
|
||||
{completed}/{items.length}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Dismiss onboarding checklist"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-2xl border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-900"
|
||||
onClick={() => {
|
||||
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
|
||||
setDismissed(true);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-slate-100">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary-600 transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 sm:grid-cols-6">
|
||||
{items.map((item, index) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={appendRedirectParam('/onboarding', '/dashboard', {
|
||||
step: String(stepMap[item.id] || index + 1),
|
||||
})}
|
||||
className={`rounded-full border px-3 py-2 text-center text-xs font-medium transition-colors ${
|
||||
item.done
|
||||
? 'border-primary-600 bg-primary-600 text-white'
|
||||
: 'border-slate-200 bg-slate-50 text-slate-500 hover:border-primary-200 hover:bg-primary-50 hover:text-primary-700'
|
||||
}`}
|
||||
>
|
||||
{index + 1}. {item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-11 rounded-2xl border-slate-300 px-5 text-sm font-semibold text-slate-700"
|
||||
onClick={() => {
|
||||
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
|
||||
setDismissed(true);
|
||||
}}
|
||||
>
|
||||
{allDone ? 'Hide checklist' : 'Dismiss'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { appendRedirectParam } from '@/lib/auth-flow';
|
||||
import {
|
||||
getChecklistItems,
|
||||
ONBOARDING_CHECKLIST_DISMISS_KEY,
|
||||
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
|
||||
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
|
||||
} from '@/lib/revops';
|
||||
|
||||
type ChecklistState = {
|
||||
id?: string;
|
||||
signupSourceSelfReported?: string | null;
|
||||
primaryUseCase?: string | null;
|
||||
firstQrCreatedAt?: string | null;
|
||||
firstDynamicQrAt?: string | null;
|
||||
firstScanAt?: string | null;
|
||||
activationAt?: string | null;
|
||||
onboardingCompletedAt?: string | null;
|
||||
};
|
||||
|
||||
type OnboardingChecklistProps = {
|
||||
state: ChecklistState | null;
|
||||
};
|
||||
|
||||
function buildChecklistItems(state: ChecklistState, downloadDone: boolean) {
|
||||
return getChecklistItems(state).map((item) =>
|
||||
item.id === 'download'
|
||||
? {
|
||||
...item,
|
||||
done: downloadDone || Boolean(state.firstScanAt),
|
||||
}
|
||||
: item
|
||||
);
|
||||
}
|
||||
|
||||
export function OnboardingChecklist({ state }: OnboardingChecklistProps) {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [downloadDone, setDownloadDone] = useState(false);
|
||||
const [storageReady, setStorageReady] = useState(false);
|
||||
|
||||
const stepMap: Record<string, number> = {
|
||||
source: 1,
|
||||
'use-case': 2,
|
||||
'first-qr': 7,
|
||||
'first-dynamic': 7,
|
||||
download: 8,
|
||||
scan: 8,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDismissed(localStorage.getItem(ONBOARDING_CHECKLIST_DISMISS_KEY) === '1');
|
||||
setDownloadDone(localStorage.getItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY) === '1');
|
||||
setStorageReady(true);
|
||||
|
||||
const syncDownloadState = () => {
|
||||
setDownloadDone(localStorage.getItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY) === '1');
|
||||
};
|
||||
|
||||
window.addEventListener(ONBOARDING_DOWNLOAD_COMPLETE_EVENT, syncDownloadState);
|
||||
window.addEventListener('storage', syncDownloadState);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(ONBOARDING_DOWNLOAD_COMPLETE_EVENT, syncDownloadState);
|
||||
window.removeEventListener('storage', syncDownloadState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const items = state ? buildChecklistItems(state, downloadDone) : [];
|
||||
const completed = items.filter((item) => item.done).length;
|
||||
const allDone = items.length > 0 && completed === items.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (!storageReady || !state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.onboardingCompletedAt || allDone) {
|
||||
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
|
||||
setDismissed(true);
|
||||
}
|
||||
}, [allDone, state, storageReady]);
|
||||
|
||||
if (!state || !storageReady || dismissed || state.onboardingCompletedAt || allDone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const progress = Math.round((completed / items.length) * 100);
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden rounded-[28px] border border-slate-200 bg-white p-0 shadow-none">
|
||||
<CardContent className="space-y-5 p-5 sm:p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||
{allDone ? 'Onboarding complete' : 'Onboarding progress'}
|
||||
</p>
|
||||
<h3 className="mt-2 text-xl font-semibold tracking-tight text-slate-950">
|
||||
{allDone ? 'Your first setup is complete' : 'Finish the first-run checklist'}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
{allDone
|
||||
? 'You created, downloaded, and tested your first code. You can keep this visible or dismiss it.'
|
||||
: 'Keep this minimal checklist visible until the first QR workflow is fully done.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-semibold text-primary-700">
|
||||
{completed}/{items.length}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Dismiss onboarding checklist"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-2xl border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-900"
|
||||
onClick={() => {
|
||||
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
|
||||
setDismissed(true);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-slate-100">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary-600 transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 sm:grid-cols-6">
|
||||
{items.map((item, index) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={appendRedirectParam('/onboarding', '/dashboard', {
|
||||
step: String(stepMap[item.id] || index + 1),
|
||||
})}
|
||||
className={`rounded-full border px-3 py-2 text-center text-xs font-medium transition-colors ${
|
||||
item.done
|
||||
? 'border-primary-600 bg-primary-600 text-white'
|
||||
: 'border-slate-200 bg-slate-50 text-slate-500 hover:border-primary-200 hover:bg-primary-50 hover:text-primary-700'
|
||||
}`}
|
||||
>
|
||||
{index + 1}. {item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-11 rounded-2xl border-slate-300 px-5 text-sm font-semibold text-slate-700"
|
||||
onClick={() => {
|
||||
localStorage.setItem(ONBOARDING_CHECKLIST_DISMISS_KEY, '1');
|
||||
setDismissed(true);
|
||||
}}
|
||||
>
|
||||
{allDone ? 'Hide checklist' : 'Dismiss'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user