diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..59d3ddf --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +public/fonts/ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..341239c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,18 @@ + + + + + + + Excalidraw – schémas + + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..bd02304 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "excalidraw-go-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "prebuild": "node scripts/copy-fonts.mjs", + "predev": "node scripts/copy-fonts.mjs", + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@excalidraw/excalidraw": "0.18.1", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "^5.8.0", + "vite": "^7.0.0" + } +} diff --git a/frontend/scripts/copy-fonts.mjs b/frontend/scripts/copy-fonts.mjs new file mode 100644 index 0000000..e0a0349 --- /dev/null +++ b/frontend/scripts/copy-fonts.mjs @@ -0,0 +1,10 @@ +// Copie les polices d'Excalidraw dans public/fonts pour un usage hors ligne. +import { cpSync, existsSync } from "node:fs"; + +const src = "node_modules/@excalidraw/excalidraw/dist/prod/fonts"; +if (!existsSync(src)) { + console.error(`polices introuvables : ${src}`); + process.exit(1); +} +cpSync(src, "public/fonts", { recursive: true }); +console.log("polices Excalidraw copiées dans public/fonts"); diff --git a/frontend/src/api.ts b/frontend/src/api.ts new file mode 100644 index 0000000..6f94e79 --- /dev/null +++ b/frontend/src/api.ts @@ -0,0 +1,91 @@ +// Client de l'API Go (voir internal/api/api.go). + +export interface Entry { + name: string; + path: string; + isDir: boolean; + size: number; + modTime: string; +} + +export class ApiError extends Error { + constructor(public status: number, message: string) { + super(message); + } +} + +async function request(url: string, init?: RequestInit): Promise { + const res = await fetch(url, init); + if (!res.ok) { + let msg = res.statusText; + try { + msg = (await res.json()).error ?? msg; + } catch { + /* corps non JSON */ + } + throw new ApiError(res.status, msg); + } + return res; +} + +const q = (p: string) => encodeURIComponent(p); +const json = (body: unknown): RequestInit => ({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), +}); + +export const api = { + list: async (dir: string): Promise => + (await request(`/api/list?path=${q(dir)}`)).json(), + + search: async (text: string): Promise => + (await request(`/api/search?q=${q(text)}`)).json(), + + read: async (path: string): Promise<{ text: string; etag: string }> => { + const res = await request(`/api/file?path=${q(path)}`); + return { text: await res.text(), etag: res.headers.get("ETag") ?? "" }; + }, + + /** etag : "" = écrasement libre, "*" = création seule, sinon contrôle optimiste. */ + write: async (path: string, body: string, etag: string): Promise => { + const headers: Record = { "Content-Type": "application/json" }; + if (etag === "*") headers["If-None-Match"] = "*"; + else if (etag) headers["If-Match"] = etag; + const res = await request(`/api/file?path=${q(path)}`, { method: "PUT", headers, body }); + return res.headers.get("ETag") ?? ""; + }, + + mkdir: async (path: string): Promise => (await request("/api/folder", json({ path }))).json(), + + move: async (from: string, to: string): Promise => + (await request("/api/move", json({ from, to }))).json(), + + copy: async (from: string, to: string): Promise => + (await request("/api/copy", json({ from, to }))).json(), + + remove: async (path: string, recursive: boolean): Promise => { + await request(`/api/entry?path=${q(path)}&recursive=${recursive}`, { method: "DELETE" }); + }, + + readLibrary: async (): Promise => (await request("/api/library")).json(), + + writeLibrary: async (body: string): Promise => { + await request("/api/library", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body, + }); + }, + + downloadUrl: (path: string) => `/api/file?path=${q(path)}`, +}; + +// Helpers de chemins (format "slash", relatifs à la racine). +export const dirname = (p: string) => (p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : ""); +export const basename = (p: string) => p.slice(p.lastIndexOf("/") + 1); +export const join = (dir: string, name: string) => (dir ? `${dir}/${name}` : name); +export const EXT = ".excalidraw"; +export const withExt = (name: string) => (name.toLowerCase().endsWith(EXT) ? name : name + EXT); +export const stripExt = (name: string) => + name.toLowerCase().endsWith(EXT) ? name.slice(0, -EXT.length) : name; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..8f24a9b --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "@excalidraw/excalidraw/index.css"; +import "./styles.css"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..fbfbd1f --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,5 @@ +/// + +interface Window { + EXCALIDRAW_ASSET_PATH?: string | string[]; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..b5318a6 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "isolatedModules": true, + "allowImportingTsExtensions": false, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..dd18e44 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + // Le build est embarqué dans le binaire Go (web/dist). + build: { outDir: "../web/dist", emptyOutDir: true, chunkSizeWarningLimit: 4096 }, + // `npm run dev` : l'API Go tourne sur :8080. + server: { proxy: { "/api": "http://localhost:8080" } }, + define: { "process.env.IS_PREACT": JSON.stringify("false") }, +});