|
@@ -1,6 +1,6 @@
|
|
|
'use client';
|
|
'use client';
|
|
|
|
|
|
|
|
-import { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
|
|
+import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
import { useAuth } from '@/lib/auth-context';
|
|
import { useAuth } from '@/lib/auth-context';
|
|
|
import { projectsApi, Project } from '@/lib/api';
|
|
import { projectsApi, Project } from '@/lib/api';
|
|
|
import { Modal } from '@/components/ui/modal';
|
|
import { Modal } from '@/components/ui/modal';
|
|
@@ -8,6 +8,9 @@ import { Avatar } from '@/components/ui/avatar';
|
|
|
import { useRouter } from 'next/navigation';
|
|
import { useRouter } from 'next/navigation';
|
|
|
import Link from 'next/link';
|
|
import Link from 'next/link';
|
|
|
|
|
|
|
|
|
|
+type SortKey = 'createdAt' | 'updatedAt' | 'name';
|
|
|
|
|
+type SortDir = 'asc' | 'desc';
|
|
|
|
|
+
|
|
|
export default function ProjectsPage() {
|
|
export default function ProjectsPage() {
|
|
|
const { user, token } = useAuth();
|
|
const { user, token } = useAuth();
|
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
@@ -22,6 +25,11 @@ export default function ProjectsPage() {
|
|
|
const [inviteRole, setInviteRole] = useState('REVIEWER');
|
|
const [inviteRole, setInviteRole] = useState('REVIEWER');
|
|
|
const [inviting, setInviting] = useState(false);
|
|
const [inviting, setInviting] = useState(false);
|
|
|
|
|
|
|
|
|
|
+ // Sort & group state
|
|
|
|
|
+ const [sortKey, setSortKey] = useState<SortKey>('updatedAt');
|
|
|
|
|
+ const [sortDir, setSortDir] = useState<SortDir>('desc');
|
|
|
|
|
+ const [groupByRole, setGroupByRole] = useState(false);
|
|
|
|
|
+
|
|
|
const loadProjects = useCallback(async () => {
|
|
const loadProjects = useCallback(async () => {
|
|
|
if (!token) return;
|
|
if (!token) return;
|
|
|
try {
|
|
try {
|
|
@@ -36,6 +44,27 @@ export default function ProjectsPage() {
|
|
|
|
|
|
|
|
useEffect(() => { loadProjects(); }, [loadProjects]);
|
|
useEffect(() => { loadProjects(); }, [loadProjects]);
|
|
|
|
|
|
|
|
|
|
+ // ── Sort & group ─────────────────────────────────────────────────────────────
|
|
|
|
|
+ const { myProjects, otherProjects } = useMemo(() => {
|
|
|
|
|
+ const sorted = [...projects].sort((a, b) => {
|
|
|
|
|
+ let aVal: string, bVal: string;
|
|
|
|
|
+ if (sortKey === 'name') {
|
|
|
|
|
+ aVal = a.name.toLowerCase();
|
|
|
|
|
+ bVal = b.name.toLowerCase();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ aVal = a[sortKey] ?? '';
|
|
|
|
|
+ bVal = b[sortKey] ?? '';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (aVal < bVal) return sortDir === 'asc' ? -1 : 1;
|
|
|
|
|
+ if (aVal > bVal) return sortDir === 'asc' ? 1 : -1;
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const mine = sorted.filter(p => p.myRole === 'OWNER' || p.ownerId === user?.id);
|
|
|
|
|
+ const other = sorted.filter(p => p.ownerId !== user?.id);
|
|
|
|
|
+ return { myProjects: mine, otherProjects: other };
|
|
|
|
|
+ }, [projects, sortKey, sortDir, user?.id]);
|
|
|
|
|
+
|
|
|
const handleCreate = async (e: React.FormEvent) => {
|
|
const handleCreate = async (e: React.FormEvent) => {
|
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
|
if (!token || !createName.trim()) return;
|
|
if (!token || !createName.trim()) return;
|
|
@@ -73,21 +102,76 @@ export default function ProjectsPage() {
|
|
|
<div className="min-h-screen" style={{ background: 'var(--bg)' }}>
|
|
<div className="min-h-screen" style={{ background: 'var(--bg)' }}>
|
|
|
|
|
|
|
|
{/* ── Header ─────────────────────────────────────────────── */}
|
|
{/* ── Header ─────────────────────────────────────────────── */}
|
|
|
- <header className="sticky top-0 z-10 px-8 py-4 flex items-center justify-between shrink-0"
|
|
|
|
|
|
|
+ <header className="sticky top-0 z-10 px-4 md:px-8 py-3 md:py-4 flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 shrink-0"
|
|
|
style={{
|
|
style={{
|
|
|
background: 'rgba(10,11,20,0.80)',
|
|
background: 'rgba(10,11,20,0.80)',
|
|
|
backdropFilter: 'blur(12px)',
|
|
backdropFilter: 'blur(12px)',
|
|
|
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
|
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
|
|
}}>
|
|
}}>
|
|
|
- <div>
|
|
|
|
|
- <h1 className="text-xl font-semibold" style={{ color: 'var(--text)' }}>Projects</h1>
|
|
|
|
|
- <p className="text-sm mt-0.5" style={{ color: 'var(--text-muted)' }}>
|
|
|
|
|
|
|
+ <div className="flex-1 min-w-0">
|
|
|
|
|
+ <h1 className="text-lg md:text-xl font-semibold" style={{ color: 'var(--text)' }}>Projects</h1>
|
|
|
|
|
+ <p className="text-xs md:text-sm mt-0.5" style={{ color: 'var(--text-muted)' }}>
|
|
|
{loading ? '…' : `${projects.length} project${projects.length !== 1 ? 's' : ''}`}
|
|
{loading ? '…' : `${projects.length} project${projects.length !== 1 ? 's' : ''}`}
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Sort + group controls */}
|
|
|
|
|
+ <div className="flex items-center gap-2 flex-wrap">
|
|
|
|
|
+ {/* Group toggle */}
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setGroupByRole(g => !g)}
|
|
|
|
|
+ className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-lg transition-all"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ background: groupByRole ? 'rgba(99,102,241,0.15)' : 'rgba(255,255,255,0.04)',
|
|
|
|
|
+ color: groupByRole ? '#A5B4FC' : 'var(--text-muted)',
|
|
|
|
|
+ border: groupByRole ? '1px solid rgba(99,102,241,0.3)' : '1px solid rgba(255,255,255,0.06)',
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ Group by role
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Sort select */}
|
|
|
|
|
+ <select
|
|
|
|
|
+ value={sortKey}
|
|
|
|
|
+ onChange={e => setSortKey(e.target.value as SortKey)}
|
|
|
|
|
+ className="text-xs px-2 py-1.5 rounded-lg appearance-none"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ background: 'rgba(255,255,255,0.04)',
|
|
|
|
|
+ color: 'var(--text-muted)',
|
|
|
|
|
+ border: '1px solid rgba(255,255,255,0.06)',
|
|
|
|
|
+ cursor: 'pointer',
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="updatedAt">Recent activity</option>
|
|
|
|
|
+ <option value="createdAt">Date created</option>
|
|
|
|
|
+ <option value="name">Name (A–Z)</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Sort direction */}
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setSortDir(d => d === 'asc' ? 'desc' : 'asc')}
|
|
|
|
|
+ className="p-1.5 rounded-lg"
|
|
|
|
|
+ style={{ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)' }}
|
|
|
|
|
+ title={sortDir === 'asc' ? 'Ascending' : 'Descending'}
|
|
|
|
|
+ >
|
|
|
|
|
+ {sortDir === 'asc' ? (
|
|
|
|
|
+ <svg className="w-3.5 h-3.5" style={{ color: 'var(--text-muted)' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0v-8.25m0 8.25h-8.25" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <svg className="w-3.5 h-3.5" style={{ color: 'var(--text-muted)' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0V6.75m0 6.75h-8.25" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<button
|
|
<button
|
|
|
onClick={() => setShowCreate(true)}
|
|
onClick={() => setShowCreate(true)}
|
|
|
- className="btn btn-primary btn-md flex items-center gap-2"
|
|
|
|
|
|
|
+ className="btn btn-primary btn-md flex items-center gap-2 shrink-0"
|
|
|
>
|
|
>
|
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
@@ -97,16 +181,16 @@ export default function ProjectsPage() {
|
|
|
</header>
|
|
</header>
|
|
|
|
|
|
|
|
{/* ── Content ─────────────────────────────────────────────── */}
|
|
{/* ── Content ─────────────────────────────────────────────── */}
|
|
|
- <div className="px-8 py-6">
|
|
|
|
|
|
|
+ <div className="px-4 md:px-8 py-4 md:py-6">
|
|
|
|
|
|
|
|
{/* Loading skeletons */}
|
|
{/* Loading skeletons */}
|
|
|
{loading && (
|
|
{loading && (
|
|
|
- <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
|
|
|
- {[1,2,3,4,5,6].map(i => (
|
|
|
|
|
- <div key={i} className="card p-5 h-36"
|
|
|
|
|
|
|
+ <div className="space-y-3">
|
|
|
|
|
+ {[1,2,3,4].map(i => (
|
|
|
|
|
+ <div key={i} className="card p-4"
|
|
|
style={{ animation: `fadeIn 0.2s ease-out ${i * 60}ms both` }}>
|
|
style={{ animation: `fadeIn 0.2s ease-out ${i * 60}ms both` }}>
|
|
|
- <div className="skeleton h-4 w-3/4 mb-3 rounded-md" />
|
|
|
|
|
- <div className="skeleton h-3 w-full mb-5 rounded-md" />
|
|
|
|
|
|
|
+ <div className="skeleton h-4 w-3/4 mb-2 rounded-md" />
|
|
|
|
|
+ <div className="skeleton h-3 w-full mb-3 rounded-md" />
|
|
|
<div className="skeleton h-3 w-1/3 rounded-md" />
|
|
<div className="skeleton h-3 w-1/3 rounded-md" />
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
@@ -137,17 +221,62 @@ export default function ProjectsPage() {
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- {/* Project grid */}
|
|
|
|
|
|
|
+ {/* Project list — single column */}
|
|
|
{!loading && projects.length > 0 && (
|
|
{!loading && projects.length > 0 && (
|
|
|
- <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
|
|
|
- {projects.map((project, i) => (
|
|
|
|
|
- <ProjectCard
|
|
|
|
|
- key={project.id}
|
|
|
|
|
- project={project}
|
|
|
|
|
- index={i}
|
|
|
|
|
- onInvite={() => setInviteModal({ projectId: project.id, name: project.name })}
|
|
|
|
|
- />
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ <div className="space-y-3">
|
|
|
|
|
+ {groupByRole ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {myProjects.length > 0 && (
|
|
|
|
|
+ <section>
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-2">
|
|
|
|
|
+ <span className="text-[11px] font-semibold uppercase tracking-wider px-2 py-1 rounded"
|
|
|
|
|
+ style={{ background: 'rgba(99,102,241,0.12)', color: '#818CF8' }}>
|
|
|
|
|
+ My Projects ({myProjects.length})
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="space-y-3">
|
|
|
|
|
+ {myProjects.map((project, i) => (
|
|
|
|
|
+ <ProjectCard
|
|
|
|
|
+ key={project.id}
|
|
|
|
|
+ project={project}
|
|
|
|
|
+ index={i}
|
|
|
|
|
+ onInvite={() => setInviteModal({ projectId: project.id, name: project.name })}
|
|
|
|
|
+ />
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {otherProjects.length > 0 && (
|
|
|
|
|
+ <section className="mt-6">
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-2">
|
|
|
|
|
+ <span className="text-[11px] font-semibold uppercase tracking-wider px-2 py-1 rounded"
|
|
|
|
|
+ style={{ background: 'rgba(255,255,255,0.06)', color: 'var(--text-muted)' }}>
|
|
|
|
|
+ Shared with Me ({otherProjects.length})
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="space-y-3">
|
|
|
|
|
+ {otherProjects.map((project, i) => (
|
|
|
|
|
+ <ProjectCard
|
|
|
|
|
+ key={project.id}
|
|
|
|
|
+ project={project}
|
|
|
|
|
+ index={i}
|
|
|
|
|
+ onInvite={() => setInviteModal({ projectId: project.id, name: project.name })}
|
|
|
|
|
+ />
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ projects.map((project, i) => (
|
|
|
|
|
+ <ProjectCard
|
|
|
|
|
+ key={project.id}
|
|
|
|
|
+ project={project}
|
|
|
|
|
+ index={i}
|
|
|
|
|
+ onInvite={() => setInviteModal({ projectId: project.id, name: project.name })}
|
|
|
|
|
+ />
|
|
|
|
|
+ ))
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
@@ -247,24 +376,28 @@ export default function ProjectsPage() {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function ProjectCard({ project, index, onInvite }: {
|
|
|
|
|
|
|
+function ProjectCard({ project, index, onInvite, showOwner = false }: {
|
|
|
project: Project;
|
|
project: Project;
|
|
|
index: number;
|
|
index: number;
|
|
|
onInvite: () => void;
|
|
onInvite: () => void;
|
|
|
|
|
+ showOwner?: boolean;
|
|
|
}) {
|
|
}) {
|
|
|
const assetCount = project._count?.assets ?? 0;
|
|
const assetCount = project._count?.assets ?? 0;
|
|
|
const memberCount = project.members?.length ?? 0;
|
|
const memberCount = project.members?.length ?? 0;
|
|
|
|
|
|
|
|
|
|
+ // Owner name — find the owner member record
|
|
|
|
|
+ const owner = project.members?.find(m => m.user.id === project.ownerId);
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<Link
|
|
<Link
|
|
|
href={`/projects/${project.id}`}
|
|
href={`/projects/${project.id}`}
|
|
|
- className="card block p-5 group"
|
|
|
|
|
|
|
+ className="card block p-4 group"
|
|
|
style={{ animationDelay: `${index * 50}ms`, animation: `slideUp 0.25s ease-out ${index * 50}ms both` }}
|
|
style={{ animationDelay: `${index * 50}ms`, animation: `slideUp 0.25s ease-out ${index * 50}ms both` }}
|
|
|
>
|
|
>
|
|
|
{/* Header row */}
|
|
{/* Header row */}
|
|
|
- <div className="flex items-start justify-between mb-4">
|
|
|
|
|
|
|
+ <div className="flex items-start justify-between mb-3">
|
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
|
- <div className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 transition-colors"
|
|
|
|
|
|
|
+ <div className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0"
|
|
|
style={{ background: 'rgba(99,102,241,0.12)', border: '1px solid rgba(99,102,241,0.18)' }}>
|
|
style={{ background: 'rgba(99,102,241,0.12)', border: '1px solid rgba(99,102,241,0.18)' }}>
|
|
|
<svg className="w-4 h-4" style={{ color: '#818CF8' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<svg className="w-4 h-4" style={{ color: '#818CF8' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h1.5A1.125 1.125 0 005.625 18.375m-2.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0112 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375m18.75 0c.621 0 1.125.504 1.125 1.125m-18.75 0v1.5c0 .621-.504 1.125-1.125 1.125M12 10.875v-1.5m0 4.5c0 .621.504 1.125 1.125 1.125m0-4.5c0-.621-.504-1.125-1.125-1.125m0-4.5c0 .621.504 1.125 1.125 1.125m0-4.5c0 .621.504 1.125 1.125 1.125M12 10.875c0 .621-.504 1.125-1.125 1.125M12 10.875v1.5m0-4.5c0 .621.504 1.125 1.125 1.125m0-4.5v4.5" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h1.5A1.125 1.125 0 005.625 18.375m-2.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0112 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375m18.75 0c.621 0 1.125.504 1.125 1.125m-18.75 0v1.5c0 .621-.504 1.125-1.125 1.125M12 10.875v-1.5m0 4.5c0 .621.504 1.125 1.125 1.125m0-4.5c0-.621-.504-1.125-1.125-1.125m0-4.5c0 .621.504 1.125 1.125 1.125m0-4.5c0 .621.504 1.125 1.125 1.125M12 10.875c0 .621-.504 1.125-1.125 1.125M12 10.875v1.5m0-4.5c0 .621.504 1.125 1.125 1.125m0-4.5v4.5" />
|
|
@@ -290,6 +423,17 @@ function ProjectCard({ project, index, onInvite }: {
|
|
|
</span>
|
|
</span>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div className="flex items-center gap-2 text-[11px] mt-0.5" style={{ color: 'var(--text-muted)' }}>
|
|
|
|
|
+ {showOwner && owner && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <span>{owner.user.name}</span>
|
|
|
|
|
+ <span className="text-[10px]" style={{ color: 'var(--text-subtle)' }}>·</span>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <span>{assetCount} video{assetCount !== 1 ? 's' : ''}</span>
|
|
|
|
|
+ <span className="text-[10px]" style={{ color: 'var(--text-subtle)' }}>·</span>
|
|
|
|
|
+ <span>{memberCount} member{memberCount !== 1 ? 's' : ''}</span>
|
|
|
|
|
+ </div>
|
|
|
{project.description && (
|
|
{project.description && (
|
|
|
<p className="text-xs mt-0.5 truncate" style={{ color: 'var(--text-muted)' }}>
|
|
<p className="text-xs mt-0.5 truncate" style={{ color: 'var(--text-muted)' }}>
|
|
|
{project.description}
|
|
{project.description}
|
|
@@ -299,7 +443,7 @@ function ProjectCard({ project, index, onInvite }: {
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Arrow indicator */}
|
|
{/* Arrow indicator */}
|
|
|
- <div className="w-7 h-7 rounded-lg flex items-center justify-center shrink-0 ml-2 transition-all"
|
|
|
|
|
|
|
+ <div className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ml-2 transition-all min-w-[32px] min-h-[32px]"
|
|
|
style={{ background: 'rgba(255,255,255,0.04)' }}>
|
|
style={{ background: 'rgba(255,255,255,0.04)' }}>
|
|
|
<svg className="w-3.5 h-3.5 transition-transform group-hover:translate-x-0.5"
|
|
<svg className="w-3.5 h-3.5 transition-transform group-hover:translate-x-0.5"
|
|
|
style={{ color: 'var(--text-subtle)' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
style={{ color: 'var(--text-subtle)' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
@@ -308,25 +452,8 @@ function ProjectCard({ project, index, onInvite }: {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* Stats */}
|
|
|
|
|
- <div className="flex items-center gap-3 text-xs mb-4" style={{ color: 'var(--text-muted)' }}>
|
|
|
|
|
- <div className="flex items-center gap-1.5">
|
|
|
|
|
- <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
|
|
|
- <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
|
|
|
|
|
- </svg>
|
|
|
|
|
- <span>{assetCount} video{assetCount !== 1 ? 's' : ''}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="w-1 h-1 rounded-full" style={{ background: 'rgba(255,255,255,0.12)' }} />
|
|
|
|
|
- <div className="flex items-center gap-1.5">
|
|
|
|
|
- <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
|
|
|
- <path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
|
|
|
|
- </svg>
|
|
|
|
|
- <span>{memberCount} member{memberCount !== 1 ? 's' : ''}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
{/* Footer */}
|
|
{/* Footer */}
|
|
|
- <div className="flex items-center justify-between pt-3.5" style={{ borderTop: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
|
|
|
|
+ <div className="flex items-center justify-between pt-3" style={{ borderTop: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
{/* Member avatars */}
|
|
{/* Member avatars */}
|
|
|
<div className="flex -space-x-1.5">
|
|
<div className="flex -space-x-1.5">
|
|
|
{(project.members ?? []).slice(0, 5).map(m => (
|
|
{(project.members ?? []).slice(0, 5).map(m => (
|
|
@@ -351,7 +478,7 @@ function ProjectCard({ project, index, onInvite }: {
|
|
|
{/* Invite button */}
|
|
{/* Invite button */}
|
|
|
<button
|
|
<button
|
|
|
onClick={e => { e.preventDefault(); onInvite(); }}
|
|
onClick={e => { e.preventDefault(); onInvite(); }}
|
|
|
- className="text-xs font-medium px-2.5 py-1 rounded-md transition-all"
|
|
|
|
|
|
|
+ className="text-xs font-medium px-3 py-1.5 rounded-lg transition-all min-h-[36px] min-w-[36px]"
|
|
|
style={{ color: '#818CF8', background: 'rgba(99,102,241,0.08)' }}
|
|
style={{ color: '#818CF8', background: 'rgba(99,102,241,0.08)' }}
|
|
|
>
|
|
>
|
|
|
+ Invite
|
|
+ Invite
|