diff --git a/app/snippets/page.tsx b/app/snippets/page.tsx new file mode 100644 index 0000000..40b8c3b --- /dev/null +++ b/app/snippets/page.tsx @@ -0,0 +1,9 @@ +import { SnippetsContent } from "@/components/snippets-content"; +import { listSnippets } from "@/data/snippets"; + +export default async function SnippetsPage() { + const snippets = await listSnippets(); + const categories = ["All", ...new Set(snippets.map((s) => s.category))]; + + return ; +} diff --git a/apps/data-panel/main.cjs b/apps/data-panel/main.cjs index 1aefd7a..989c9cc 100644 --- a/apps/data-panel/main.cjs +++ b/apps/data-panel/main.cjs @@ -35,6 +35,7 @@ let DATA_DIR = path.join(WORKSPACE_ROOT, "data"); let PUBLIC_DIR = path.join(WORKSPACE_ROOT, "public"); let BLOG_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "blog"); let PODCAST_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "podcasts"); +let SNIPPETS_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "snippets"); function setWorkspaceRoot(nextRoot) { WORKSPACE_ROOT = path.resolve(nextRoot); @@ -42,6 +43,7 @@ function setWorkspaceRoot(nextRoot) { PUBLIC_DIR = path.join(WORKSPACE_ROOT, "public"); BLOG_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "blog"); PODCAST_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "podcasts"); + SNIPPETS_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "snippets"); } async function pathExists(target) { @@ -59,6 +61,7 @@ async function ensureWorkspaceStructure() { { label: "public", target: PUBLIC_DIR }, { label: "content/blog", target: BLOG_CONTENT_DIR }, { label: "content/podcasts", target: PODCAST_CONTENT_DIR }, + { label: "content/snippets", target: SNIPPETS_CONTENT_DIR }, ]; const missing = []; @@ -582,6 +585,84 @@ async function deletePodcastEpisode(kind, slug) { return { ok: true }; } +function mapMarkdownToSnippet(fileName, raw) { + const parsed = matter(raw); + const slug = fileName.replace(/\.md$/i, ""); + return { + slug, + title: String(parsed.data.title || slug), + language: String(parsed.data.language || "text"), + category: String(parsed.data.category || "General"), + description: String(parsed.data.description || ""), + markdown: String(parsed.content || "").trim(), + }; +} + +async function listSnippets() { + await fs.mkdir(SNIPPETS_CONTENT_DIR, { recursive: true }); + const files = await fs.readdir(SNIPPETS_CONTENT_DIR, { withFileTypes: true }); + const mdFiles = files + .filter((item) => item.isFile() && item.name.endsWith(".md")) + .map((item) => item.name) + .sort((a, b) => a.localeCompare(b)); + + const snippets = await Promise.all( + mdFiles.map(async (fileName) => { + const target = assertSafePath(SNIPPETS_CONTENT_DIR, path.join(SNIPPETS_CONTENT_DIR, fileName)); + const raw = await fs.readFile(target, "utf8"); + return mapMarkdownToSnippet(fileName, raw); + }), + ); + + return snippets; +} + +async function upsertSnippet({ originalSlug, snippet }) { + if (!snippet || typeof snippet !== "object") { + throw new Error("Snippet payload is invalid."); + } + + const slug = validateBlogSlug(snippet.slug); + const previousSlug = originalSlug ? validateBlogSlug(originalSlug) : null; + + const frontmatter = { + title: String(snippet.title || slug), + language: String(snippet.language || "text"), + category: String(snippet.category || "General"), + description: String(snippet.description || ""), + }; + + const markdownBody = String(snippet.markdown || "").trim(); + const raw = matter.stringify(markdownBody ? `${markdownBody}\n` : "", frontmatter); + + await fs.mkdir(SNIPPETS_CONTENT_DIR, { recursive: true }); + + const nextPath = assertSafePath(SNIPPETS_CONTENT_DIR, path.join(SNIPPETS_CONTENT_DIR, `${slug}.md`)); + const previousPath = + previousSlug && previousSlug !== slug + ? assertSafePath(SNIPPETS_CONTENT_DIR, path.join(SNIPPETS_CONTENT_DIR, `${previousSlug}.md`)) + : null; + + await fs.writeFile(nextPath, raw, "utf8"); + + if (previousPath) { + try { + await fs.unlink(previousPath); + } catch { + // ignore + } + } + + return { ok: true, slug }; +} + +async function deleteSnippetBySlug(slug) { + const normalized = validateBlogSlug(slug); + const target = assertSafePath(SNIPPETS_CONTENT_DIR, path.join(SNIPPETS_CONTENT_DIR, `${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 }); @@ -747,6 +828,10 @@ 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("snippet:list", async () => listSnippets()); +ipcMain.handle("snippet:upsert", async (_, payload) => upsertSnippet(payload)); +ipcMain.handle("snippet:delete", async (_, slug) => deleteSnippetBySlug(slug)); + ipcMain.handle("media:listFolders", async () => listMediaFolders()); ipcMain.handle("media:createFolder", async (_, folder) => createMediaFolder(folder)); ipcMain.handle("media:listFiles", async (_, folder) => listMediaFiles(folder)); diff --git a/apps/data-panel/preload.cjs b/apps/data-panel/preload.cjs index 64ad3b9..3827c5a 100644 --- a/apps/data-panel/preload.cjs +++ b/apps/data-panel/preload.cjs @@ -20,6 +20,11 @@ contextBridge.exposeInMainWorld("panelAPI", { upsert: (payload) => ipcRenderer.invoke("podcast:upsert", payload), delete: (payload) => ipcRenderer.invoke("podcast:delete", payload), }, + snippet: { + list: () => ipcRenderer.invoke("snippet:list"), + upsert: (payload) => ipcRenderer.invoke("snippet:upsert", payload), + delete: (slug) => ipcRenderer.invoke("snippet:delete", slug), + }, media: { listFolders: () => ipcRenderer.invoke("media:listFolders"), createFolder: (folder) => ipcRenderer.invoke("media:createFolder", folder), diff --git a/components/site-navbar.tsx b/components/site-navbar.tsx index 116858c..90a93e6 100644 --- a/components/site-navbar.tsx +++ b/components/site-navbar.tsx @@ -1,4 +1,4 @@ -"use client"; +"use client"; import { Icon } from "@iconify/react"; import Link from "next/link"; @@ -90,7 +90,7 @@ export function SiteNavbar() { type="button" onClick={() => setSearchOpen(true)} className="inline-flex h-8 w-44 cursor-pointer items-center justify-between rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground sm:w-52" - aria-label="Komut paletini aç" + aria-label="Komut paletini aç" > @@ -128,7 +128,7 @@ export function SiteNavbar() { - Menü + Menü Mobil Menü @@ -138,7 +138,7 @@ export function SiteNavbar() { type="button" onClick={() => setSearchOpen(true)} className="inline-flex h-9 w-full cursor-pointer items-center justify-between rounded-sm border border-border px-3 text-sm text-muted-foreground transition-colors hover:text-foreground" - aria-label="Komut paletini aç" + aria-label="Komut paletini aç" > diff --git a/components/snippets-content.tsx b/components/snippets-content.tsx new file mode 100644 index 0000000..49c2c84 --- /dev/null +++ b/components/snippets-content.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useState } from "react"; +import { Badge, Card, Typography } from "poyraz-ui/atoms"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { StaggerContainer, StaggerItem } from "@/components/motion-wrapper"; +import type { Snippet } from "@/data/snippets"; + +type SnippetsContentProps = { + snippets: Snippet[]; + categories: string[]; +}; + +function extractCode(markdown: string) { + const match = /```\w*\n([\s\S]*?)```/.exec(markdown); + return match ? match[1].trim() : markdown; +} + +export function SnippetsContent({ snippets, categories }: SnippetsContentProps) { + const [selected, setSelected] = useState("All"); + + const filtered = selected === "All" + ? snippets + : snippets.filter((s) => s.category === selected); + + return ( + + + + + + Kod Parçacıkları + + + Sıkça kullandığım, tekrar kullanılabilir kod parçacıkları. + + + + {categories.map((cat) => ( + setSelected(cat)}> + + {cat} + + + ))} + + + + + + {filtered.map((snippet) => ( + + + + + + {snippet.title} + + + {snippet.description} + + + + {snippet.language} + + + + + {extractCode(snippet.markdown)} + + + + + ))} + + + {filtered.length === 0 && ( + + + Bu kategoride henüz snippet bulunmuyor. + + + )} + + ); +} diff --git a/content/snippets/css-reset.md b/content/snippets/css-reset.md new file mode 100644 index 0000000..301920e --- /dev/null +++ b/content/snippets/css-reset.md @@ -0,0 +1,36 @@ +--- +title: "CSS Reset" +language: "css" +category: "CSS" +description: "Minimal ve modern bir CSS reset şablonu." +--- + +```css +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + -webkit-text-size-adjust: 100%; + -moz-tab-size: 4; + tab-size: 4; +} + +body { + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +input, button, textarea, select { + font: inherit; +} +``` diff --git a/content/snippets/use-local-storage.md b/content/snippets/use-local-storage.md new file mode 100644 index 0000000..cc361b1 --- /dev/null +++ b/content/snippets/use-local-storage.md @@ -0,0 +1,24 @@ +--- +title: "useLocalStorage Hook" +language: "typescript" +category: "React Hooks" +description: "localStorage ile senkronize çalışan bir React hook'u." +--- + +```typescript +import { useState, useEffect } from "react"; + +export function useLocalStorage(key: string, initialValue: T) { + const [value, setValue] = useState(() => { + if (typeof window === "undefined") return initialValue; + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : initialValue; + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(value)); + }, [key, value]); + + return [value, setValue] as const; +} +``` diff --git a/data/snippets.ts b/data/snippets.ts new file mode 100644 index 0000000..49d8ab5 --- /dev/null +++ b/data/snippets.ts @@ -0,0 +1,44 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import matter from "gray-matter"; + +export type Snippet = { + slug: string; + title: string; + language: string; + category: string; + description: string; + markdown: string; +}; + +const SNIPPETS_DIR = path.join(process.cwd(), "content", "snippets"); + +export async function listSnippets(): Promise { + let files: string[] = []; + try { + files = await fs.readdir(SNIPPETS_DIR); + } catch { + return []; + } + + const mdFiles = files.filter((f) => f.endsWith(".md")); + + const snippets = await Promise.all( + mdFiles.map(async (fileName) => { + const raw = await fs.readFile(path.join(SNIPPETS_DIR, fileName), "utf8"); + const parsed = matter(raw); + const slug = fileName.replace(/\.md$/i, ""); + + return { + slug, + title: String(parsed.data.title || slug), + language: String(parsed.data.language || "text"), + category: String(parsed.data.category || "General"), + description: String(parsed.data.description || ""), + markdown: parsed.content.trim(), + }; + }), + ); + + return snippets.sort((a, b) => a.title.localeCompare(b.title)); +} diff --git a/lib/links.ts b/lib/links.ts index fbd0afe..e6c461c 100644 --- a/lib/links.ts +++ b/lib/links.ts @@ -1,8 +1,9 @@ -export const NAV_LINKS = [ +export const NAV_LINKS = [ { id: "about", label: "Hakkımda", href: "/about" }, { id: "blog", label: "Blog", href: "/blog" }, { id: "content", label: "İçerikler", href: "/content" }, { id: "projects", label: "Projeler", href: "/projects" }, + { id: "snippets", label: "Snippets", href: "/snippets" }, { id: "contact", label: "İletişim", href: "/contact" }, ] as const;