diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
index dbee0bc..9235251 100644
--- a/app/(auth)/login/page.tsx
+++ b/app/(auth)/login/page.tsx
@@ -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.');
diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx
index 981e57d..8aad243 100644
--- a/app/(auth)/register/page.tsx
+++ b/app/(auth)/register/page.tsx
@@ -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) => {
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() {
-
+ {canCreateProjects && (
+
+ )}
)}
diff --git a/app/(dashboard)/projects/[projectId]/members/page.tsx b/app/(dashboard)/projects/[projectId]/members/page.tsx
index 1dc69b7..4ec9487 100644
--- a/app/(dashboard)/projects/[projectId]/members/page.tsx
+++ b/app/(dashboard)/projects/[projectId]/members/page.tsx
@@ -1,335 +1,26 @@
'use client';
-import { useState, useEffect, useCallback } from 'react';
-import { useParams, useRouter } from 'next/navigation';
-import Link from 'next/link';
-import {
- ArrowLeft,
- Plus,
- Loader2,
- Crown,
- Shield,
- MessageSquare,
- Trash2,
- UserPlus,
-} from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import { Badge } from '@/components/ui/badge';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-
-interface Member {
- id: string;
- role: 'ADMIN' | 'COMMENTATOR';
- userId: string;
- user: {
- id: string;
- name: string | null;
- email: string | null;
- image: string | null;
- };
-}
-
-interface Owner {
- id: string;
- name: string | null;
- email: string | null;
- image: string | null;
-}
+import { useParams } from 'next/navigation';
+import { MembersManagementPage } from '@/components/members-management-page';
export default function ProjectMembersPage() {
const params = useParams();
- const router = useRouter();
const projectId = params.projectId as string;
- const [members, setMembers] = useState([]);
- const [owner, setOwner] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
- const [inviteEmail, setInviteEmail] = useState('');
- const [inviteRole, setInviteRole] = useState<'ADMIN' | 'COMMENTATOR'>('COMMENTATOR');
- const [isInviting, setIsInviting] = useState(false);
- const [error, setError] = useState('');
- const [success, setSuccess] = useState('');
-
- const fetchMembers = useCallback(async () => {
- try {
- const res = await fetch(`/api/projects/${projectId}/members`);
- if (!res.ok) {
- if (res.status === 403) router.push('/dashboard');
- return;
- }
- const data = await res.json();
- setMembers(data.data.members);
- setOwner(data.data.owner);
- } catch {
- setError('Failed to load members');
- } finally {
- setIsLoading(false);
- }
- }, [projectId, router]);
-
- useEffect(() => {
- fetchMembers();
- }, [fetchMembers]);
-
- const handleInvite = async (e: React.FormEvent) => {
- e.preventDefault();
- setIsInviting(true);
- setError('');
- setSuccess('');
-
- try {
- const res = await fetch(`/api/projects/${projectId}/members`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email: inviteEmail, role: inviteRole }),
- });
-
- const data = await res.json();
-
- if (!res.ok) {
- setError(data.error || 'Failed to invite member');
- return;
- }
-
- if (data.user) {
- setSuccess(`Invited ${data.user.name || data.user.email || inviteEmail} as ${inviteRole.toLowerCase()}`);
- } else {
- setSuccess(data.message || `Invitation sent to ${inviteEmail}`);
- }
- setInviteEmail('');
- fetchMembers();
- } catch {
- setError('Something went wrong');
- } finally {
- setIsInviting(false);
- }
- };
-
- const handleRoleChange = async (memberId: string, newRole: string) => {
- try {
- const res = await fetch(`/api/projects/${projectId}/members/${memberId}`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ role: newRole }),
- });
-
- if (!res.ok) {
- const data = await res.json();
- setError(data.error || 'Failed to update role');
- return;
- }
-
- fetchMembers();
- } catch {
- setError('Failed to update role');
- }
- };
-
- const handleRemove = async (memberId: string) => {
- if (!confirm('Are you sure you want to remove this member?')) return;
-
- try {
- const res = await fetch(`/api/projects/${projectId}/members/${memberId}`, {
- method: 'DELETE',
- });
-
- if (!res.ok) {
- const data = await res.json();
- setError(data.error || 'Failed to remove member');
- return;
- }
-
- fetchMembers();
- } catch {
- setError('Failed to remove member');
- }
- };
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
return (
-
-
-
-
- Back to Project
-
-
-
-
-
Project Members
-
- Manage who has access to this project. Admins can manage settings and delete content.
- Commentators can only view and leave comments.
-
-
-
- {/* Invite Form */}
-
-
-
-
- Invite Member
-
-
- Invite someone by email. They must have an account to be added.
-
-
-
-
-
- {error && (
-
- {error}
-
- )}
- {success && (
-
- {success}
-
- )}
-
-
-
- {/* Members List */}
-
-
- Current Members
-
- Admin — can manage project settings, members, and delete content.{' '}
- Commentator — can view and comment only.
-
-
-
- {/* Owner */}
- {owner && (
-
-
-
-
- {owner.name?.charAt(0).toUpperCase() ?? 'U'}
-
-
-
{owner.name || 'Unnamed'}
-
{owner.email}
-
-
-
-
- Owner
-
-
- )}
-
- {/* Members */}
- {members.map((member) => (
-
-
-
-
- {member.user.name?.charAt(0).toUpperCase() ?? 'U'}
-
-
-
{member.user.name || 'Unnamed'}
-
{member.user.email}
-
-
-
-
-
-
-
- ))}
-
- {members.length === 0 && (
-
- No members yet. Invite someone above.
-
- )}
-
-
-
+
+ Admin - can manage project settings, members, and delete content.{' '}
+ Commentator - can view and comment only.
+ >
+ }
+ forbiddenRedirect="/dashboard"
+ />
);
}
diff --git a/app/(dashboard)/projects/[projectId]/project-content-client.tsx b/app/(dashboard)/projects/[projectId]/project-content-client.tsx
index 3a50a0a..1edf450 100644
--- a/app/(dashboard)/projects/[projectId]/project-content-client.tsx
+++ b/app/(dashboard)/projects/[projectId]/project-content-client.tsx
@@ -143,12 +143,14 @@ export function ProjectContentClient({
>
)}
-
+ {canEdit && (
+
+ )}
{(isOwner || project.members[0]?.role === 'ADMIN') && (
<>