Initial Planlink implementation

This commit is contained in:
2026-03-05 14:15:58 -05:00
commit ad6c63b699
39 changed files with 7510 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="es" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Planlink</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body class="h-full bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
<div id="root" class="h-full"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+2614
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "planlink-client",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"socket.io-client": "^4.7.5"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10",
"vite": "^5.4.1"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
+1569
View File
File diff suppressed because it is too large Load Diff
+238
View File
@@ -0,0 +1,238 @@
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:4000/api";
async function request(path, options = {}) {
const res = await fetch(`${API_URL}${path}`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
...(options.headers || {})
},
...options
});
const data = await res.json();
if (!res.ok) {
const message = data?.message || "request failed";
throw new Error(message);
}
return data;
}
export function login(payload) {
return request("/auth/login", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function register(payload) {
return request("/auth/register", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function logout() {
return request("/auth/logout", { method: "POST" });
}
export function me() {
return request("/me");
}
export function listProjects() {
return request("/projects");
}
export function createProject(payload) {
return request("/projects", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function listProjectMembers(projectId) {
return request(`/projects/${projectId}/members`);
}
export function inviteProjectMember(projectId, payload) {
return request(`/projects/${projectId}/invites`, {
method: "POST",
body: JSON.stringify(payload)
});
}
export function listProjectInvites(projectId) {
return request(`/projects/${projectId}/invites`);
}
export function acceptProjectInvite(projectId, token) {
return request(`/projects/${projectId}/invites/accept`, {
method: "POST",
body: JSON.stringify({ token })
});
}
export function revokeProjectInvite(projectId, inviteId) {
return request(`/projects/${projectId}/invites/revoke`, {
method: "POST",
body: JSON.stringify({ inviteId })
});
}
export function listBoards(projectId) {
return request(`/boards?projectId=${projectId}`);
}
export function createBoard(payload) {
return request("/boards", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function listBoardActivity(boardId) {
return request(`/boards/${boardId}/activity`);
}
export function listLists(boardId) {
return request(`/lists?boardId=${boardId}`);
}
export function createList(payload) {
return request("/lists", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function updateList(id, payload) {
return request(`/lists/${id}`, {
method: "PATCH",
body: JSON.stringify(payload)
});
}
export function listCards(listId) {
return request(`/cards?listId=${listId}`);
}
export function createCard(payload) {
return request("/cards", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function updateCard(id, payload) {
return request(`/cards/${id}`, {
method: "PATCH",
body: JSON.stringify(payload)
});
}
export function listComments(cardId) {
return request(`/cards/${cardId}/comments`);
}
export function addComment(cardId, body) {
return request(`/cards/${cardId}/comments`, {
method: "POST",
body: JSON.stringify({ body })
});
}
export function listAttachments(cardId) {
return request(`/cards/${cardId}/attachments`);
}
export function addAttachment(cardId, payload) {
return request(`/cards/${cardId}/attachments`, {
method: "POST",
body: JSON.stringify(payload)
});
}
export function listCardMembers(cardId) {
return request(`/cards/${cardId}/members`);
}
export function addCardMember(cardId, userId) {
return request(`/cards/${cardId}/members`, {
method: "POST",
body: JSON.stringify({ userId })
});
}
export function listCardLabels(cardId) {
return request(`/cards/${cardId}/labels`);
}
export function addCardLabel(cardId, labelId) {
return request(`/cards/${cardId}/labels`, {
method: "POST",
body: JSON.stringify({ labelId })
});
}
export function listLabels(projectId) {
return request(`/labels?projectId=${projectId}`);
}
export function createLabel(payload) {
return request("/labels", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function createPlannerRequest(payload) {
return request("/planner/requests", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function runPlannerRequest(id) {
return request(`/planner/requests/${id}/run`, { method: "POST" });
}
export function streamPlannerRequest(id, onDelta, onDone, onError) {
const url = `${API_URL}/planner/requests/${id}/stream`;
const source = new EventSource(url, { withCredentials: true });
source.addEventListener("delta", (event) => {
const data = JSON.parse(event.data);
onDelta?.(data.delta || "");
});
source.addEventListener("done", () => {
onDone?.();
source.close();
});
source.addEventListener("error", () => {
onError?.();
source.close();
});
return source;
}
export function saveManualPlanner(payload) {
return request("/planner/manual", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function listPlanners() {
return request("/planner");
}
export function updatePlanner(id, outputText) {
return request(`/planner/requests/${id}`, {
method: "PATCH",
body: JSON.stringify({ outputText })
});
}
+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+12
View File
@@ -0,0 +1,12 @@
import { io } from "socket.io-client";
const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || "http://localhost:4000";
let socket;
export function getSocket() {
if (!socket) {
socket = io(SOCKET_URL, { withCredentials: true });
}
return socket;
}
+7
View File
@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
color-scheme: light dark;
}
+12
View File
@@ -0,0 +1,12 @@
export default {
darkMode: "class",
content: ["./index.html", "./src/**/*.{js,jsx}"],
theme: {
extend: {
fontFamily: {
display: ["Space Grotesk", "ui-sans-serif", "system-ui"]
}
}
},
plugins: []
};
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5173
}
});