115 lines
3.5 KiB
TypeScript
115 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, useContext, useState, useCallback } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import type { ThemeName } from "@/lib/themes";
|
|
import type { ProjectDTO } from "@/types/project";
|
|
import {
|
|
createProject,
|
|
renameProject as renameProjectAction,
|
|
deleteProject as deleteProjectAction,
|
|
setProjectTheme as setProjectThemeAction,
|
|
} from "@/lib/actions/projects";
|
|
|
|
interface ProjectsContextValue {
|
|
projects: ProjectDTO[];
|
|
addProject: (title: string) => Promise<ProjectDTO | undefined>;
|
|
renameProject: (projectId: string, title: string) => Promise<void>;
|
|
removeProject: (projectId: string) => Promise<boolean>;
|
|
/** Assign a project theme, or pass null for "Default Theme" (the user's
|
|
* global preference). Optimistic: the new value lands immediately and
|
|
* rolls back on failure. */
|
|
setProjectTheme: (projectId: string, theme: ThemeName | null) => Promise<void>;
|
|
}
|
|
|
|
const ProjectsContext = createContext<ProjectsContextValue | null>(null);
|
|
|
|
/**
|
|
* Holds the signed-in user's project list, fetched once in the (app)
|
|
* layout and kept live from there -- the sidebar's expandable "Projects"
|
|
* section and the /projects page both read from this same instance, so
|
|
* creating/renaming/deleting a project in one place shows up in the other
|
|
* immediately, with no extra round-trip.
|
|
*/
|
|
export function ProjectsProvider({
|
|
initialProjects,
|
|
children,
|
|
}: {
|
|
initialProjects: ProjectDTO[];
|
|
children: React.ReactNode;
|
|
}) {
|
|
const [projects, setProjects] = useState(initialProjects);
|
|
|
|
const addProject = useCallback(async (title: string) => {
|
|
try {
|
|
const project = await createProject(title);
|
|
setProjects((prev) => [...prev, project]);
|
|
return project;
|
|
} catch {
|
|
toast.error("Couldn't create project. Try again.");
|
|
return undefined;
|
|
}
|
|
}, []);
|
|
|
|
const renameProject = useCallback(async (projectId: string, title: string) => {
|
|
let prevState: ProjectDTO[] = [];
|
|
setProjects((prev) => {
|
|
prevState = prev;
|
|
return prev.map((p) => (p.id === projectId ? { ...p, title } : p));
|
|
});
|
|
try {
|
|
await renameProjectAction(projectId, title);
|
|
} catch {
|
|
setProjects(prevState);
|
|
toast.error("Couldn't rename project. Try again.");
|
|
}
|
|
}, []);
|
|
|
|
const removeProject = useCallback(async (projectId: string) => {
|
|
let prevState: ProjectDTO[] = [];
|
|
setProjects((prev) => {
|
|
prevState = prev;
|
|
return prev.filter((p) => p.id !== projectId);
|
|
});
|
|
const result = await deleteProjectAction(projectId);
|
|
if (result?.error) {
|
|
setProjects(prevState);
|
|
toast.error(result.error);
|
|
return false;
|
|
}
|
|
return true;
|
|
}, []);
|
|
|
|
const setProjectTheme = useCallback(
|
|
async (projectId: string, theme: ThemeName | null) => {
|
|
let prevState: ProjectDTO[] = [];
|
|
setProjects((prev) => {
|
|
prevState = prev;
|
|
return prev.map((p) => (p.id === projectId ? { ...p, theme } : p));
|
|
});
|
|
try {
|
|
await setProjectThemeAction(projectId, theme);
|
|
} catch {
|
|
setProjects(prevState);
|
|
toast.error("Couldn't save the project theme. Try again.");
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<ProjectsContext.Provider
|
|
value={{ projects, addProject, renameProject, removeProject, setProjectTheme }}
|
|
>
|
|
{children}
|
|
</ProjectsContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useProjects() {
|
|
const ctx = useContext(ProjectsContext);
|
|
if (!ctx) throw new Error("useProjects must be used within a ProjectsProvider");
|
|
return ctx;
|
|
}
|