From e3f1fa3fa0089a06c4d4dc1528d3ec0080798b9c Mon Sep 17 00:00:00 2001 From: Poyraz Avsever Date: Wed, 11 Mar 2026 11:03:56 +0300 Subject: [PATCH] feat(panel): add electron dashboards for data, podcasts, media, and publish --- apps/data-panel/.gitignore | 41 ++ apps/data-panel/main.cjs | 698 ++++++++++++++++++++++++++ apps/data-panel/package.json | 12 + apps/data-panel/preload.cjs | 34 ++ apps/data-panel/renderer/app.js | 742 ++++++++++++++++++++++++++++ apps/data-panel/renderer/configs.js | 206 ++++++++ apps/data-panel/renderer/index.html | 231 +++++++++ apps/data-panel/renderer/styles.css | 332 +++++++++++++ package.json | 4 +- pnpm-lock.yaml | 419 ++++++++++++++++ pnpm-workspace.yaml | 7 + 11 files changed, 2725 insertions(+), 1 deletion(-) create mode 100644 apps/data-panel/.gitignore create mode 100644 apps/data-panel/main.cjs create mode 100644 apps/data-panel/package.json create mode 100644 apps/data-panel/preload.cjs create mode 100644 apps/data-panel/renderer/app.js create mode 100644 apps/data-panel/renderer/configs.js create mode 100644 apps/data-panel/renderer/index.html create mode 100644 apps/data-panel/renderer/styles.css diff --git a/apps/data-panel/.gitignore b/apps/data-panel/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/apps/data-panel/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/data-panel/main.cjs b/apps/data-panel/main.cjs new file mode 100644 index 0000000..b1bcb6b --- /dev/null +++ b/apps/data-panel/main.cjs @@ -0,0 +1,698 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const { app, BrowserWindow, dialog, ipcMain } = require("electron"); +const { execFile } = require("node:child_process"); +const fs = require("node:fs/promises"); +const path = require("node:path"); +const vm = require("node:vm"); +const matter = require("gray-matter"); + +const WORKSPACE_ROOT = path.resolve(__dirname, "../.."); +const DATA_DIR = path.join(WORKSPACE_ROOT, "data"); +const PUBLIC_DIR = path.join(WORKSPACE_ROOT, "public"); +const BLOG_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "blog"); +const PODCAST_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "podcasts"); + +function createWindow() { + const win = new BrowserWindow({ + width: 1320, + height: 860, + minWidth: 1080, + minHeight: 700, + webPreferences: { + preload: path.join(__dirname, "preload.cjs"), + contextIsolation: true, + nodeIntegration: false, + }, + }); + + win.loadFile(path.join(__dirname, "renderer", "index.html")); +} + +function assertSafePath(baseDir, targetPath) { + const resolvedBase = path.resolve(baseDir); + const resolvedTarget = path.resolve(targetPath); + const isSafe = + resolvedTarget === resolvedBase || resolvedTarget.startsWith(`${resolvedBase}${path.sep}`); + + if (!isSafe) { + throw new Error("Invalid path access attempt."); + } + + return resolvedTarget; +} + +function validateDataFileName(fileName) { + if (!/^[a-z0-9-]+\.ts$/i.test(fileName)) { + throw new Error("File name must match: .ts"); + } +} + +function validateBlogSlug(slug) { + const normalized = String(slug || "") + .trim() + .toLowerCase(); + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalized)) { + throw new Error("Slug must match: lowercase-kebab-case"); + } + return normalized; +} + +function validatePodcastKind(kind) { + const normalized = String(kind || "") + .trim() + .toLowerCase(); + + if (normalized !== "yazilim" && normalized !== "masa-basi") { + throw new Error("Podcast kind must be one of: yazilim, masa-basi"); + } + + return normalized; +} + +function runGit(args) { + return new Promise((resolve, reject) => { + execFile("git", args, { cwd: WORKSPACE_ROOT }, (error, stdout, stderr) => { + if (error) { + reject(new Error(stderr || error.message)); + return; + } + resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); + }); + }); +} + +function formatTsKey(key) { + return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key); +} + +function toTsLiteral(value, indent = 0) { + const space = " "; + const current = space.repeat(indent); + const next = space.repeat(indent + 1); + + if (value === null) return "null"; + if (typeof value === "string") return JSON.stringify(value); + if (typeof value === "number" || typeof value === "boolean") return String(value); + + if (Array.isArray(value)) { + if (!value.length) return "[]"; + return `[\n${value + .map((item) => `${next}${toTsLiteral(item, indent + 1)}`) + .join(",\n")}\n${current}]`; + } + + if (typeof value === "object") { + const entries = Object.entries(value); + if (!entries.length) return "{}"; + return `{\n${entries + .map(([key, entryValue]) => { + return `${next}${formatTsKey(key)}: ${toTsLiteral(entryValue, indent + 1)}`; + }) + .join(",\n")}\n${current}}`; + } + + return JSON.stringify(value); +} + +function findExpressionBounds(source, startIndex) { + let i = startIndex; + while (i < source.length && /\s/.test(source[i])) i += 1; + + const start = i; + const first = source[start]; + if (!first) throw new Error("Cannot parse export value."); + + if (first !== "[" && first !== "{" && first !== "(" && first !== '"' && first !== "'" && first !== "`") { + throw new Error("Only literal exports are supported in structured mode."); + } + + const closingMap = { "[": "]", "{": "}", "(": ")" }; + let stack = []; + if (closingMap[first]) stack = [closingMap[first]]; + + let mode = first === '"' || first === "'" || first === "`" ? first : "normal"; + if (mode !== "normal") i += 1; + if (mode === "normal") i = start + 1; + + while (i < source.length) { + const ch = source[i]; + const next = source[i + 1]; + + if (mode === "normal") { + if (ch === "'" || ch === '"' || ch === "`") { + mode = ch; + i += 1; + continue; + } + + if (ch === "/" && next === "/") { + mode = "line-comment"; + i += 2; + continue; + } + + if (ch === "/" && next === "*") { + mode = "block-comment"; + i += 2; + continue; + } + + if (closingMap[ch]) { + stack.push(closingMap[ch]); + i += 1; + continue; + } + + if (stack.length && ch === stack[stack.length - 1]) { + stack.pop(); + i += 1; + if (!stack.length) { + return { start, end: i }; + } + continue; + } + + i += 1; + continue; + } + + if (mode === "line-comment") { + if (ch === "\n") mode = "normal"; + i += 1; + continue; + } + + if (mode === "block-comment") { + if (ch === "*" && next === "/") { + mode = "normal"; + i += 2; + continue; + } + i += 1; + continue; + } + + if (mode === "`") { + if (ch === "\\") { + i += 2; + continue; + } + if (ch === "`") { + mode = "normal"; + i += 1; + if (!stack.length) return { start, end: i }; + continue; + } + i += 1; + continue; + } + + if (mode === "'" || mode === '"') { + if (ch === "\\") { + i += 2; + continue; + } + if (ch === mode) { + mode = "normal"; + i += 1; + if (!stack.length) return { start, end: i }; + continue; + } + i += 1; + } + } + + throw new Error("Unable to parse export expression boundaries."); +} + +function getExportBlocks(source) { + const blocks = []; + const regex = /export\s+const\s+([A-Za-z0-9_]+)(\s*:\s*[^=]+)?\s*=\s*/g; + let match = regex.exec(source); + + while (match) { + const exportName = match[1]; + const expressionStart = regex.lastIndex; + + try { + const bounds = findExpressionBounds(source, expressionStart); + const expression = source.slice(bounds.start, bounds.end); + + let parsed = null; + let parseError = null; + try { + parsed = vm.runInNewContext(`(${expression})`, {}, { timeout: 800 }); + } catch (error) { + parseError = error instanceof Error ? error.message : "Unknown parse error."; + } + + blocks.push({ + name: exportName, + start: bounds.start, + end: bounds.end, + expression, + parsed, + isArray: Array.isArray(parsed), + parseError, + }); + } catch (error) { + blocks.push({ + name: exportName, + start: -1, + end: -1, + expression: "", + parsed: null, + isArray: false, + parseError: error instanceof Error ? error.message : "Unknown parse error.", + }); + } + + match = regex.exec(source); + } + + return blocks; +} + +async function listDataFiles() { + const files = await fs.readdir(DATA_DIR, { withFileTypes: true }); + return files + .filter((item) => item.isFile() && item.name.endsWith(".ts")) + .map((item) => item.name) + .sort((a, b) => a.localeCompare(b)); +} + +async function readDataFile(fileName) { + validateDataFileName(fileName); + const target = assertSafePath(DATA_DIR, path.join(DATA_DIR, fileName)); + const raw = await fs.readFile(target, "utf8"); + const exports = getExportBlocks(raw).map((entry) => ({ + name: entry.name, + isArray: entry.isArray, + value: entry.parsed, + parseError: entry.parseError, + })); + + return { fileName, raw, exports }; +} + +async function saveRawDataFile(fileName, raw) { + validateDataFileName(fileName); + const target = assertSafePath(DATA_DIR, path.join(DATA_DIR, fileName)); + await fs.writeFile(target, raw, "utf8"); + return { ok: true }; +} + +async function updateDataExport({ fileName, exportName, value }) { + validateDataFileName(fileName); + const target = assertSafePath(DATA_DIR, path.join(DATA_DIR, fileName)); + const source = await fs.readFile(target, "utf8"); + const blocks = getExportBlocks(source); + const selected = blocks.find((block) => block.name === exportName); + + if (!selected || selected.start < 0 || selected.end < 0) { + throw new Error(`Export "${exportName}" not found or cannot be parsed.`); + } + + const literal = toTsLiteral(value, 0); + const nextSource = `${source.slice(0, selected.start)}${literal}${source.slice(selected.end)}`; + await fs.writeFile(target, nextSource, "utf8"); + return { ok: true }; +} + +async function createDataFile({ fileName, exportName }) { + validateDataFileName(fileName); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(exportName)) { + throw new Error("Export name is invalid."); + } + const target = assertSafePath(DATA_DIR, path.join(DATA_DIR, fileName)); + await fs.writeFile(target, `export const ${exportName} = [] as const;\n`, { + encoding: "utf8", + flag: "wx", + }); + return { ok: true }; +} + +async function deleteDataFile(fileName) { + validateDataFileName(fileName); + const target = assertSafePath(DATA_DIR, path.join(DATA_DIR, fileName)); + await fs.unlink(target); + return { ok: true }; +} + +function mapMarkdownToBlog(fileName, raw) { + const parsed = matter(raw); + const slug = fileName.replace(/\.md$/i, ""); + return { + slug, + title: String(parsed.data.title || slug), + category: String(parsed.data.category || "General"), + date: String(parsed.data.date || ""), + readTime: String(parsed.data.readTime || ""), + author: String(parsed.data.author || "Poyraz Avsever"), + excerpt: String(parsed.data.excerpt || ""), + coverImage: String(parsed.data.coverImage || "/news/design.svg"), + markdown: String(parsed.content || "").trim(), + }; +} + +async function listBlogs() { + await fs.mkdir(BLOG_CONTENT_DIR, { recursive: true }); + const files = await fs.readdir(BLOG_CONTENT_DIR, { withFileTypes: true }); + const markdownFiles = files + .filter((item) => item.isFile() && item.name.endsWith(".md")) + .map((item) => item.name) + .sort((a, b) => a.localeCompare(b)); + + const posts = await Promise.all( + markdownFiles.map(async (fileName) => { + const target = assertSafePath(BLOG_CONTENT_DIR, path.join(BLOG_CONTENT_DIR, fileName)); + const raw = await fs.readFile(target, "utf8"); + return mapMarkdownToBlog(fileName, raw); + }), + ); + + return posts; +} + +async function upsertBlog({ originalSlug, post }) { + if (!post || typeof post !== "object") { + throw new Error("Post payload is invalid."); + } + + const slug = validateBlogSlug(post.slug); + const previousSlug = originalSlug ? validateBlogSlug(originalSlug) : null; + + const frontmatter = { + title: String(post.title || slug), + category: String(post.category || "General"), + date: String(post.date || ""), + readTime: String(post.readTime || ""), + author: String(post.author || "Poyraz Avsever"), + excerpt: String(post.excerpt || ""), + coverImage: String(post.coverImage || "/news/design.svg"), + }; + + const markdownBody = String(post.markdown || "").trim(); + const raw = matter.stringify(markdownBody ? `${markdownBody}\n` : "", frontmatter); + + await fs.mkdir(BLOG_CONTENT_DIR, { recursive: true }); + + const nextPath = assertSafePath(BLOG_CONTENT_DIR, path.join(BLOG_CONTENT_DIR, `${slug}.md`)); + const previousPath = + previousSlug && previousSlug !== slug + ? assertSafePath(BLOG_CONTENT_DIR, path.join(BLOG_CONTENT_DIR, `${previousSlug}.md`)) + : null; + + await fs.writeFile(nextPath, raw, "utf8"); + + if (previousPath) { + try { + await fs.unlink(previousPath); + } catch { + // ignore missing previous file + } + } + + return { ok: true, slug }; +} + +async function deleteBlogBySlug(slug) { + const normalized = validateBlogSlug(slug); + const target = assertSafePath(BLOG_CONTENT_DIR, path.join(BLOG_CONTENT_DIR, `${normalized}.md`)); + await fs.unlink(target); + return { ok: true }; +} + +function mapMarkdownToPodcast(kind, fileName, raw) { + const parsed = matter(raw); + const slug = fileName.replace(/\.md$/i, ""); + + return { + slug, + title: String(parsed.data.title || slug), + date: String(parsed.data.date || ""), + youtubeUrl: String(parsed.data.youtubeUrl || ""), + spotifyUrl: String(parsed.data.spotifyUrl || ""), + podcast: kind, + markdown: String(parsed.content || "").trim(), + }; +} + +async function listPodcastEpisodes(kind) { + const safeKind = validatePodcastKind(kind); + const podcastDir = assertSafePath(PODCAST_CONTENT_DIR, path.join(PODCAST_CONTENT_DIR, safeKind)); + + await fs.mkdir(podcastDir, { recursive: true }); + const files = await fs.readdir(podcastDir, { withFileTypes: true }); + const markdownFiles = files + .filter((item) => item.isFile() && item.name.endsWith(".md")) + .map((item) => item.name) + .sort((a, b) => b.localeCompare(a)); + + const episodes = await Promise.all( + markdownFiles.map(async (fileName) => { + const target = assertSafePath(podcastDir, path.join(podcastDir, fileName)); + const raw = await fs.readFile(target, "utf8"); + return mapMarkdownToPodcast(safeKind, fileName, raw); + }), + ); + + return episodes; +} + +async function upsertPodcastEpisode(kind, { originalSlug, episode }) { + const safeKind = validatePodcastKind(kind); + if (!episode || typeof episode !== "object") { + throw new Error("Episode payload is invalid."); + } + + const slug = validateBlogSlug(episode.slug); + const previousSlug = originalSlug ? validateBlogSlug(originalSlug) : null; + + const frontmatter = { + title: String(episode.title || slug), + date: String(episode.date || ""), + youtubeUrl: String(episode.youtubeUrl || ""), + spotifyUrl: String(episode.spotifyUrl || ""), + podcast: safeKind, + }; + + const markdownBody = String(episode.markdown || "").trim(); + const raw = matter.stringify(markdownBody ? `${markdownBody}\n` : "", frontmatter); + + const podcastDir = assertSafePath(PODCAST_CONTENT_DIR, path.join(PODCAST_CONTENT_DIR, safeKind)); + await fs.mkdir(podcastDir, { recursive: true }); + + const nextPath = assertSafePath(podcastDir, path.join(podcastDir, `${slug}.md`)); + const previousPath = + previousSlug && previousSlug !== slug + ? assertSafePath(podcastDir, path.join(podcastDir, `${previousSlug}.md`)) + : null; + + await fs.writeFile(nextPath, raw, "utf8"); + + if (previousPath) { + try { + await fs.unlink(previousPath); + } catch { + // ignore missing previous file + } + } + + return { ok: true, slug }; +} + +async function deletePodcastEpisode(kind, slug) { + const safeKind = validatePodcastKind(kind); + const normalized = validateBlogSlug(slug); + const podcastDir = assertSafePath(PODCAST_CONTENT_DIR, path.join(PODCAST_CONTENT_DIR, safeKind)); + const target = assertSafePath(podcastDir, path.join(podcastDir, `${normalized}.md`)); + await fs.unlink(target); + return { ok: true }; +} + +async function listFoldersRecursive(baseDir, current = "") { + const target = assertSafePath(baseDir, path.join(baseDir, current)); + const entries = await fs.readdir(target, { withFileTypes: true }); + const folders = [current]; + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const child = current ? path.posix.join(current, entry.name) : entry.name; + const nested = await listFoldersRecursive(baseDir, child); + folders.push(...nested); + } + + return folders; +} + +async function listMediaFolders() { + const folders = await listFoldersRecursive(PUBLIC_DIR); + return folders.sort((a, b) => a.localeCompare(b)); +} + +async function createMediaFolder(folder) { + const normalized = folder.trim().replace(/\\/g, "/").replace(/^\/+/, ""); + if (!normalized) throw new Error("Folder path is required."); + const target = assertSafePath(PUBLIC_DIR, path.join(PUBLIC_DIR, normalized)); + await fs.mkdir(target, { recursive: true }); + return { ok: true }; +} + +async function listMediaFiles(folder = "") { + const normalized = folder.trim().replace(/\\/g, "/").replace(/^\/+/, ""); + const target = assertSafePath(PUBLIC_DIR, path.join(PUBLIC_DIR, normalized)); + const entries = await fs.readdir(target, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + if (!entry.isFile()) continue; + const relativePath = normalized + ? path.posix.join(normalized, entry.name) + : entry.name; + const absolutePath = assertSafePath(PUBLIC_DIR, path.join(PUBLIC_DIR, relativePath)); + files.push({ + name: entry.name, + relativePath, + absolutePath, + isImage: /\.(png|jpe?g|webp|gif|svg)$/i.test(entry.name), + }); + } + + return files.sort((a, b) => a.name.localeCompare(b.name)); +} + +async function uploadMediaFiles(folder = "") { + const normalized = folder.trim().replace(/\\/g, "/").replace(/^\/+/, ""); + const targetDir = assertSafePath(PUBLIC_DIR, path.join(PUBLIC_DIR, normalized)); + await fs.mkdir(targetDir, { recursive: true }); + + const { canceled, filePaths } = await dialog.showOpenDialog({ + title: "Select image files", + properties: ["openFile", "multiSelections"], + filters: [ + { + name: "Images", + extensions: ["png", "jpg", "jpeg", "webp", "gif", "svg"], + }, + ], + }); + + if (canceled || !filePaths.length) { + return { uploaded: 0, files: [] }; + } + + const uploaded = []; + + for (const sourcePath of filePaths) { + const originalName = path.basename(sourcePath); + let finalName = originalName; + let counter = 1; + + while (true) { + const candidate = path.join(targetDir, finalName); + try { + await fs.access(candidate); + const ext = path.extname(originalName); + const base = path.basename(originalName, ext); + finalName = `${base}-${counter}${ext}`; + counter += 1; + } catch { + break; + } + } + + const destinationPath = path.join(targetDir, finalName); + await fs.copyFile(sourcePath, destinationPath); + + const relativePath = normalized + ? path.posix.join(normalized, finalName) + : finalName; + uploaded.push(relativePath); + } + + return { uploaded: uploaded.length, files: uploaded }; +} + +async function deleteMediaFile(relativePath) { + const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, ""); + if (!normalized) throw new Error("File path is required."); + const target = assertSafePath(PUBLIC_DIR, path.join(PUBLIC_DIR, normalized)); + await fs.unlink(target); + return { ok: true }; +} + +async function getPublishStatus() { + const { stdout } = await runGit(["status", "--short", "--", "data", "public", "content"]); + return stdout + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); +} + +function buildAutoMessage(files) { + const names = files + .map((item) => path.basename(item)) + .slice(0, 4) + .join(", "); + const now = new Date().toISOString().slice(0, 16).replace("T", " "); + return names ? `content: update ${names} (${now})` : `content: update data/public/content (${now})`; +} + +async function publishChanges(message = "") { + await runGit(["add", "data", "public", "content"]); + const staged = await runGit(["diff", "--cached", "--name-only"]); + const files = staged.stdout.split("\n").map((line) => line.trim()).filter(Boolean); + + if (!files.length) { + return { ok: false, message: "No staged changes in data/public/content." }; + } + + const commitMessage = message.trim() || buildAutoMessage(files); + await runGit(["commit", "-m", commitMessage]); + await runGit(["push"]); + const head = await runGit(["rev-parse", "--short", "HEAD"]); + + return { + ok: true, + message: `Published successfully (${head.stdout}).`, + commit: head.stdout, + commitMessage, + files, + }; +} + +ipcMain.handle("data:list", async () => listDataFiles()); +ipcMain.handle("data:read", async (_, fileName) => readDataFile(fileName)); +ipcMain.handle("data:saveRaw", async (_, payload) => saveRawDataFile(payload.fileName, payload.raw)); +ipcMain.handle("data:updateExport", async (_, payload) => updateDataExport(payload)); +ipcMain.handle("data:create", async (_, payload) => createDataFile(payload)); +ipcMain.handle("data:delete", async (_, fileName) => deleteDataFile(fileName)); + +ipcMain.handle("blog:list", async () => listBlogs()); +ipcMain.handle("blog:upsert", async (_, payload) => upsertBlog(payload)); +ipcMain.handle("blog:delete", async (_, slug) => deleteBlogBySlug(slug)); +ipcMain.handle("podcast:list", async (_, kind) => listPodcastEpisodes(kind)); +ipcMain.handle("podcast:upsert", async (_, payload) => upsertPodcastEpisode(payload.kind, payload)); +ipcMain.handle("podcast:delete", async (_, payload) => deletePodcastEpisode(payload.kind, payload.slug)); + +ipcMain.handle("media:listFolders", async () => listMediaFolders()); +ipcMain.handle("media:createFolder", async (_, folder) => createMediaFolder(folder)); +ipcMain.handle("media:listFiles", async (_, folder) => listMediaFiles(folder)); +ipcMain.handle("media:upload", async (_, folder) => uploadMediaFiles(folder)); +ipcMain.handle("media:deleteFile", async (_, relativePath) => deleteMediaFile(relativePath)); + +ipcMain.handle("publish:status", async () => getPublishStatus()); +ipcMain.handle("publish:run", async (_, message) => publishChanges(message)); + +app.whenReady().then(() => { + createWindow(); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); +}); diff --git a/apps/data-panel/package.json b/apps/data-panel/package.json new file mode 100644 index 0000000..9909113 --- /dev/null +++ b/apps/data-panel/package.json @@ -0,0 +1,12 @@ +{ + "name": "@portfolio/data-panel", + "version": "0.1.0", + "private": true, + "main": "main.cjs", + "scripts": { + "dev": "electron ." + }, + "devDependencies": { + "electron": "^33.2.0" + } +} diff --git a/apps/data-panel/preload.cjs b/apps/data-panel/preload.cjs new file mode 100644 index 0000000..64ad3b9 --- /dev/null +++ b/apps/data-panel/preload.cjs @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("panelAPI", { + data: { + list: () => ipcRenderer.invoke("data:list"), + read: (fileName) => ipcRenderer.invoke("data:read", fileName), + saveRaw: (payload) => ipcRenderer.invoke("data:saveRaw", payload), + updateExport: (payload) => ipcRenderer.invoke("data:updateExport", payload), + create: (payload) => ipcRenderer.invoke("data:create", payload), + delete: (fileName) => ipcRenderer.invoke("data:delete", fileName), + }, + blog: { + list: () => ipcRenderer.invoke("blog:list"), + upsert: (payload) => ipcRenderer.invoke("blog:upsert", payload), + delete: (slug) => ipcRenderer.invoke("blog:delete", slug), + }, + podcast: { + list: (kind) => ipcRenderer.invoke("podcast:list", kind), + upsert: (payload) => ipcRenderer.invoke("podcast:upsert", payload), + delete: (payload) => ipcRenderer.invoke("podcast:delete", payload), + }, + media: { + listFolders: () => ipcRenderer.invoke("media:listFolders"), + createFolder: (folder) => ipcRenderer.invoke("media:createFolder", folder), + listFiles: (folder) => ipcRenderer.invoke("media:listFiles", folder), + upload: (folder) => ipcRenderer.invoke("media:upload", folder), + deleteFile: (relativePath) => ipcRenderer.invoke("media:deleteFile", relativePath), + }, + publish: { + status: () => ipcRenderer.invoke("publish:status"), + run: (message) => ipcRenderer.invoke("publish:run", message), + }, +}); diff --git a/apps/data-panel/renderer/app.js b/apps/data-panel/renderer/app.js new file mode 100644 index 0000000..78df45a --- /dev/null +++ b/apps/data-panel/renderer/app.js @@ -0,0 +1,742 @@ + +import { COLLECTION_CONFIGS, PODCAST_LABELS } from "./configs.js"; + +const state = { + activeTab: "blog", + folders: [], + selectedFolder: "", + blogPosts: [], + selectedBlogSlug: "", + blogOriginalSlug: "", + collection: { + key: "announcement", + items: [], + selectedIndex: null, + editingIndex: null, + formInputs: {}, + }, + podcast: { + kind: "yazilim", + episodes: [], + selectedSlug: "", + originalSlug: "", + }, +}; + +const el = { + tabs: document.querySelectorAll(".tab-btn"), + panels: { + blog: document.getElementById("tab-blog"), + collection: document.getElementById("tab-collection"), + podcast: document.getElementById("tab-podcast"), + media: document.getElementById("tab-media"), + publish: document.getElementById("tab-publish"), + }, + blogCardList: document.getElementById("blog-card-list"), + refreshBlogFiles: document.getElementById("refresh-blog-files"), + newBlogFile: document.getElementById("new-blog-file"), + deleteBlogFile: document.getElementById("delete-blog-file"), + blogEditorTitle: document.getElementById("blog-editor-title"), + blogSlug: document.getElementById("blog-slug"), + blogTitle: document.getElementById("blog-title"), + blogCategory: document.getElementById("blog-category"), + blogDate: document.getElementById("blog-date"), + blogReadTime: document.getElementById("blog-read-time"), + blogAuthor: document.getElementById("blog-author"), + blogCoverImage: document.getElementById("blog-cover-image"), + blogExcerpt: document.getElementById("blog-excerpt"), + blogEditor: document.getElementById("blog-editor"), + saveBlogFile: document.getElementById("save-blog-file"), + collectionSidebarTitle: document.getElementById("collection-sidebar-title"), + collectionCardList: document.getElementById("collection-card-list"), + refreshCollection: document.getElementById("refresh-collection"), + newCollectionItem: document.getElementById("new-collection-item"), + deleteCollectionItem: document.getElementById("delete-collection-item"), + saveCollectionItem: document.getElementById("save-collection-item"), + collectionEditorTitle: document.getElementById("collection-editor-title"), + collectionHelper: document.getElementById("collection-helper"), + collectionFormGrid: document.getElementById("collection-form-grid"), + podcastSidebarTitle: document.getElementById("podcast-sidebar-title"), + podcastCardList: document.getElementById("podcast-card-list"), + refreshPodcastFiles: document.getElementById("refresh-podcast-files"), + newPodcastFile: document.getElementById("new-podcast-file"), + deletePodcastFile: document.getElementById("delete-podcast-file"), + podcastEditorTitle: document.getElementById("podcast-editor-title"), + podcastSlug: document.getElementById("podcast-slug"), + podcastTitle: document.getElementById("podcast-title"), + podcastDate: document.getElementById("podcast-date"), + podcastYoutubeUrl: document.getElementById("podcast-youtube-url"), + podcastSpotifyUrl: document.getElementById("podcast-spotify-url"), + podcastEditor: document.getElementById("podcast-editor"), + savePodcastFile: document.getElementById("save-podcast-file"), + folderSelect: document.getElementById("folder-select"), + newFolder: document.getElementById("new-folder"), + createFolder: document.getElementById("create-folder"), + uploadFile: document.getElementById("upload-file"), + refreshMedia: document.getElementById("refresh-media"), + mediaFiles: document.getElementById("media-files"), + refreshStatus: document.getElementById("refresh-status"), + commitMessage: document.getElementById("commit-message"), + publishBtn: document.getElementById("publish-btn"), + gitStatus: document.getElementById("git-status"), + publishLog: document.getElementById("publish-log"), +}; + +function notify(message) { + el.publishLog.textContent = message; +} + +function getCollectionConfig(key = state.collection.key) { + const config = COLLECTION_CONFIGS[key]; + if (!config) throw new Error(`Unknown collection config: ${key}`); + return config; +} + +function setActiveTab(tab, options = {}) { + state.activeTab = tab; + if (tab === "collection" && options.collectionKey) state.collection.key = options.collectionKey; + if (tab === "podcast" && options.podcastKind) state.podcast.kind = options.podcastKind; + + for (const button of el.tabs) { + const buttonTab = button.dataset.tab; + const isCollection = buttonTab === "collection"; + const isPodcast = buttonTab === "podcast"; + const isActive = isCollection + ? tab === "collection" && button.dataset.collectionKey === state.collection.key + : isPodcast + ? tab === "podcast" && button.dataset.podcastKind === state.podcast.kind + : buttonTab === tab; + button.classList.toggle("active", isActive); + } + + for (const [key, panel] of Object.entries(el.panels)) { + panel.classList.toggle("active", key === tab); + } + + if (tab === "collection") { + void loadCollection(state.collection.key).catch((error) => notify(String(error.message || error))); + } + + if (tab === "podcast") { + void loadPodcastFiles(state.podcast.kind).catch((error) => notify(String(error.message || error))); + } +} + +function emptyBlogDraft() { + return { + slug: "", + title: "", + category: "General", + date: "", + readTime: "", + author: "Poyraz Avsever", + excerpt: "", + coverImage: "/news/design.svg", + markdown: "", + }; +} + +function currentBlogDraft() { + return { + slug: el.blogSlug.value.trim(), + title: el.blogTitle.value.trim(), + category: el.blogCategory.value.trim(), + date: el.blogDate.value.trim(), + readTime: el.blogReadTime.value.trim(), + author: el.blogAuthor.value.trim(), + excerpt: el.blogExcerpt.value.trim(), + coverImage: el.blogCoverImage.value.trim(), + markdown: el.blogEditor.value, + }; +} + +function fillBlogForm(post) { + const draft = post || emptyBlogDraft(); + el.blogSlug.value = draft.slug || ""; + el.blogTitle.value = draft.title || ""; + el.blogCategory.value = draft.category || "General"; + el.blogDate.value = draft.date || ""; + el.blogReadTime.value = draft.readTime || ""; + el.blogAuthor.value = draft.author || "Poyraz Avsever"; + el.blogExcerpt.value = draft.excerpt || ""; + el.blogCoverImage.value = draft.coverImage || "/news/design.svg"; + el.blogEditor.value = draft.markdown || ""; +} + +function renderPostCards(root, items, selectedSlug, onEdit, onDelete) { + root.innerHTML = ""; + for (const item of items) { + const card = document.createElement("article"); + card.className = `blog-card ${selectedSlug === item.slug ? "active" : ""}`; + + const title = document.createElement("div"); + title.className = "title"; + title.textContent = item.title || item.slug; + + const meta = document.createElement("div"); + meta.className = "meta"; + meta.textContent = item.category ? `${item.category} - ${item.date || "-"}` : item.date || "-"; + + const slug = document.createElement("div"); + slug.className = "meta"; + slug.textContent = item.slug; + + const row = document.createElement("div"); + row.className = "row"; + + const editBtn = document.createElement("button"); + editBtn.className = "btn btn-ghost"; + editBtn.textContent = "Edit"; + editBtn.addEventListener("click", () => onEdit(item.slug)); + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "btn btn-danger"; + deleteBtn.textContent = "Delete"; + deleteBtn.addEventListener("click", () => onDelete(item.slug)); + + row.append(editBtn, deleteBtn); + card.append(title, meta, slug, row); + root.appendChild(card); + } +} + +async function loadBlogFiles() { + state.blogPosts = await window.panelAPI.blog.list(); + renderPostCards(el.blogCardList, state.blogPosts, state.selectedBlogSlug, selectBlog, (slug) => void removeBlogFile(slug)); + if (!state.selectedBlogSlug && state.blogPosts.length) selectBlog(state.blogPosts[0].slug); +} + +function selectBlog(slug) { + const post = state.blogPosts.find((item) => item.slug === slug); + if (!post) return; + state.selectedBlogSlug = slug; + state.blogOriginalSlug = slug; + el.blogEditorTitle.textContent = `Edit Blog: ${slug}`; + fillBlogForm(post); + renderPostCards(el.blogCardList, state.blogPosts, state.selectedBlogSlug, selectBlog, (value) => void removeBlogFile(value)); +} + +function createBlogFile() { + state.selectedBlogSlug = ""; + state.blogOriginalSlug = ""; + el.blogEditorTitle.textContent = "Create Blog Post"; + fillBlogForm(emptyBlogDraft()); + renderPostCards(el.blogCardList, state.blogPosts, state.selectedBlogSlug, selectBlog, (slug) => void removeBlogFile(slug)); +} + +async function saveBlogFile() { + const post = currentBlogDraft(); + if (!post.slug || !post.title) { + notify("Slug and title are required."); + return; + } + + const result = await window.panelAPI.blog.upsert({ + originalSlug: state.blogOriginalSlug || undefined, + post, + }); + + await loadBlogFiles(); + state.selectedBlogSlug = result.slug; + state.blogOriginalSlug = result.slug; + selectBlog(result.slug); + notify(`Saved blog post: ${result.slug}`); +} + +async function removeBlogFile(slugArg) { + const targetSlug = slugArg || state.selectedBlogSlug; + if (!targetSlug) return; + if (!window.confirm(`Delete blog post ${targetSlug}?`)) return; + + await window.panelAPI.blog.delete(targetSlug); + state.selectedBlogSlug = ""; + state.blogOriginalSlug = ""; + createBlogFile(); + await loadBlogFiles(); + notify("Blog post deleted."); +} + +function emptyPodcastDraft(kind = state.podcast.kind) { + return { slug: "", title: "", date: "", youtubeUrl: "", spotifyUrl: "", podcast: kind, markdown: "" }; +} + +function fillPodcastForm(episode) { + const draft = episode || emptyPodcastDraft(); + el.podcastSlug.value = draft.slug || ""; + el.podcastTitle.value = draft.title || ""; + el.podcastDate.value = draft.date || ""; + el.podcastYoutubeUrl.value = draft.youtubeUrl || ""; + el.podcastSpotifyUrl.value = draft.spotifyUrl || ""; + el.podcastEditor.value = draft.markdown || ""; +} + +function selectPodcast(slug) { + const episode = state.podcast.episodes.find((item) => item.slug === slug); + if (!episode) return; + state.podcast.selectedSlug = slug; + state.podcast.originalSlug = slug; + el.podcastEditorTitle.textContent = `Edit Episode: ${slug}`; + fillPodcastForm(episode); + renderPostCards(el.podcastCardList, state.podcast.episodes, state.podcast.selectedSlug, selectPodcast, (value) => void removePodcastFile(value)); +} +function createPodcastFile() { + state.podcast.selectedSlug = ""; + state.podcast.originalSlug = ""; + el.podcastEditorTitle.textContent = `Create Episode (${PODCAST_LABELS[state.podcast.kind] || state.podcast.kind})`; + fillPodcastForm(emptyPodcastDraft(state.podcast.kind)); + renderPostCards(el.podcastCardList, state.podcast.episodes, state.podcast.selectedSlug, selectPodcast, (slug) => void removePodcastFile(slug)); +} + +async function loadPodcastFiles(kind = state.podcast.kind) { + state.podcast.kind = kind; + state.podcast.episodes = await window.panelAPI.podcast.list(kind); + el.podcastSidebarTitle.textContent = PODCAST_LABELS[kind] || "Podcast"; + renderPostCards(el.podcastCardList, state.podcast.episodes, state.podcast.selectedSlug, selectPodcast, (value) => void removePodcastFile(value)); + + const selected = state.podcast.episodes.find((item) => item.slug === state.podcast.selectedSlug); + if (selected) { + selectPodcast(selected.slug); + } else if (state.podcast.episodes.length > 0) { + selectPodcast(state.podcast.episodes[0].slug); + } else { + createPodcastFile(); + } +} + +function currentPodcastDraft() { + return { + slug: el.podcastSlug.value.trim(), + title: el.podcastTitle.value.trim(), + date: el.podcastDate.value.trim(), + youtubeUrl: el.podcastYoutubeUrl.value.trim(), + spotifyUrl: el.podcastSpotifyUrl.value.trim(), + podcast: state.podcast.kind, + markdown: el.podcastEditor.value, + }; +} + +async function savePodcastFile() { + const episode = currentPodcastDraft(); + if (!episode.slug || !episode.title) { + notify("Slug and title are required."); + return; + } + + const result = await window.panelAPI.podcast.upsert({ + kind: state.podcast.kind, + originalSlug: state.podcast.originalSlug || undefined, + episode, + }); + + await loadPodcastFiles(state.podcast.kind); + state.podcast.selectedSlug = result.slug; + state.podcast.originalSlug = result.slug; + selectPodcast(result.slug); + notify(`Saved podcast episode: ${result.slug}`); +} + +async function removePodcastFile(slugArg) { + const targetSlug = slugArg || state.podcast.selectedSlug; + if (!targetSlug) return; + if (!window.confirm(`Delete podcast episode ${targetSlug}?`)) return; + + await window.panelAPI.podcast.delete({ kind: state.podcast.kind, slug: targetSlug }); + state.podcast.selectedSlug = ""; + state.podcast.originalSlug = ""; + createPodcastFile(); + await loadPodcastFiles(state.podcast.kind); + notify("Podcast episode deleted."); +} + +function deserializeCollectionItem(config, rawItem) { + if (typeof config.deserializeItem === "function") return config.deserializeItem(rawItem); + if (rawItem && typeof rawItem === "object" && !Array.isArray(rawItem)) return { ...rawItem }; + return {}; +} + +function serializeCollectionItem(config, item) { + if (typeof config.serializeItem === "function") return config.serializeItem(item); + return { ...item }; +} + +function emptyCollectionItem(config) { + const base = {}; + for (const field of config.fields) { + if (field.type !== "number") base[field.key] = field.defaultValue || ""; + } + return base; +} + +function renderCollectionHeader(config) { + el.collectionSidebarTitle.textContent = config.label; + el.collectionHelper.textContent = `${config.fileName} -> ${config.exportName}`; + el.newCollectionItem.textContent = `New ${config.itemLabel}`; +} + +function renderCollectionForm(config) { + state.collection.formInputs = {}; + el.collectionFormGrid.innerHTML = ""; + + for (const field of config.fields) { + const wrapper = document.createElement("div"); + if (field.full) wrapper.className = "full"; + + const label = document.createElement("label"); + const fieldId = `collection-${state.collection.key}-${field.key}`; + label.setAttribute("for", fieldId); + label.textContent = field.label; + + let input; + if (field.type === "textarea") { + input = document.createElement("textarea"); + input.className = "small-editor"; + input.spellcheck = false; + } else { + input = document.createElement("input"); + input.type = field.type === "number" ? "number" : field.type === "url" ? "url" : "text"; + } + + input.id = fieldId; + if (field.placeholder) input.placeholder = field.placeholder; + if (field.required) input.required = true; + if (field.type === "number") { + if (typeof field.min === "number") input.min = String(field.min); + if (typeof field.max === "number") input.max = String(field.max); + if (typeof field.step === "number") input.step = String(field.step); + } + + state.collection.formInputs[field.key] = input; + wrapper.append(label, input); + el.collectionFormGrid.appendChild(wrapper); + } +} + +function fillCollectionForm(config, item) { + const source = item || emptyCollectionItem(config); + for (const field of config.fields) { + const input = state.collection.formInputs[field.key]; + if (!input) continue; + const value = source[field.key]; + input.value = value === undefined || value === null ? "" : String(value); + } +} + +function readCollectionForm(config) { + const values = {}; + + for (const field of config.fields) { + const input = state.collection.formInputs[field.key]; + if (!input) continue; + + if (field.type === "number") { + const value = input.value.trim(); + if (!value) { + if (field.required) throw new Error(`${field.label} is required.`); + continue; + } + const parsed = Number(value); + if (Number.isNaN(parsed)) throw new Error(`${field.label} must be a number.`); + values[field.key] = parsed; + continue; + } + + const value = field.type === "textarea" ? input.value.trim() : input.value.trim(); + if (!value) { + if (field.required) throw new Error(`${field.label} is required.`); + continue; + } + values[field.key] = value; + } + + return values; +} + +function renderCollectionCards(config) { + el.collectionCardList.innerHTML = ""; + + state.collection.items.forEach((item, index) => { + const summary = typeof config.card === "function" + ? config.card(item, index) + : { title: item.title || item.id || `Item ${index + 1}`, meta: [] }; + + const card = document.createElement("article"); + card.className = `blog-card ${state.collection.selectedIndex === index ? "active" : ""}`; + + const title = document.createElement("div"); + title.className = "title"; + title.textContent = summary.title || `Item ${index + 1}`; + card.appendChild(title); + + for (const metaText of summary.meta || []) { + if (!metaText) continue; + const meta = document.createElement("div"); + meta.className = "meta"; + meta.textContent = String(metaText); + card.appendChild(meta); + } + + if (summary.footer) { + const footer = document.createElement("div"); + footer.className = "meta"; + footer.textContent = String(summary.footer); + card.appendChild(footer); + } + + const row = document.createElement("div"); + row.className = "row"; + + const editBtn = document.createElement("button"); + editBtn.className = "btn btn-ghost"; + editBtn.textContent = "Edit"; + editBtn.addEventListener("click", () => selectCollectionItem(index)); + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "btn btn-danger"; + deleteBtn.textContent = "Delete"; + deleteBtn.addEventListener("click", () => void removeCollectionItem(index)); + + row.append(editBtn, deleteBtn); + card.appendChild(row); + el.collectionCardList.appendChild(card); + }); +} + +function startCollectionCreateMode(config) { + state.collection.selectedIndex = null; + state.collection.editingIndex = null; + el.collectionEditorTitle.textContent = `Create ${config.itemLabel}`; + fillCollectionForm(config, emptyCollectionItem(config)); + renderCollectionCards(config); +} + +function selectCollectionItem(index) { + const config = getCollectionConfig(); + const item = state.collection.items[index]; + if (!item) return; + state.collection.selectedIndex = index; + state.collection.editingIndex = index; + el.collectionEditorTitle.textContent = `Edit ${config.itemLabel} #${index + 1}`; + fillCollectionForm(config, item); + renderCollectionCards(config); +} +async function loadCollection(key = state.collection.key, options = {}) { + const config = getCollectionConfig(key); + const file = await window.panelAPI.data.read(config.fileName); + const targetExport = file.exports.find((entry) => entry.name === config.exportName); + + if (!targetExport) throw new Error(`Export ${config.exportName} not found in ${config.fileName}.`); + if (targetExport.parseError) throw new Error(targetExport.parseError); + if (!Array.isArray(targetExport.value)) throw new Error(`Export ${config.exportName} must be array.`); + + state.collection.key = key; + state.collection.items = targetExport.value.map((item) => deserializeCollectionItem(config, item)); + + renderCollectionHeader(config); + renderCollectionForm(config); + + let selectedIndex = null; + if (typeof options.selectIndex === "number") { + selectedIndex = options.selectIndex; + } else if (options.keepSelection && typeof state.collection.selectedIndex === "number") { + selectedIndex = state.collection.selectedIndex; + } + + if (typeof selectedIndex === "number" && selectedIndex >= 0 && selectedIndex < state.collection.items.length) { + state.collection.selectedIndex = selectedIndex; + state.collection.editingIndex = selectedIndex; + el.collectionEditorTitle.textContent = `Edit ${config.itemLabel} #${selectedIndex + 1}`; + fillCollectionForm(config, state.collection.items[selectedIndex]); + renderCollectionCards(config); + } else { + startCollectionCreateMode(config); + } +} + +async function saveCollectionItem() { + const config = getCollectionConfig(); + const draft = readCollectionForm(config); + + const next = [...state.collection.items]; + let selectedIndex; + + if (typeof state.collection.editingIndex === "number") { + selectedIndex = state.collection.editingIndex; + next[selectedIndex] = draft; + } else { + next.push(draft); + selectedIndex = next.length - 1; + } + + await window.panelAPI.data.updateExport({ + fileName: config.fileName, + exportName: config.exportName, + value: next.map((item) => serializeCollectionItem(config, item)), + }); + + await loadCollection(state.collection.key, { selectIndex: selectedIndex }); + notify(`Saved ${config.itemLabel.toLowerCase()} in ${config.label}.`); +} + +async function removeCollectionItem(indexArg) { + const config = getCollectionConfig(); + const targetIndex = typeof indexArg === "number" ? indexArg : state.collection.selectedIndex; + if (typeof targetIndex !== "number" || !state.collection.items[targetIndex]) return; + if (!window.confirm(`Delete ${config.itemLabel} #${targetIndex + 1}?`)) return; + + const next = state.collection.items.filter((_, index) => index !== targetIndex); + await window.panelAPI.data.updateExport({ + fileName: config.fileName, + exportName: config.exportName, + value: next.map((item) => serializeCollectionItem(config, item)), + }); + + const selectedIndex = next.length ? Math.max(0, targetIndex - 1) : undefined; + await loadCollection(state.collection.key, { selectIndex: selectedIndex }); + notify(`${config.itemLabel} deleted from ${config.label}.`); +} + +async function loadMediaFolders() { + state.folders = await window.panelAPI.media.listFolders(); + el.folderSelect.innerHTML = ""; + + for (const folder of state.folders) { + const option = document.createElement("option"); + option.value = folder; + option.textContent = folder || "/"; + el.folderSelect.appendChild(option); + } + + if (!state.selectedFolder || !state.folders.includes(state.selectedFolder)) { + state.selectedFolder = state.folders[0] ?? ""; + } + el.folderSelect.value = state.selectedFolder; + await loadMediaFiles(); +} + +async function loadMediaFiles() { + const files = await window.panelAPI.media.listFiles(state.selectedFolder); + el.mediaFiles.innerHTML = ""; + + for (const file of files) { + const item = document.createElement("article"); + item.className = "media-item"; + + if (file.isImage) { + const img = document.createElement("img"); + img.src = `file:///${file.absolutePath.replaceAll("\\", "/")}`; + img.alt = file.name; + item.appendChild(img); + } + + const meta = document.createElement("div"); + meta.className = "meta"; + + const name = document.createElement("div"); + name.className = "name"; + name.textContent = file.relativePath; + + const del = document.createElement("button"); + del.className = "btn btn-danger"; + del.textContent = "Delete"; + del.addEventListener("click", async () => { + if (!window.confirm(`Delete ${file.relativePath}?`)) return; + await window.panelAPI.media.deleteFile(file.relativePath); + await loadMediaFiles(); + notify(`Deleted: ${file.relativePath}`); + }); + + meta.append(name, del); + item.appendChild(meta); + el.mediaFiles.appendChild(item); + } +} + +async function createFolder() { + const folder = el.newFolder.value.trim(); + if (!folder) return; + await window.panelAPI.media.createFolder(folder); + el.newFolder.value = ""; + await loadMediaFolders(); + state.selectedFolder = folder.replaceAll("\\", "/").replace(/^\/+/, ""); + el.folderSelect.value = state.selectedFolder; + await loadMediaFiles(); +} + +async function uploadFiles() { + await window.panelAPI.media.upload(state.selectedFolder); + await loadMediaFiles(); + notify("Upload completed."); +} + +async function refreshPublishStatus() { + const status = await window.panelAPI.publish.status(); + el.gitStatus.textContent = status.length + ? status.join("\n") + : "No pending changes in data/, public/ or content/."; +} + +async function publishNow() { + const result = await window.panelAPI.publish.run(el.commitMessage.value); + el.publishLog.textContent = JSON.stringify(result, null, 2); + await refreshPublishStatus(); +} + +function bindEvents() { + for (const button of el.tabs) { + button.addEventListener("click", () => { + const tab = button.dataset.tab; + if (tab === "collection") { + setActiveTab("collection", { collectionKey: button.dataset.collectionKey }); + return; + } + if (tab === "podcast") { + setActiveTab("podcast", { podcastKind: button.dataset.podcastKind }); + return; + } + setActiveTab(tab); + }); + } + + el.refreshBlogFiles.addEventListener("click", () => void loadBlogFiles()); + el.newBlogFile.addEventListener("click", () => createBlogFile()); + el.deleteBlogFile.addEventListener("click", () => void removeBlogFile().catch((error) => notify(String(error.message || error)))); + el.saveBlogFile.addEventListener("click", () => void saveBlogFile().catch((error) => notify(String(error.message || error)))); + + el.refreshCollection.addEventListener("click", () => + void loadCollection(state.collection.key, { keepSelection: true }).catch((error) => notify(String(error.message || error))) + ); + el.newCollectionItem.addEventListener("click", () => startCollectionCreateMode(getCollectionConfig())); + el.deleteCollectionItem.addEventListener("click", () => void removeCollectionItem().catch((error) => notify(String(error.message || error)))); + el.saveCollectionItem.addEventListener("click", () => void saveCollectionItem().catch((error) => notify(String(error.message || error)))); + + el.refreshPodcastFiles.addEventListener("click", () => void loadPodcastFiles(state.podcast.kind).catch((error) => notify(String(error.message || error)))); + el.newPodcastFile.addEventListener("click", () => createPodcastFile()); + el.deletePodcastFile.addEventListener("click", () => void removePodcastFile().catch((error) => notify(String(error.message || error)))); + el.savePodcastFile.addEventListener("click", () => void savePodcastFile().catch((error) => notify(String(error.message || error)))); + + el.folderSelect.addEventListener("change", () => { + state.selectedFolder = el.folderSelect.value; + void loadMediaFiles(); + }); + el.refreshMedia.addEventListener("click", () => void loadMediaFolders()); + el.createFolder.addEventListener("click", () => void createFolder().catch((error) => notify(String(error.message || error)))); + el.uploadFile.addEventListener("click", () => void uploadFiles().catch((error) => notify(String(error.message || error)))); + + el.refreshStatus.addEventListener("click", () => void refreshPublishStatus()); + el.publishBtn.addEventListener("click", () => void publishNow().catch((error) => notify(String(error.message || error)))); +} + +async function init() { + bindEvents(); + await loadBlogFiles(); + if (!state.selectedBlogSlug) createBlogFile(); + await loadCollection(state.collection.key); + await loadPodcastFiles(state.podcast.kind); + await loadMediaFolders(); + await refreshPublishStatus(); + setActiveTab("blog"); +} + +void init().catch((error) => { + notify(`Initialization error: ${String(error.message || error)}`); +}); diff --git a/apps/data-panel/renderer/configs.js b/apps/data-panel/renderer/configs.js new file mode 100644 index 0000000..a48dd61 --- /dev/null +++ b/apps/data-panel/renderer/configs.js @@ -0,0 +1,206 @@ + +export const COLLECTION_CONFIGS = { + announcement: { + label: "Announcement", + itemLabel: "Announcement", + fileName: "site-settings.ts", + exportName: "ANNOUNCEMENT_ITEMS", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "main-announcement" }, + { key: "text", label: "Text", type: "textarea", full: true, required: true, placeholder: "Bu hafta yeni bilesenler eklendi!" }, + { key: "actionLabel", label: "Action Label", required: true, placeholder: "Incele ->" }, + { key: "actionHref", label: "Action Link", type: "url", required: true, placeholder: "https://..." }, + ], + card: (item) => ({ + title: item.id || "Announcement", + meta: [item.actionLabel, item.actionHref], + footer: item.text, + }), + }, + bookmarks: { + label: "Bookmarks", + itemLabel: "Bookmark", + fileName: "bookmarks.ts", + exportName: "BOOKMARKS", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "bookmark-poyraz-ui" }, + { key: "title", label: "Title", required: true, placeholder: "Poyraz UI" }, + { key: "href", label: "Link", type: "url", required: true, placeholder: "https://..." }, + { key: "tag", label: "Tag", required: true, placeholder: "UI Kit" }, + { key: "description", label: "Description", type: "textarea", full: true, required: true, placeholder: "Short description" }, + ], + card: (item) => ({ + title: item.title || item.id || "Untitled bookmark", + meta: [item.tag, item.href], + footer: item.description, + }), + }, + certificates: { + label: "Certificates", + itemLabel: "Certificate", + fileName: "certificates.ts", + exportName: "certificates", + fields: [ + { key: "name", label: "Name", required: true, placeholder: "Certificate name" }, + { key: "organization", label: "Organization", required: true, placeholder: "Udemy" }, + { key: "date", label: "Date", required: true, placeholder: "June 2025" }, + { key: "category", label: "Category", required: true, placeholder: "mobile" }, + { key: "image", label: "Image Path", required: true, placeholder: "/certificates/reactnative.png" }, + { key: "description", label: "Description", type: "textarea", full: true, required: true, placeholder: "What this certificate represents" }, + ], + card: (item) => ({ + title: item.name || "Untitled certificate", + meta: [item.organization, item.date, item.category], + footer: item.image, + }), + }, + education: { + label: "Education", + itemLabel: "Education Item", + fileName: "education.ts", + exportName: "EDUCATION", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "ostim-university" }, + { key: "title", label: "Title", required: true, placeholder: "Software Engineering - 2nd Year Student" }, + { key: "institution", label: "Institution", required: true, placeholder: "OSTIM Technical University" }, + { key: "period", label: "Period", required: true, placeholder: "2024 - Present" }, + { key: "description", label: "Description", type: "textarea", full: true, required: true, placeholder: "Education summary" }, + ], + card: (item) => ({ + title: item.title || item.id || "Education", + meta: [item.institution, item.period], + footer: item.description, + }), + }, + experience: { + label: "Experience", + itemLabel: "Experience Item", + fileName: "experience.ts", + exportName: "EXPERIENCE", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "omedya-part-time" }, + { key: "role", label: "Role", required: true, placeholder: "Fullstack Developer" }, + { key: "company", label: "Company", required: true, placeholder: "Omedya Bilisim A.S" }, + { key: "period", label: "Period", required: true, placeholder: "August 2025 - Present" }, + ], + card: (item) => ({ + title: item.role || item.id || "Experience", + meta: [item.company, item.period], + }), + }, + references: { + label: "References", + itemLabel: "Reference", + fileName: "references.ts", + exportName: "REFERENCES", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "ali-korkmaz" }, + { key: "author", label: "Author", required: true, placeholder: "Ali Korkmaz" }, + { key: "role", label: "Role", required: true, placeholder: "Musteri - 2025" }, + { key: "avatar", label: "Avatar Path", required: true, placeholder: "/avatars/ali.png" }, + { key: "rating", label: "Rating", type: "number", min: 1, max: 5, step: 1, placeholder: "5" }, + { key: "profileHref", label: "Profile Link", type: "url", placeholder: "https://linkedin.com/in/..." }, + { key: "quote", label: "Quote", type: "textarea", full: true, required: true, placeholder: "Reference quote" }, + ], + card: (item) => ({ + title: item.author || item.id || "Reference", + meta: [item.role, item.rating ? `Rating: ${item.rating}` : ""], + footer: item.quote, + }), + }, + projectsMobile: { + label: "Projects Mobile", + itemLabel: "Mobile Project", + fileName: "projects.ts", + exportName: "MOBILE_APPS", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "mobile-habit-flow" }, + { key: "title", label: "Title", required: true, placeholder: "Habit Flow" }, + { key: "badge", label: "Badge", placeholder: "React Native" }, + { key: "image", label: "Image Path", required: true, placeholder: "/images/hero1.png" }, + { key: "href", label: "Link", type: "url", placeholder: "https://..." }, + { key: "description", label: "Description", type: "textarea", full: true, required: true, placeholder: "Project summary" }, + ], + card: (item) => ({ + title: item.title || item.id || "Project", + meta: [item.badge, item.href], + footer: item.description, + }), + }, + projectsWeb: { + label: "Projects Web", + itemLabel: "Web Project", + fileName: "projects.ts", + exportName: "WEB_APPS", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "web-portfolio" }, + { key: "title", label: "Title", required: true, placeholder: "Personal Portfolio" }, + { key: "badge", label: "Badge", placeholder: "Next.js" }, + { key: "image", label: "Image Path", required: true, placeholder: "/news/design.svg" }, + { key: "href", label: "Link", type: "url", placeholder: "https://..." }, + { key: "description", label: "Description", type: "textarea", full: true, required: true, placeholder: "Project summary" }, + ], + card: (item) => ({ + title: item.title || item.id || "Project", + meta: [item.badge, item.href], + footer: item.description, + }), + }, + projectsFigma: { + label: "Projects Figma", + itemLabel: "Figma Template", + fileName: "projects.ts", + exportName: "FIGMA_TEMPLATES", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "figma-minimal-saas" }, + { key: "title", label: "Title", required: true, placeholder: "Minimal SaaS Landing Kit" }, + { key: "badge", label: "Badge", placeholder: "Figma" }, + { key: "image", label: "Image Path", required: true, placeholder: "/news/design.svg" }, + { key: "href", label: "Link", type: "url", placeholder: "https://..." }, + { key: "description", label: "Description", type: "textarea", full: true, required: true, placeholder: "Project summary" }, + ], + card: (item) => ({ + title: item.title || item.id || "Project", + meta: [item.badge, item.href], + footer: item.description, + }), + }, + community: { + label: "Community", + itemLabel: "Community Item", + fileName: "volunteer-community.ts", + exportName: "VOLUNTEER_COMMUNITY_ITEMS", + fields: [ + { key: "id", label: "ID", required: true, placeholder: "youtube" }, + { key: "title", label: "Title", required: true, placeholder: "YouTube" }, + { key: "timeline", label: "Timeline", required: true, placeholder: "2025 - Present" }, + { key: "link", label: "Link", type: "url", placeholder: "https://..." }, + { key: "focus", label: "Focus", type: "textarea", full: true, required: true, placeholder: "Community contribution summary" }, + ], + card: (item) => ({ + title: item.title || item.id || "Community", + meta: [item.timeline, item.link], + footer: item.focus, + }), + }, + youtube: { + label: "YouTube Videos", + itemLabel: "Video Link", + fileName: "youtube-videos.ts", + exportName: "YOUTUBE_VIDEO_LINKS", + fields: [ + { key: "url", label: "Video URL", type: "url", required: true, placeholder: "https://www.youtube.com/watch?v=..." }, + ], + deserializeItem: (raw) => ({ url: String(raw || "") }), + serializeItem: (item) => String(item.url || "").trim(), + card: (item) => ({ + title: item.url || "YouTube Link", + meta: [], + }), + }, +}; + +export const PODCAST_LABELS = { + yazilim: "Podcast Yazilim", + "masa-basi": "Podcast Masa Basi", +}; diff --git a/apps/data-panel/renderer/index.html b/apps/data-panel/renderer/index.html new file mode 100644 index 0000000..8edf16a --- /dev/null +++ b/apps/data-panel/renderer/index.html @@ -0,0 +1,231 @@ + + + + + + Portfolio Data Panel + + + +
+
+

Portfolio Data Panel

+

Manage blog posts, dashboard collections, media files, and publish updates.

+
+
Electron Monorepo
+
+ + + +
+
+
+ + +
+
+

Create Blog Post

+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+ + +
+
+

Create Item

+
+ + +
+
+ +

Fill inputs and save to update the related data export.

+
+
+
+
+ +
+
+ + +
+
+

Create Episode

+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+
+

Public Folders

+ +
+ + + + +
+ + +
+ +
+ +
+
+ +
+

Files

+
+
+
+
+ +
+
+
+

Publish Changes

+ +
+ +

This will stage data/, public/ and content/, create commit, and push.

+ + + + +
+ +
+ +

Git Status

+

+
+          

Publish Log

+

+        
+
+
+ + + + diff --git a/apps/data-panel/renderer/styles.css b/apps/data-panel/renderer/styles.css new file mode 100644 index 0000000..03c8569 --- /dev/null +++ b/apps/data-panel/renderer/styles.css @@ -0,0 +1,332 @@ +:root { + --bg: #f4f5f7; + --card: #ffffff; + --muted: #6b7280; + --border: #d8dbe2; + --text: #111827; + --red: #dc2626; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Segoe UI", Arial, sans-serif; + background: var(--bg); + color: var(--text); +} + +.topbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 14px 16px; + border-bottom: 1px solid var(--border); + background: #fff; +} + +.topbar h1 { + margin: 0; + font-size: 20px; +} + +.topbar p { + margin: 4px 0 0; + color: var(--muted); + font-size: 13px; +} + +.chip { + border: 1px solid var(--border); + background: #fff; + padding: 6px 10px; + border-radius: 6px; + font-size: 12px; +} + +.tabs { + display: flex; + gap: 8px; + padding: 10px 16px; + overflow-x: auto; + scrollbar-width: thin; +} + +.tab-btn { + border: 1px solid var(--border); + background: #fff; + border-radius: 6px; + padding: 6px 12px; + cursor: pointer; + white-space: nowrap; + flex: 0 0 auto; +} + +.tab-btn.active { + border-color: var(--text); + font-weight: 600; +} + +main { + padding: 0 16px 16px; +} + +.tab-panel { + display: none; +} + +.tab-panel.active { + display: block; +} + +.split { + display: grid; + gap: 12px; + grid-template-columns: 280px 1fr; +} + +.card { + background: var(--card); + border: 1px solid var(--border); + border-radius: 8px; + padding: 12px; +} + +.card.soft { + background: #fafafa; +} + +.sidebar { + display: flex; + flex-direction: column; + min-height: 680px; +} + +.sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.file-list { + list-style: none; + padding: 0; + margin: 10px 0; + display: grid; + gap: 4px; + max-height: 560px; + overflow: auto; +} + +.file-list button { + width: 100%; + text-align: left; + border: 1px solid var(--border); + background: #fff; + border-radius: 6px; + padding: 8px; + cursor: pointer; +} + +.file-list button.active { + border-color: var(--text); + background: #f7f7f7; + font-weight: 600; +} + +.blog-card-list { + display: grid; + gap: 8px; + max-height: 620px; + overflow: auto; +} + +.blog-card { + border: 1px solid var(--border); + border-radius: 6px; + padding: 8px; + display: grid; + gap: 4px; + background: #fff; +} + +.blog-card.active { + border-color: var(--text); + background: #f7f7f7; +} + +.blog-card .title { + font-size: 13px; + font-weight: 600; +} + +.blog-card .meta { + font-size: 11px; + color: var(--muted); + word-break: break-word; +} + +.blog-form-grid { + display: grid; + gap: 10px; + grid-template-columns: 1fr 1fr; +} + +.blog-form-grid .full { + grid-column: 1 / -1; +} + +.editor { + display: grid; + gap: 10px; +} + +.editor-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.row { + display: flex; + align-items: center; + gap: 8px; +} + +.row.between { + justify-content: space-between; +} + +.mt-8 { + margin-top: 8px; +} + +label { + font-size: 12px; + color: var(--muted); +} + +textarea, +select, +input { + width: 100%; + border: 1px solid var(--border); + border-radius: 6px; + padding: 8px; + font: inherit; +} + +textarea { + min-height: 340px; + font-family: Consolas, monospace; + font-size: 12px; + resize: vertical; +} + +.small-editor { + min-height: 160px; +} + +.structured { + display: grid; + gap: 8px; + margin-top: 8px; +} + +.hidden { + display: none; +} + +.btn { + border: 1px solid var(--text); + background: var(--text); + color: #fff; + border-radius: 6px; + padding: 6px 10px; + cursor: pointer; + font-size: 12px; +} + +.btn-ghost { + background: #fff; + color: var(--text); + border-color: var(--border); +} + +.btn-danger { + background: #fff; + color: var(--red); + border-color: #f3b3b3; +} + +.hint { + color: var(--muted); + font-size: 12px; + margin: 6px 0; +} + +.media-grid { + display: grid; + gap: 8px; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + max-height: 680px; + overflow: auto; +} + +.media-item { + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; + background: #fff; +} + +.media-item img { + width: 100%; + height: 96px; + object-fit: cover; + display: block; +} + +.media-item .meta { + padding: 6px; + display: grid; + gap: 6px; +} + +.media-item .name { + font-size: 11px; + word-break: break-all; +} + +.publish-card { + display: grid; + gap: 8px; +} + +.log { + border: 1px solid var(--border); + background: #f9fafb; + padding: 8px; + border-radius: 6px; + min-height: 80px; + max-height: 220px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + font-size: 12px; +} + +@media (max-width: 980px) { + .split { + grid-template-columns: 1fr; + } + + .sidebar { + min-height: auto; + } +} diff --git a/package.json b/package.json index 76ffa64..7ee896c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "panel:dev": "pnpm --filter @portfolio/data-panel dev", + "panel:start": "pnpm --filter @portfolio/data-panel dev" }, "dependencies": { "@iconify/react": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e792f91..ed1e44a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,12 @@ importers: specifier: ^5 version: 5.9.3 + apps/data-panel: + devDependencies: + electron: + specifier: ^33.2.0 + version: 33.4.11 + packages: '@alloc/quick-lru@5.2.0': @@ -168,6 +174,10 @@ packages: '@chevrotain/utils@11.1.2': resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==} + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -1098,9 +1108,17 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + '@tailwindcss/node@4.2.1': resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} @@ -1192,6 +1210,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1300,12 +1321,18 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1326,6 +1353,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1335,6 +1365,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.56.1': resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1592,6 +1625,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1608,6 +1645,17 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1660,6 +1708,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1898,9 +1949,17 @@ packages: decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1923,6 +1982,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1941,13 +2003,25 @@ packages: electron-to-chromium@1.5.307: resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + electron@33.4.11: + resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==} + engines: {node: '>= 12.20.55'} + hasBin: true + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.20.0: resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -1980,6 +2054,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2127,6 +2204,11 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2146,6 +2228,9 @@ packages: fault@1.0.4: resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2182,6 +2267,10 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2212,6 +2301,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -2227,6 +2320,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2243,6 +2340,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2307,6 +2408,13 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2503,6 +2611,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -2512,6 +2623,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -2638,6 +2752,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} @@ -2660,6 +2778,10 @@ packages: engines: {node: '>= 20'} hasBin: true + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2804,6 +2926,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -2864,6 +2994,10 @@ packages: node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2896,6 +3030,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2904,6 +3041,10 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2943,6 +3084,9 @@ packages: resolution: {integrity: sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==} engines: {node: '>=20.19.0 || >=22.13.0 || >=24'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3004,12 +3148,19 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3017,6 +3168,10 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -3100,6 +3255,9 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3117,10 +3275,17 @@ packages: engines: {node: '>= 0.4'} hasBin: true + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -3155,6 +3320,9 @@ packages: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3164,6 +3332,10 @@ packages: engines: {node: '>=10'} hasBin: true + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3220,6 +3392,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -3287,6 +3462,10 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3343,6 +3522,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -3399,6 +3582,10 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -3497,9 +3684,15 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3646,6 +3839,20 @@ snapshots: '@chevrotain/utils@11.1.2': {} + '@electron/get@2.0.3': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -4527,10 +4734,16 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@sindresorhus/is@4.6.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + '@tailwindcss/node@4.2.1': dependencies: '@jridgewell/remapping': 2.3.5 @@ -4605,6 +4818,13 @@ snapshots: tslib: 2.8.1 optional: true + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 20.19.37 + '@types/responselike': 1.0.3 + '@types/d3-array@3.2.2': {} '@types/d3-axis@3.0.6': @@ -4738,10 +4958,16 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/http-cache-semantics@4.2.0': {} + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/keyv@3.1.4': + dependencies: + '@types/node': 20.19.37 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -4762,6 +4988,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/responselike@1.0.3': + dependencies: + '@types/node': 20.19.37 + '@types/trusted-types@2.0.7': optional: true @@ -4769,6 +4999,11 @@ snapshots: '@types/unist@3.0.3': {} + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 20.19.37 + optional: true + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5042,6 +5277,9 @@ snapshots: baseline-browser-mapping@2.10.0: {} + boolean@3.2.0: + optional: true + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5063,6 +5301,20 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-crc32@0.2.13: {} + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5119,6 +5371,10 @@ snapshots: client-only@0.0.1: {} + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + clsx@2.1.1: {} color-convert@2.0.1: @@ -5373,8 +5629,14 @@ snapshots: dependencies: character-entities: 2.0.2 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + deep-is@0.1.4: {} + defer-to-connect@2.0.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -5397,6 +5659,9 @@ snapshots: detect-node-es@1.1.0: {} + detect-node@2.1.0: + optional: true + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -5417,13 +5682,27 @@ snapshots: electron-to-chromium@1.5.307: {} + electron@33.4.11: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 20.19.37 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.20.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 + env-paths@2.2.1: {} + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -5525,6 +5804,9 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es6-error@4.1.1: + optional: true + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -5746,6 +6028,16 @@ snapshots: extend@3.0.2: {} + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -5768,6 +6060,10 @@ snapshots: dependencies: format: 0.2.2 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -5798,6 +6094,12 @@ snapshots: format@0.2.2: {} + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -5835,6 +6137,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -5853,6 +6159,16 @@ snapshots: dependencies: is-glob: 4.0.3 + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.4 + serialize-error: 7.0.1 + optional: true + globals@14.0.0: {} globals@16.4.0: {} @@ -5864,6 +6180,20 @@ snapshots: gopd@1.2.0: {} + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + graceful-fs@4.2.11: {} gray-matter@4.0.3: @@ -5945,6 +6275,13 @@ snapshots: html-url-attributes@3.0.1: {} + http-cache-semantics@4.2.0: {} + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -6133,12 +6470,19 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: + optional: true + json5@1.0.2: dependencies: minimist: 1.2.8 json5@2.2.3: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -6244,6 +6588,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lowercase-keys@2.0.0: {} + lowlight@1.20.0: dependencies: fault: 1.0.4 @@ -6265,6 +6611,11 @@ snapshots: marked@16.4.2: {} + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -6642,6 +6993,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + minimatch@10.2.4: dependencies: brace-expansion: 5.0.4 @@ -6703,6 +7058,8 @@ snapshots: node-releases@2.0.36: {} + normalize-url@6.1.0: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -6745,6 +7102,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6760,6 +7121,8 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-cancelable@2.1.1: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -6799,6 +7162,8 @@ snapshots: '@napi-rs/canvas': 0.1.96 node-readable-to-web-readable-stream: 0.4.2 + pend@1.2.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -6871,6 +7236,8 @@ snapshots: prismjs@1.30.0: {} + progress@2.0.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -6879,10 +7246,17 @@ snapshots: property-information@7.1.0: {} + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} queue-microtask@1.2.3: {} + quick-lru@5.1.1: {} + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -7012,6 +7386,8 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + resolve-alpn@1.2.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -7031,8 +7407,22 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + reusify@1.1.0: {} + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + robust-predicates@3.0.2: {} roughjs@4.6.6: @@ -7076,10 +7466,18 @@ snapshots: extend-shallow: 2.0.1 kind-of: 6.0.3 + semver-compare@1.0.0: + optional: true + semver@6.3.1: {} semver@7.7.4: {} + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -7179,6 +7577,9 @@ snapshots: sprintf-js@1.0.3: {} + sprintf-js@1.1.3: + optional: true + stable-hash@0.0.5: {} stop-iteration-iterator@1.1.0: @@ -7264,6 +7665,12 @@ snapshots: stylis@4.3.6: {} + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -7310,6 +7717,9 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.13.1: + optional: true + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -7400,6 +7810,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + universalify@0.1.2: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -7538,8 +7950,15 @@ snapshots: word-wrap@1.2.5: {} + wrappy@1.0.2: {} + yallist@3.1.1: {} + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@4.3.6): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 581a9d5..78b9aa1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,10 @@ +packages: + - . + - apps/* + +onlyBuiltDependencies: + - electron + ignoredBuiltDependencies: - sharp - unrs-resolver