Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
apps/data-panel/dist
|
||||
apps/data-panel/release
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Portfolio New
|
||||
|
||||
Poyraz Avsever icin hazirlanmis, `Next.js + poyraz-ui` tabanli portfolyo ve ayni repoda yer alan Electron tabanli veri yonetim paneli.
|
||||
|
||||
## Monorepo Yapisi
|
||||
|
||||
```text
|
||||
.
|
||||
|- app/ # Next.js route'lari
|
||||
|- components/ # UI component'leri
|
||||
|- content/ # Markdown icerikleri (blog, podcast)
|
||||
|- data/ # TypeScript veri kaynaklari
|
||||
|- public/ # Statik dosyalar
|
||||
|- apps/
|
||||
| |- data-panel/ # Electron dashboard (icerik yonetimi + publish)
|
||||
```
|
||||
|
||||
## Gereksinimler
|
||||
|
||||
- Node.js 20+
|
||||
- pnpm 9+
|
||||
- Git
|
||||
|
||||
## Kurulum
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Frontend Komutlari
|
||||
|
||||
```bash
|
||||
pnpm dev # Next.js local
|
||||
pnpm lint # ESLint
|
||||
pnpm build # Production build
|
||||
pnpm start # Production run
|
||||
```
|
||||
|
||||
## Electron Data Panel Komutlari
|
||||
|
||||
```bash
|
||||
pnpm panel:dev # Electron panel (development)
|
||||
pnpm panel:clean # apps/data-panel/dist temizle
|
||||
pnpm panel:pack # Paket test (unpacked)
|
||||
pnpm panel:dist # Tum hedefler icin distributable
|
||||
pnpm panel:dist:win # Windows NSIS installer
|
||||
pnpm panel:dist:portable # Windows portable exe
|
||||
```
|
||||
|
||||
## Icerik Kaynaklari
|
||||
|
||||
- Blog markdown: `content/blog/*.md`
|
||||
- Podcast markdown:
|
||||
- `content/podcasts/yazilim/*.md`
|
||||
- `content/podcasts/masa-basi/*.md`
|
||||
- Site verileri: `data/*.ts`
|
||||
|
||||
## Electron Panel Neleri Yapar?
|
||||
|
||||
- Blog/podcast icerigi olusturma-guncelleme-silme
|
||||
- `data/*.ts` export dizilerini form tabanli yonetme
|
||||
- `public/` altina medya yukleme/silme
|
||||
- `git add + commit + push` ile yayinlama akisi
|
||||
|
||||
## Open-Source ve Ozellestirme Rehberi
|
||||
|
||||
### 1) Dashboard tab/field ozellestirme
|
||||
|
||||
Paneldeki koleksiyonlar ve form alanlari su dosyadan yonetilir:
|
||||
|
||||
- `apps/data-panel/renderer/configs.js`
|
||||
|
||||
Buradan:
|
||||
|
||||
- yeni tab/collection ekleyebilir,
|
||||
- alan tiplerini (`text`, `textarea`, `number`, `url`) degistirebilir,
|
||||
- kart gorunumunu (`card`) ozellestirebilirsin.
|
||||
|
||||
### 2) Farkli repo koku ile calistirma
|
||||
|
||||
Panel varsayilan olarak bu monorepo kokunu kullanir. Farkli klasor icin:
|
||||
|
||||
- Cevresel degisken: `PORTFOLIO_WORKSPACE_ROOT`
|
||||
- veya CLI arguman: `--workspace-root=<path>`
|
||||
|
||||
Ornek (PowerShell):
|
||||
|
||||
```powershell
|
||||
$env:PORTFOLIO_WORKSPACE_ROOT = "D:\my-portfolio-repo"
|
||||
pnpm panel:dev
|
||||
```
|
||||
|
||||
Ornek (packaged exe):
|
||||
|
||||
```powershell
|
||||
.\apps\data-panel\dist\portfolio-data-panel-0.1.0-x64-portable.exe --workspace-root="D:\my-portfolio-repo"
|
||||
```
|
||||
|
||||
Panel baslarken `data`, `public`, `content/blog`, `content/podcasts` klasorlerini dogrular.
|
||||
|
||||
### 3) Publish akisini degistirme
|
||||
|
||||
Git publish akisi `apps/data-panel/main.cjs` icindeki `publishChanges` fonksiyonunda.
|
||||
Varsayilan akista:
|
||||
|
||||
- `git add data public content`
|
||||
- otomatik/manuel commit message
|
||||
- `git push`
|
||||
|
||||
Istersen branch secimi, PR tabanli akış, veya sadece commit modlari ekleyebilirsin.
|
||||
|
||||
## Release Checklist
|
||||
|
||||
1. `pnpm lint`
|
||||
2. `pnpm build`
|
||||
3. Icerik/veri kontrolu (`content/`, `data/`, `public/`)
|
||||
4. Electron panel akisi kontrolu (`pnpm panel:dev`)
|
||||
5. Gerekirse distributable kontrolu (`pnpm panel:pack` / `pnpm panel:dist`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### YouTube cookie/SameSite uyarilari
|
||||
|
||||
Embed URL'ler `youtube-nocookie.com` formatina cekildi. Bu degisiklik cross-site cookie kaynakli konsol gurultusunu azaltir.
|
||||
Tarayici guvenlik politikasi nedeniyle bazi 3rd-party loglar tamamen sifirlanamayabilir; bu durumda uygulama davranisi etkilenmez.
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
/release
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Portfolio Data Panel (Electron)
|
||||
|
||||
Bu uygulama `data/`, `content/` ve `public/` klasorlerini yonetmek icin hazirlanmis Electron dashboard'dur.
|
||||
|
||||
## Scripts
|
||||
|
||||
```bash
|
||||
pnpm run clean
|
||||
pnpm run dev
|
||||
pnpm run pack
|
||||
pnpm run dist
|
||||
pnpm run dist:win
|
||||
pnpm run dist:portable
|
||||
```
|
||||
|
||||
## Workspace Root Secimi
|
||||
|
||||
Panel varsayilan olarak monorepo kokunu kullanir (`../../`).
|
||||
Farkli klasor ile calistirmak icin:
|
||||
|
||||
- Environment variable: `PORTFOLIO_WORKSPACE_ROOT`
|
||||
- CLI arg: `--workspace-root=<path>`
|
||||
|
||||
PowerShell ornegi:
|
||||
|
||||
```powershell
|
||||
$env:PORTFOLIO_WORKSPACE_ROOT = "D:\my-portfolio-repo"
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Portable EXE ornegi:
|
||||
|
||||
```powershell
|
||||
.\dist\portfolio-data-panel-0.1.0-x64-portable.exe --workspace-root="D:\my-portfolio-repo"
|
||||
```
|
||||
|
||||
## Ozellestirme
|
||||
|
||||
Form ve tab yapisi:
|
||||
|
||||
- `renderer/configs.js`
|
||||
|
||||
Publish akisi:
|
||||
|
||||
- `main.cjs` icindeki `publishChanges` fonksiyonu.
|
||||
@@ -6,11 +6,82 @@ 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 getCliArgValue(flag) {
|
||||
const exact = process.argv.find((arg) => arg.startsWith(`${flag}=`));
|
||||
if (exact) return exact.slice(flag.length + 1);
|
||||
|
||||
const index = process.argv.findIndex((arg) => arg === flag);
|
||||
if (index >= 0 && process.argv[index + 1]) {
|
||||
return process.argv[index + 1];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function resolveWorkspaceRoot() {
|
||||
const fromArg = getCliArgValue("--workspace-root");
|
||||
const fromEnv = process.env.PORTFOLIO_WORKSPACE_ROOT || "";
|
||||
const selected = fromArg || fromEnv;
|
||||
|
||||
if (selected) {
|
||||
return path.resolve(selected);
|
||||
}
|
||||
|
||||
return path.resolve(__dirname, "../..");
|
||||
}
|
||||
|
||||
let WORKSPACE_ROOT = resolveWorkspaceRoot();
|
||||
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");
|
||||
|
||||
function setWorkspaceRoot(nextRoot) {
|
||||
WORKSPACE_ROOT = path.resolve(nextRoot);
|
||||
DATA_DIR = path.join(WORKSPACE_ROOT, "data");
|
||||
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");
|
||||
}
|
||||
|
||||
async function pathExists(target) {
|
||||
try {
|
||||
await fs.access(target);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWorkspaceStructure() {
|
||||
const checks = [
|
||||
{ label: "data", target: DATA_DIR },
|
||||
{ label: "public", target: PUBLIC_DIR },
|
||||
{ label: "content/blog", target: BLOG_CONTENT_DIR },
|
||||
{ label: "content/podcasts", target: PODCAST_CONTENT_DIR },
|
||||
];
|
||||
|
||||
const missing = [];
|
||||
for (const check of checks) {
|
||||
if (!(await pathExists(check.target))) {
|
||||
missing.push(check.label);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missing.length) return;
|
||||
|
||||
const details = [
|
||||
`Workspace root: ${WORKSPACE_ROOT}`,
|
||||
`Missing: ${missing.join(", ")}`,
|
||||
"",
|
||||
"Fix options:",
|
||||
"1) Run panel from repository root.",
|
||||
"2) Set PORTFOLIO_WORKSPACE_ROOT environment variable.",
|
||||
"3) Launch app with --workspace-root=<path>.",
|
||||
].join("\n");
|
||||
|
||||
throw new Error(details);
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
@@ -685,11 +756,29 @@ ipcMain.handle("media:deleteFile", async (_, relativePath) => deleteMediaFile(re
|
||||
ipcMain.handle("publish:status", async () => getPublishStatus());
|
||||
ipcMain.handle("publish:run", async (_, message) => publishChanges(message));
|
||||
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
try {
|
||||
setWorkspaceRoot(resolveWorkspaceRoot());
|
||||
await ensureWorkspaceStructure();
|
||||
createWindow();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
dialog.showErrorBox("Portfolio Data Panel", message);
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
app.on("activate", async () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
try {
|
||||
await ensureWorkspaceStructure();
|
||||
createWindow();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
dialog.showErrorBox("Portfolio Data Panel", message);
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,11 +2,56 @@
|
||||
"name": "@portfolio/data-panel",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Electron dashboard for managing portfolio data/content and publishing updates.",
|
||||
"author": "Poyraz Avsever",
|
||||
"main": "main.cjs",
|
||||
"scripts": {
|
||||
"dev": "electron ."
|
||||
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||
"dev": "electron .",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "pnpm run dist:win && pnpm run dist:portable",
|
||||
"dist:win": "electron-builder --win nsis --config.artifactName=portfolio-data-panel-${version}-${arch}-setup.${ext}",
|
||||
"dist:portable": "electron-builder --win portable --config.artifactName=portfolio-data-panel-${version}-${arch}-portable.${ext}"
|
||||
},
|
||||
"dependencies": {
|
||||
"gray-matter": "^4.0.3",
|
||||
"js-yaml": "^3.14.2",
|
||||
"kind-of": "^6.0.3",
|
||||
"section-matter": "^1.0.0",
|
||||
"strip-bom-string": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^33.2.0"
|
||||
"electron": "^33.2.0",
|
||||
"electron-builder": "^24.13.3"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.poyraz.portfolio.datapanel",
|
||||
"productName": "Portfolio Data Panel",
|
||||
"asar": true,
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"main.cjs",
|
||||
"preload.cjs",
|
||||
"renderer/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"win": {
|
||||
"signAndEditExecutable": false,
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"perMachine": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +123,10 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
||||
</header>
|
||||
|
||||
<Card className="relative aspect-[16/8] overflow-hidden rounded-sm border-border">
|
||||
<img
|
||||
<Image
|
||||
src={post.coverImage}
|
||||
alt={post.title}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Icon } from "@iconify/react";
|
||||
import { Badge, Button, Card, Typography } from "poyraz-ui/atoms";
|
||||
import { Modal, ModalContent, ModalTitle, Sheet, SheetContent, SheetTitle } from "poyraz-ui/molecules";
|
||||
import type { PodcastEpisode } from "@/data/content-types";
|
||||
import { getYoutubeEmbedUrl } from "@/lib/youtube";
|
||||
|
||||
type ContentContentProps = {
|
||||
yazilimEpisodes: PodcastEpisode[];
|
||||
@@ -102,16 +103,6 @@ function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function getYoutubeEmbedUrl(link: string) {
|
||||
try {
|
||||
const url = new URL(link);
|
||||
const videoId = url.searchParams.get("v");
|
||||
return videoId ? `https://www.youtube.com/embed/${videoId}` : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function PodcastColumn({
|
||||
title,
|
||||
subtitle,
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { Card, Typography } from "poyraz-ui/atoms";
|
||||
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
|
||||
|
||||
function getYoutubeEmbedUrl(link: string) {
|
||||
try {
|
||||
const url = new URL(link);
|
||||
const videoId = url.searchParams.get("v");
|
||||
return videoId ? `https://www.youtube.com/embed/${videoId}` : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
import { getYoutubeEmbedUrl } from "@/lib/youtube";
|
||||
|
||||
export function HomeVideosSection() {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ReferenceCards } from "@/components/reference-cards";
|
||||
|
||||
export function ReferencesSection() {
|
||||
return (
|
||||
<section className="space-y-8 md:pt-12">
|
||||
<section className="space-y-8 md:pt-4">
|
||||
<div className="relative overflow-hidden rounded-sm">
|
||||
<ReferenceCards className="flex w-max gap-3" />
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
const SUPPORTED_HOSTS = new Set([
|
||||
"youtube.com",
|
||||
"m.youtube.com",
|
||||
"youtu.be",
|
||||
"youtube-nocookie.com",
|
||||
]);
|
||||
|
||||
const VIDEO_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/;
|
||||
|
||||
function normalizeHost(hostname: string) {
|
||||
return hostname.toLowerCase().replace(/^www\./, "");
|
||||
}
|
||||
|
||||
function fromPathSegments(pathname: string) {
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
if (!segments.length) return null;
|
||||
|
||||
if (segments[0] === "watch") return null;
|
||||
if (segments[0] === "shorts" && segments[1]) return segments[1];
|
||||
if (segments[0] === "embed" && segments[1]) return segments[1];
|
||||
|
||||
return segments[0];
|
||||
}
|
||||
|
||||
export function getYoutubeVideoId(link: string) {
|
||||
try {
|
||||
const url = new URL(link);
|
||||
const host = normalizeHost(url.hostname);
|
||||
|
||||
if (!SUPPORTED_HOSTS.has(host)) return null;
|
||||
|
||||
const videoId = host === "youtu.be" ? fromPathSegments(url.pathname) : url.searchParams.get("v") ?? fromPathSegments(url.pathname);
|
||||
if (!videoId || !VIDEO_ID_REGEX.test(videoId)) return null;
|
||||
|
||||
return videoId;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getYoutubeEmbedUrl(link: string) {
|
||||
const videoId = getYoutubeVideoId(link);
|
||||
if (!videoId) return null;
|
||||
|
||||
// Use privacy-enhanced mode to reduce cross-site cookie noise in modern browsers.
|
||||
return `https://www.youtube-nocookie.com/embed/${videoId}?rel=0&modestbranding=1`;
|
||||
}
|
||||
+7
-2
@@ -7,8 +7,13 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"panel:dev": "pnpm --filter @portfolio/data-panel dev",
|
||||
"panel:start": "pnpm --filter @portfolio/data-panel dev"
|
||||
"panel:dev": "pnpm --filter @portfolio/data-panel run dev",
|
||||
"panel:start": "pnpm --filter @portfolio/data-panel run dev",
|
||||
"panel:clean": "pnpm --filter @portfolio/data-panel run clean",
|
||||
"panel:pack": "pnpm --filter @portfolio/data-panel run pack",
|
||||
"panel:dist": "pnpm --filter @portfolio/data-panel run dist",
|
||||
"panel:dist:win": "pnpm --filter @portfolio/data-panel run dist:win",
|
||||
"panel:dist:portable": "pnpm --filter @portfolio/data-panel run dist:portable"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/react": "^6.0.2",
|
||||
|
||||
Generated
+1403
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user