'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import {
Video,
Building2,
FolderPlus,
PlayCircle,
Bell,
ChevronRight,
Loader2,
Lock,
UserPlus,
Globe,
Youtube,
Upload,
Mail,
AlertCircle,
Info,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
type Visibility = 'PRIVATE' | 'INVITE' | 'PUBLIC';
const TOTAL_STEPS = 5;
const visibilityOptions: { value: Visibility; label: string; description: string; icon: React.ReactNode }[] = [
{
value: 'PRIVATE',
label: 'Private',
description: 'Only workspace members and project members can access',
icon: ,
},
{
value: 'INVITE',
label: 'Invite Only',
description: 'Share with specific people via email',
icon: ,
},
{
value: 'PUBLIC',
label: 'Public',
description: 'Anyone with the link can view',
icon: ,
},
];
function ToggleButton({
enabled,
onToggle,
label,
description,
}: {
enabled: boolean;
onToggle: () => void;
label: string;
description?: string;
}) {
return (
{label}
{description && (
{description}
)}
);
}
// ─── Step 1: Welcome ───────────────────────────────────────────────────────────
function StepWelcome({ userName, onNext }: { userName: string; onNext: () => void }) {
return (
Welcome to OpenFrame, {userName.split(' ')[0]}!
OpenFrame is your collaborative video review platform. Collect timestamped feedback, manage versions, and streamline approvals — all in one place.
Get Started
);
}
// ─── Step 2: Create Workspace ──────────────────────────────────────────────────
function StepWorkspace({
onNext,
onWorkspaceCreated,
}: {
onNext: () => void;
onWorkspaceCreated: (id: string) => void;
}) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({ name: '', description: '' });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
const res = await fetch('/api/workspaces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Failed to create workspace');
return;
}
onWorkspaceCreated(data.data.id);
onNext();
} catch {
setError('Something went wrong. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
Create your workspace
Workspaces organize your projects and team members.
);
}
// ─── Step 3: Create Project ────────────────────────────────────────────────────
function StepProject({
workspaceId,
onNext,
onProjectCreated,
}: {
workspaceId: string | null;
onNext: () => void;
onProjectCreated: (id: string) => void;
}) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({
name: '',
description: '',
visibility: 'PRIVATE' as Visibility,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!workspaceId) return;
setIsLoading(true);
setError('');
try {
const res = await fetch('/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formData, workspaceId }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Failed to create project');
return;
}
onProjectCreated(data.data.id);
onNext();
} catch {
setError('Something went wrong. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
Create your first project
Projects hold your videos and collected feedback.
{!workspaceId ? (
You skipped workspace creation. Projects require a workspace — you can create both from the dashboard later.
Continue
) : (
Project Name
setFormData({ ...formData, name: e.target.value })}
required
disabled={isLoading}
className="h-11"
/>
Description (optional)
setFormData({ ...formData, description: e.target.value })}
rows={3}
disabled={isLoading}
className="resize-none"
/>
Who can access?
{visibilityOptions.map((option) => (
setFormData({ ...formData, visibility: option.value })}
disabled={isLoading}
className={cn(
'w-full flex items-center gap-4 p-4 rounded-xl border-2 text-left transition-all',
formData.visibility === option.value
? 'border-primary bg-primary/5 ring-1 ring-primary/20'
: 'border-border hover:border-border/80 hover:bg-accent/50'
)}
>
{option.icon}
{option.label}
{option.description}
{formData.visibility === option.value && (
)}
))}
{error && (
)}
{isLoading ? (
<>
Creating...
>
) : (
'Create Project'
)}
Skip this step
)}
);
}
// ─── Step 4: Add First Video (informational) ───────────────────────────────────
function StepVideo({ onNext }: { onNext: () => void }) {
return (
Adding videos
OpenFrame supports two ways to add video content to your projects.
YouTube link
Paste a link to any YouTube video. OpenFrame will pull in the title, thumbnail, and duration automatically — no file upload needed.
Direct upload
Upload video files directly from your device. Files are processed and delivered via CDN for fast, reliable playback worldwide.
Got it, continue
);
}
// ─── Step 5: Notification Preferences ─────────────────────────────────────────
function StepNotifications({ onFinish }: { onFinish: () => Promise }) {
const [isSaving, setIsSaving] = useState(false);
const [emailEnabled, setEmailEnabled] = useState(false);
const [events, setEvents] = useState({
onNewVideo: true,
onNewComment: true,
onNewReply: true,
});
const handleSave = async () => {
setIsSaving(true);
try {
await fetch('/api/settings/notifications', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
emailEnabled,
telegramEnabled: false,
telegramBotToken: null,
telegramChatId: null,
onNewVideo: events.onNewVideo,
onNewVersion: true,
onNewComment: events.onNewComment,
onNewReply: events.onNewReply,
onApprovalEvents: true,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
}),
});
} catch {
// best-effort; don't block finishing
}
await onFinish();
};
return (
Notification preferences
Choose when you want to be notified. Email and Telegram are both supported — you can configure Telegram anytime in Settings.
Channels
setEmailEnabled((v) => !v)}
label="Email notifications"
description="Receive emails to your account address"
/>
Events
setEvents((e) => ({ ...e, onNewVideo: !e.onNewVideo }))}
label="New Video Added"
description="When a new video is added to one of your projects"
/>
setEvents((e) => ({ ...e, onNewComment: !e.onNewComment }))}
label="New Comment"
description="When someone leaves a comment on your videos"
/>
setEvents((e) => ({ ...e, onNewReply: !e.onNewReply }))}
label="New Reply"
description="When someone replies to a comment thread"
/>
{isSaving ? (
<>
Saving...
>
) : (
'Save & finish'
)}
Skip
);
}
// ─── Wizard Shell ──────────────────────────────────────────────────────────────
export function OnboardingWizard({ userName }: { userName: string }) {
const router = useRouter();
const [currentStep, setCurrentStep] = useState(1);
const [createdWorkspaceId, setCreatedWorkspaceId] = useState(null);
const [isCompleting, setIsCompleting] = useState(false);
const goNext = () => setCurrentStep((s) => Math.min(s + 1, TOTAL_STEPS));
const completeOnboarding = async () => {
setIsCompleting(true);
try {
const res = await fetch('/api/onboarding/complete', { method: 'POST' });
if (!res.ok) {
toast.error('Failed to complete setup. Please try again.');
setIsCompleting(false);
return;
}
} catch {
toast.error('Something went wrong. Please try again.');
setIsCompleting(false);
return;
}
router.push('/dashboard');
};
return (
{/* Top bar */}
{/* Step dots */}
{Array.from({ length: TOTAL_STEPS }, (_, i) => i + 1).map((step) => (
))}
{/* Skip button */}
{currentStep > 1 && (
{isCompleting ? : null}
Skip setup
)}
{/* Step content */}
{currentStep === 1 && (
)}
{currentStep === 2 && (
)}
{currentStep === 3 && (
{}} />
)}
{currentStep === 4 && (
)}
{currentStep === 5 && (
)}
{/* Step label */}
Step {currentStep} of {TOTAL_STEPS}
);
}