Add frontend config and API client
This commit is contained in:
1 parent
e2d0a01fd2
commit
0661612150
9 files changed
+190
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
public/fonts/
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<meta name="theme-color" content="#6965db" />
|
||||||
|
<title>Excalidraw – schémas</title>
|
||||||
|
<script>
|
||||||
|
// Polices Excalidraw servies localement (copiées dans public/fonts au build)
|
||||||
|
// au lieu du CDN esm.sh : fonctionnement hors ligne.
|
||||||
|
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
@@ -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<Response> {
|
||||||
|
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<Entry[]> =>
|
||||||
|
(await request(`/api/list?path=${q(dir)}`)).json(),
|
||||||
|
|
||||||
|
search: async (text: string): Promise<Entry[]> =>
|
||||||
|
(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<string> => {
|
||||||
|
const headers: Record<string, string> = { "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<Entry> => (await request("/api/folder", json({ path }))).json(),
|
||||||
|
|
||||||
|
move: async (from: string, to: string): Promise<Entry> =>
|
||||||
|
(await request("/api/move", json({ from, to }))).json(),
|
||||||
|
|
||||||
|
copy: async (from: string, to: string): Promise<Entry> =>
|
||||||
|
(await request("/api/copy", json({ from, to }))).json(),
|
||||||
|
|
||||||
|
remove: async (path: string, recursive: boolean): Promise<void> => {
|
||||||
|
await request(`/api/entry?path=${q(path)}&recursive=${recursive}`, { method: "DELETE" });
|
||||||
|
},
|
||||||
|
|
||||||
|
readLibrary: async (): Promise<unknown> => (await request("/api/library")).json(),
|
||||||
|
|
||||||
|
writeLibrary: async (body: string): Promise<void> => {
|
||||||
|
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;
|
||||||
@@ -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(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
EXCALIDRAW_ASSET_PATH?: string | string[];
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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") },
|
||||||
|
});
|
||||||
Reference in new issue
Block a user