feat: add approvals workflow and unified member invitation management across projects, workspaces, and videos

This commit is contained in:
Yusuf İpek
2026-02-25 16:24:45 +03:00
parent 0ae1becc1b
commit a2b07b3e19
36 changed files with 3122 additions and 982 deletions
+16 -1
View File
@@ -10,6 +10,18 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { signIn } from 'next-auth/react';
function getSafeCallbackUrl(value: string | null): string {
if (!value) return '/dashboard';
try {
const baseOrigin = typeof window === 'undefined' ? 'http://localhost' : window.location.origin;
const parsed = new URL(value, baseOrigin);
if (parsed.origin !== baseOrigin) return '/dashboard';
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
} catch {
return '/dashboard';
}
}
function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
@@ -18,6 +30,7 @@ function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showSuccess, setShowSuccess] = useState(false);
const callbackUrl = getSafeCallbackUrl(searchParams.get('callbackUrl'));
useEffect(() => {
if (searchParams.get('registered') === 'true') {
@@ -35,6 +48,7 @@ function LoginForm() {
email,
password,
redirect: false,
callbackUrl,
});
if (result?.error) {
@@ -42,7 +56,8 @@ function LoginForm() {
return;
}
router.push('/dashboard');
const destination = getSafeCallbackUrl(result?.url || callbackUrl);
router.push(destination);
router.refresh();
} catch {
setError('Something went wrong. Please try again.');
+45 -24
View File
@@ -1,8 +1,8 @@
'use client';
import { useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { Video, Loader2, KeyRound, UserPlus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -11,6 +11,10 @@ import { Label } from '@/components/ui/label';
export default function RegisterPage() {
const router = useRouter();
const searchParams = useSearchParams();
const invitationToken = useMemo(() => searchParams.get('invitationToken') || '', [searchParams]);
const invitedEmail = useMemo(() => searchParams.get('email') || '', [searchParams]);
const isInvitationFlow = invitationToken.length > 0;
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({
@@ -21,6 +25,14 @@ export default function RegisterPage() {
inviteCode: '',
});
useEffect(() => {
if (!invitedEmail) return;
setFormData((prev) => ({
...prev,
email: invitedEmail,
}));
}, [invitedEmail]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({
...prev,
@@ -55,7 +67,8 @@ export default function RegisterPage() {
name: formData.name,
email: formData.email,
password: formData.password,
inviteCode: formData.inviteCode,
inviteCode: formData.inviteCode || undefined,
invitationToken: invitationToken || undefined,
}),
});
@@ -97,28 +110,36 @@ export default function RegisterPage() {
<CardContent>
<form onSubmit={handleRegister} className="space-y-4">
{/* Invite Code - First and prominent */}
<div className="space-y-2">
<Label htmlFor="inviteCode" className="flex items-center gap-2">
<KeyRound className="h-4 w-4 text-amber-500" />
Invite Code
</Label>
<Input
id="inviteCode"
name="inviteCode"
type="text"
placeholder="Enter your invite code"
value={formData.inviteCode}
onChange={handleChange}
required
disabled={isLoading}
className="border-amber-500/30 focus:border-amber-500"
/>
<p className="text-xs text-muted-foreground">
An invite code is required to create an account
</p>
</div>
{isInvitationFlow ? (
<div className="p-3 rounded-md bg-primary/10 text-sm">
You are registering via an invitation link.
</div>
) : (
<>
<div className="space-y-2">
<Label htmlFor="inviteCode" className="flex items-center gap-2">
<KeyRound className="h-4 w-4 text-amber-500" />
Invite Code
</Label>
<Input
id="inviteCode"
name="inviteCode"
type="text"
placeholder="Enter your invite code"
value={formData.inviteCode}
onChange={handleChange}
required
disabled={isLoading}
className="border-amber-500/30 focus:border-amber-500"
/>
<p className="text-xs text-muted-foreground">
An invite code is required to create an account
</p>
</div>
<div className="h-px bg-border my-4" />
<div className="h-px bg-border my-4" />
</>
)}
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>