Documentation Index
Fetch the complete documentation index at: https://mintlify.com/pingdotgg/lawn/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Projects API manages project structures within teams. Projects organize videos and inherit permissions from their parent team.
All project functions are defined in convex/projects.ts.
Functions
create
Creates a new project within a team.
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function CreateProjectButton({ teamId }) {
const createProject = useMutation(api.projects.create);
const handleCreate = async () => {
const projectId = await createProject({
teamId,
name: 'New Project',
description: 'Project description',
});
console.log('Created project:', projectId);
};
return <button onClick={handleCreate}>Create Project</button>;
}
The team to create the project in
Project description (optional)
The ID of the created project
Permissions: Requires member role in the team. Team must have an active subscription.
Implementation: convex/projects.ts:6
list
Lists all projects in a team with video counts.
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function ProjectsList({ teamId }) {
const projects = useQuery(api.projects.list, { teamId });
return (
<div>
{projects?.map(project => (
<div key={project._id}>
<h3>{project.name}</h3>
<p>{project.description}</p>
<p>{project.videoCount} videos</p>
</div>
))}
</div>
);
}
The team to list projects from
Array of project objects with video countsNumber of videos in the project
Permissions: Requires team membership.
Implementation: convex/projects.ts:24
listUploadTargets
Lists all projects the user can upload to, optionally filtered by team slug.
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function UploadDestinationPicker({ teamSlug }) {
const targets = useQuery(api.projects.listUploadTargets, { teamSlug });
const [selected, setSelected] = useState(null);
return (
<select value={selected} onChange={(e) => setSelected(e.target.value)}>
<option value="">Select destination...</option>
{targets?.map(target => (
<option key={target.projectId} value={target.projectId}>
{target.teamName} / {target.projectName}
</option>
))}
</select>
);
}
Filter by team slug (optional)
Array of upload target objects (excludes projects where user has viewer role)role
'member' | 'admin' | 'owner'
User’s role (viewers excluded)
Permissions: Requires authentication. Only returns projects where user has upload permissions (member or higher).
Implementation: convex/projects.ts:52
get
Retrieves a single project by ID.
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function ProjectDetail({ projectId }) {
const project = useQuery(api.projects.get, { projectId });
return (
<div>
<h1>{project?.name}</h1>
<p>{project?.description}</p>
<p>Your role: {project?.role}</p>
</div>
);
}
Project object with user’s rolerole
'owner' | 'admin' | 'member' | 'viewer'
User’s role in the project’s team
Permissions: Requires access to the project (through team membership).
Implementation: convex/projects.ts:100
update
Updates project name and/or description.
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function EditProjectForm({ projectId }) {
const updateProject = useMutation(api.projects.update);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const handleSubmit = async () => {
await updateProject({ projectId, name, description });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<textarea value={description} onChange={(e) => setDescription(e.target.value)} />
<button type="submit">Save</button>
</form>
);
}
New project name (optional)
New project description (optional)
Permissions: Requires member role in the project’s team.
Implementation: convex/projects.ts:108
remove
Deletes a project and all associated data (videos, comments, share links).
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function DeleteProjectButton({ projectId }) {
const removeProject = useMutation(api.projects.remove);
const handleDelete = async () => {
if (confirm('Delete this project and all its videos? This cannot be undone!')) {
await removeProject({ projectId });
}
};
return <button onClick={handleDelete}>Delete Project</button>;
}
Permissions: Requires admin role in the project’s team.
Implementation: convex/projects.ts:125
Usage Examples
Project Selector with Video Counts
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function ProjectSelector({ teamId, onSelect }) {
const projects = useQuery(api.projects.list, { teamId });
return (
<div className="project-selector">
<h3>Select a project</h3>
{projects?.map(project => (
<button
key={project._id}
onClick={() => onSelect(project._id)}
className="project-option"
>
<div>
<h4>{project.name}</h4>
{project.description && <p>{project.description}</p>}
</div>
<span className="video-count">{project.videoCount} videos</span>
</button>
))}
</div>
);
}
Project Dashboard
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function ProjectDashboard({ projectId }) {
const project = useQuery(api.projects.get, { projectId });
const videos = useQuery(api.videos.list, { projectId });
const updateProject = useMutation(api.projects.update);
const removeProject = useMutation(api.projects.remove);
const canEdit = project?.role && ['member', 'admin', 'owner'].includes(project.role);
const canDelete = project?.role && ['admin', 'owner'].includes(project.role);
return (
<div>
<header>
<h1>{project?.name}</h1>
<p>{project?.description}</p>
{canEdit && <button onClick={() => setEditing(true)}>Edit</button>}
{canDelete && (
<button onClick={() => removeProject({ projectId })}>Delete</button>
)}
</header>
<div className="stats">
<div>Total Videos: {videos?.length || 0}</div>
<div>In Review: {videos?.filter(v => v.workflowStatus === 'review').length}</div>
<div>Completed: {videos?.filter(v => v.workflowStatus === 'done').length}</div>
</div>
<VideoGrid videos={videos} />
</div>
);
}
Multi-Team Upload Destination
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function UniversalUploader() {
const targets = useQuery(api.projects.listUploadTargets);
const createVideo = useMutation(api.videos.create);
const [selectedProject, setSelectedProject] = useState(null);
const handleUpload = async (file: File) => {
if (!selectedProject) {
alert('Please select a project');
return;
}
await createVideo({
projectId: selectedProject,
title: file.name,
fileSize: file.size,
contentType: file.type,
});
};
return (
<div>
<select
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
>
<option value="">Choose destination...</option>
{targets?.map(target => (
<option key={target.projectId} value={target.projectId}>
{target.teamName} → {target.projectName}
</option>
))}
</select>
<input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
</div>
);
}
Create Project with Redirect
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
import { useRouter } from 'next/navigation';
function CreateProjectForm({ teamId }) {
const createProject = useMutation(api.projects.create);
const router = useRouter();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const projectId = await createProject({ teamId, name, description });
router.push(`/project/${projectId}`);
} catch (error) {
if (error.message.includes('active subscription')) {
alert('Your team needs an active subscription to create projects');
} else {
alert('Failed to create project: ' + error.message);
}
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Project name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<textarea
placeholder="Description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button type="submit">Create Project</button>
</form>
);
}
Project Organization
Projects inherit all permissions from their parent team:
- Team viewers can view projects and videos
- Team members can create and edit videos in projects
- Team admins can create, edit, and delete projects
- Team owners have full control
Projects are automatically deleted when their parent team is deleted.