feat(project-crud-sync): add planning artifacts for CRUD project lifecycle #5

Open
NHoarau wants to merge 14 commits from feat/create-projects into main
4 changed files with 55 additions and 90 deletions
Showing only changes of commit a4f909289c - Show all commits

View file

@ -1,35 +1,8 @@
import type { Project, ProjectInsertDTO, ProjectUpdateDTO } from "@workspace/schema"; import type { Project, ProjectInsertDTO, ProjectUpdateDTO } from "@workspace/schema";
interface ApiResponse<T> {
data: T | null;
error: string | null;
}
function getBaseUrl(): string {
const config = useRuntimeConfig();
return config.public.apiBaseUrl as string;
}
async function request<T>(path: string, options?: RequestInit): Promise<ApiResponse<T>> {
try {
const res = await fetch(`${getBaseUrl()}${path}`, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!res.ok) {
const body = await res.json().catch(() => null);
return { data: null, error: body?.message ?? `HTTP ${res.status}` };
}
const data = (await res.json()) as T;
return { data, error: null };
} catch (err) {
return { data: null, error: err instanceof Error ? err.message : "Unknown error" };
}
}
export function useProjectApi() { export function useProjectApi() {
const config = useRuntimeConfig();
const apiBase = config.public.apiBaseUrl as string;
const NOT_FOUND = -1; const NOT_FOUND = -1;
const projects = ref<Project[]>([]); const projects = ref<Project[]>([]);
@ -39,67 +12,65 @@ export function useProjectApi() {
async function fetchProjects(): Promise<void> { async function fetchProjects(): Promise<void> {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
try {
const { data, error: err } = await request<Project[]>("/projects"); projects.value = await $fetch<Project[]>(`${apiBase}/projects`);
if (data) projects.value = data; } catch (err) {
else error.value = err; error.value = err instanceof Error ? err.message : "Unknown error";
} finally {
loading.value = false; loading.value = false;
}
} }
async function createProject(body: Partial<ProjectInsertDTO>): Promise<Project | null> { async function createProject(body: Partial<ProjectInsertDTO>): Promise<Project | null> {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
try {
const { data, error: err } = await request<Project>("/projects", { const created = await $fetch<Project>(`${apiBase}/projects`, {
method: "POST", method: "POST",
body: JSON.stringify(body), body,
}); });
projects.value.push(created);
if (data) { return created;
projects.value.push(data); } catch (err) {
} else { error.value = err instanceof Error ? err.message : "Unknown error";
error.value = err; return null;
} finally {
loading.value = false;
} }
loading.value = false;
return data;
} }
async function updateProject(id: string, body: Partial<ProjectUpdateDTO>): Promise<Project | null> { async function updateProject(id: string, body: Partial<ProjectUpdateDTO>): Promise<Project | null> {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
try {
const { data, error: err } = await request<Project>(`/projects/${id}`, { const updated = await $fetch<Project>(`${apiBase}/projects/${id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify(body), body,
}); });
const idx = projects.value.findIndex((p) => p.id === id);
if (data) { if (idx !== NOT_FOUND) projects.value[idx] = updated;
const idx = projects.value.findIndex(p => p.id === id); return updated;
if (idx !== NOT_FOUND) projects.value[idx] = data; } catch (err) {
} else { error.value = err instanceof Error ? err.message : "Unknown error";
error.value = err; return null;
} finally {
loading.value = false;
} }
loading.value = false;
return data;
} }
async function deleteProject(id: string): Promise<boolean> { async function deleteProject(id: string): Promise<boolean> {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
try {
const { error: err } = await request(`/projects/${id}`, { method: "DELETE" }); await $fetch(`${apiBase}/projects/${id}`, { method: "DELETE" });
projects.value = projects.value.filter((p) => p.id !== id);
if (err) { return true;
error.value = err; } catch (err) {
} else { error.value = err instanceof Error ? err.message : "Unknown error";
projects.value = projects.value.filter(p => p.id !== id); return false;
} finally {
loading.value = false;
} }
loading.value = false;
return !err;
} }
return { projects, loading, error, fetchProjects, createProject, updateProject, deleteProject }; return { projects, loading, error, fetchProjects, createProject, updateProject, deleteProject };

View file

@ -16,17 +16,14 @@ const project = ref<Project | null>(null);
const loading = ref(true); const loading = ref(true);
const notFound = ref(false); const notFound = ref(false);
const config = useRuntimeConfig();
const apiBase = config.public.apiBaseUrl as string;
onMounted(async () => { onMounted(async () => {
const res = await fetch(`${apiBase}/projects/${route.params.id}`); try {
if (res.ok) { project.value = await $fetch<Project>(`${useRuntimeConfig().public.apiBaseUrl as string}/projects/${route.params.id}`);
project.value = await res.json(); } catch {
} else {
notFound.value = true; notFound.value = true;
} finally {
loading.value = false;
} }
loading.value = false;
}); });
async function handleSubmit(body: { name: string; description?: string; status: string }) { async function handleSubmit(body: { name: string; description?: string; status: string }) {

View file

@ -1,14 +1,17 @@
// @ts-check // @ts-check
import withNuxt from "./.nuxt/eslint.config.mjs" import withNuxt from "./.nuxt/eslint.config.mjs";
import { baseRules, } from "@workspace/tooling/eslint/base" import { baseRules, } from "@workspace/tooling/eslint/base";
export default withNuxt({ export default withNuxt({
rules: { rules: {
...baseRules, ...baseRules,
"@stylistic/member-delimiter-style": "off", "@stylistic/member-delimiter-style": "off",
"@stylistic/arrow-parens": "off",
"vue/max-attributes-per-line": "off", "vue/max-attributes-per-line": "off",
"vue/singleline-html-element-content-newline": "off", "vue/singleline-html-element-content-newline": "off",
"vue/html-closing-bracket-newline": "off", "vue/html-closing-bracket-newline": "off",
"vue/no-multiple-template-root": "off", "vue/no-multiple-template-root": "off",
"max-lines-per-function": "off",
"max-statements": "off",
}, },
},) },);

View file

@ -21,10 +21,4 @@ export {
type ProjectUpdateDTO, type ProjectUpdateDTO,
} from "./drizzle/projects"; } from "./drizzle/projects";
export { projectsRelations, tasksRelations } from "./drizzle/relations"; export { projectsRelations, tasksRelations } from "./drizzle/relations";
export { export { tasks, taskSelect, taskInsert, taskUpdate, type Task } from "./drizzle/tasks";
tasks,
taskSelect,
taskInsert,
taskUpdate,
type Task,
} from "./drizzle/tasks";