feat: implement data management panel and blog data utilities
This commit is contained in:
@@ -1,17 +1,12 @@
|
|||||||
import { ContentContent } from "@/components/content-content";
|
import { ContentContent } from "@/components/content-content";
|
||||||
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
|
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
|
||||||
import { getPdfNotes, getPodcastCollections } from "@/lib/content-page";
|
import { getPdfNotes } from "@/lib/content-page";
|
||||||
|
|
||||||
export default async function ContentPage() {
|
export default async function ContentPage() {
|
||||||
const [{ yazilim, masaBasi }, pdfFiles] = await Promise.all([
|
const pdfFiles = await getPdfNotes();
|
||||||
getPodcastCollections(),
|
|
||||||
getPdfNotes(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentContent
|
<ContentContent
|
||||||
yazilimEpisodes={yazilim}
|
|
||||||
masaBasiEpisodes={masaBasi}
|
|
||||||
youtubeLinks={YOUTUBE_VIDEO_LINKS}
|
youtubeLinks={YOUTUBE_VIDEO_LINKS}
|
||||||
pdfFiles={pdfFiles}
|
pdfFiles={pdfFiles}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ let WORKSPACE_ROOT = resolveWorkspaceRoot();
|
|||||||
let DATA_DIR = path.join(WORKSPACE_ROOT, "data");
|
let DATA_DIR = path.join(WORKSPACE_ROOT, "data");
|
||||||
let PUBLIC_DIR = path.join(WORKSPACE_ROOT, "public");
|
let PUBLIC_DIR = path.join(WORKSPACE_ROOT, "public");
|
||||||
let BLOG_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "blog");
|
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");
|
let SNIPPETS_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "snippets");
|
||||||
|
|
||||||
function setWorkspaceRoot(nextRoot) {
|
function setWorkspaceRoot(nextRoot) {
|
||||||
@@ -42,7 +41,6 @@ function setWorkspaceRoot(nextRoot) {
|
|||||||
DATA_DIR = path.join(WORKSPACE_ROOT, "data");
|
DATA_DIR = path.join(WORKSPACE_ROOT, "data");
|
||||||
PUBLIC_DIR = path.join(WORKSPACE_ROOT, "public");
|
PUBLIC_DIR = path.join(WORKSPACE_ROOT, "public");
|
||||||
BLOG_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "blog");
|
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");
|
SNIPPETS_CONTENT_DIR = path.join(WORKSPACE_ROOT, "content", "snippets");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +58,6 @@ async function ensureWorkspaceStructure() {
|
|||||||
{ label: "data", target: DATA_DIR },
|
{ label: "data", target: DATA_DIR },
|
||||||
{ label: "public", target: PUBLIC_DIR },
|
{ label: "public", target: PUBLIC_DIR },
|
||||||
{ label: "content/blog", target: BLOG_CONTENT_DIR },
|
{ label: "content/blog", target: BLOG_CONTENT_DIR },
|
||||||
{ label: "content/podcasts", target: PODCAST_CONTENT_DIR },
|
|
||||||
{ label: "content/snippets", target: SNIPPETS_CONTENT_DIR },
|
{ label: "content/snippets", target: SNIPPETS_CONTENT_DIR },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -131,18 +128,6 @@ function validateBlogSlug(slug) {
|
|||||||
return normalized;
|
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) {
|
function runGit(args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
execFile("git", args, { cwd: WORKSPACE_ROOT }, (error, stdout, stderr) => {
|
execFile("git", args, { cwd: WORKSPACE_ROOT }, (error, stdout, stderr) => {
|
||||||
@@ -497,94 +482,6 @@ async function deleteBlogBySlug(slug) {
|
|||||||
return { ok: true };
|
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapMarkdownToSnippet(fileName, raw) {
|
function mapMarkdownToSnippet(fileName, raw) {
|
||||||
const parsed = matter(raw);
|
const parsed = matter(raw);
|
||||||
const slug = fileName.replace(/\.md$/i, "");
|
const slug = fileName.replace(/\.md$/i, "");
|
||||||
@@ -824,9 +721,6 @@ ipcMain.handle("data:delete", async (_, fileName) => deleteDataFile(fileName));
|
|||||||
ipcMain.handle("blog:list", async () => listBlogs());
|
ipcMain.handle("blog:list", async () => listBlogs());
|
||||||
ipcMain.handle("blog:upsert", async (_, payload) => upsertBlog(payload));
|
ipcMain.handle("blog:upsert", async (_, payload) => upsertBlog(payload));
|
||||||
ipcMain.handle("blog:delete", async (_, slug) => deleteBlogBySlug(slug));
|
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("snippet:list", async () => listSnippets());
|
ipcMain.handle("snippet:list", async () => listSnippets());
|
||||||
ipcMain.handle("snippet:upsert", async (_, payload) => upsertSnippet(payload));
|
ipcMain.handle("snippet:upsert", async (_, payload) => upsertSnippet(payload));
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ contextBridge.exposeInMainWorld("panelAPI", {
|
|||||||
upsert: (payload) => ipcRenderer.invoke("blog:upsert", payload),
|
upsert: (payload) => ipcRenderer.invoke("blog:upsert", payload),
|
||||||
delete: (slug) => ipcRenderer.invoke("blog:delete", slug),
|
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),
|
|
||||||
},
|
|
||||||
snippet: {
|
snippet: {
|
||||||
list: () => ipcRenderer.invoke("snippet:list"),
|
list: () => ipcRenderer.invoke("snippet:list"),
|
||||||
upsert: (payload) => ipcRenderer.invoke("snippet:upsert", payload),
|
upsert: (payload) => ipcRenderer.invoke("snippet:upsert", payload),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { BLOG_CATEGORIES, COLLECTION_CONFIGS, PODCAST_LABELS } from "./configs.js";
|
import { BLOG_CATEGORIES, COLLECTION_CONFIGS } from "./configs.js";
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
activeTab: "blog",
|
activeTab: "blog",
|
||||||
@@ -15,12 +15,6 @@ const state = {
|
|||||||
editingIndex: null,
|
editingIndex: null,
|
||||||
formInputs: {},
|
formInputs: {},
|
||||||
},
|
},
|
||||||
podcast: {
|
|
||||||
kind: "yazilim",
|
|
||||||
episodes: [],
|
|
||||||
selectedSlug: "",
|
|
||||||
originalSlug: "",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
@@ -28,7 +22,6 @@ const el = {
|
|||||||
panels: {
|
panels: {
|
||||||
blog: document.getElementById("tab-blog"),
|
blog: document.getElementById("tab-blog"),
|
||||||
collection: document.getElementById("tab-collection"),
|
collection: document.getElementById("tab-collection"),
|
||||||
podcast: document.getElementById("tab-podcast"),
|
|
||||||
media: document.getElementById("tab-media"),
|
media: document.getElementById("tab-media"),
|
||||||
publish: document.getElementById("tab-publish"),
|
publish: document.getElementById("tab-publish"),
|
||||||
},
|
},
|
||||||
@@ -56,19 +49,6 @@ const el = {
|
|||||||
collectionEditorTitle: document.getElementById("collection-editor-title"),
|
collectionEditorTitle: document.getElementById("collection-editor-title"),
|
||||||
collectionHelper: document.getElementById("collection-helper"),
|
collectionHelper: document.getElementById("collection-helper"),
|
||||||
collectionFormGrid: document.getElementById("collection-form-grid"),
|
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"),
|
folderSelect: document.getElementById("folder-select"),
|
||||||
newFolder: document.getElementById("new-folder"),
|
newFolder: document.getElementById("new-folder"),
|
||||||
createFolder: document.getElementById("create-folder"),
|
createFolder: document.getElementById("create-folder"),
|
||||||
@@ -95,16 +75,12 @@ function getCollectionConfig(key = state.collection.key) {
|
|||||||
function setActiveTab(tab, options = {}) {
|
function setActiveTab(tab, options = {}) {
|
||||||
state.activeTab = tab;
|
state.activeTab = tab;
|
||||||
if (tab === "collection" && options.collectionKey) state.collection.key = options.collectionKey;
|
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) {
|
for (const button of el.tabs) {
|
||||||
const buttonTab = button.dataset.tab;
|
const buttonTab = button.dataset.tab;
|
||||||
const isCollection = buttonTab === "collection";
|
const isCollection = buttonTab === "collection";
|
||||||
const isPodcast = buttonTab === "podcast";
|
|
||||||
const isActive = isCollection
|
const isActive = isCollection
|
||||||
? tab === "collection" && button.dataset.collectionKey === state.collection.key
|
? tab === "collection" && button.dataset.collectionKey === state.collection.key
|
||||||
: isPodcast
|
|
||||||
? tab === "podcast" && button.dataset.podcastKind === state.podcast.kind
|
|
||||||
: buttonTab === tab;
|
: buttonTab === tab;
|
||||||
button.classList.toggle("active", isActive);
|
button.classList.toggle("active", isActive);
|
||||||
}
|
}
|
||||||
@@ -116,10 +92,6 @@ function setActiveTab(tab, options = {}) {
|
|||||||
if (tab === "collection") {
|
if (tab === "collection") {
|
||||||
void loadCollection(state.collection.key).catch((error) => notify(String(error.message || error)));
|
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() {
|
function emptyBlogDraft() {
|
||||||
@@ -273,98 +245,6 @@ async function removeBlogFile(slugArg) {
|
|||||||
notify("Blog post deleted.");
|
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) {
|
function deserializeCollectionItem(config, rawItem) {
|
||||||
if (typeof config.deserializeItem === "function") return config.deserializeItem(rawItem);
|
if (typeof config.deserializeItem === "function") return config.deserializeItem(rawItem);
|
||||||
if (rawItem && typeof rawItem === "object" && !Array.isArray(rawItem)) return { ...rawItem };
|
if (rawItem && typeof rawItem === "object" && !Array.isArray(rawItem)) return { ...rawItem };
|
||||||
@@ -706,10 +586,6 @@ function bindEvents() {
|
|||||||
setActiveTab("collection", { collectionKey: button.dataset.collectionKey });
|
setActiveTab("collection", { collectionKey: button.dataset.collectionKey });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (tab === "podcast") {
|
|
||||||
setActiveTab("podcast", { podcastKind: button.dataset.podcastKind });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -726,11 +602,6 @@ function bindEvents() {
|
|||||||
el.deleteCollectionItem.addEventListener("click", () => void removeCollectionItem().catch((error) => notify(String(error.message || error))));
|
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.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", () => {
|
el.folderSelect.addEventListener("change", () => {
|
||||||
state.selectedFolder = el.folderSelect.value;
|
state.selectedFolder = el.folderSelect.value;
|
||||||
void loadMediaFiles();
|
void loadMediaFiles();
|
||||||
@@ -748,7 +619,6 @@ async function init() {
|
|||||||
await loadBlogFiles();
|
await loadBlogFiles();
|
||||||
if (!state.selectedBlogSlug) createBlogFile();
|
if (!state.selectedBlogSlug) createBlogFile();
|
||||||
await loadCollection(state.collection.key);
|
await loadCollection(state.collection.key);
|
||||||
await loadPodcastFiles(state.podcast.kind);
|
|
||||||
await loadMediaFolders();
|
await loadMediaFolders();
|
||||||
await refreshPublishStatus();
|
await refreshPublishStatus();
|
||||||
setActiveTab("blog");
|
setActiveTab("blog");
|
||||||
|
|||||||
@@ -482,8 +482,3 @@ export const COLLECTION_CONFIGS = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PODCAST_LABELS = {
|
|
||||||
yazilim: "Podcast Yazilim",
|
|
||||||
"masa-basi": "Podcast Masa Basi",
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -26,13 +26,6 @@
|
|||||||
data-collection-key="announcement"
|
data-collection-key="announcement"
|
||||||
>
|
>
|
||||||
Announcement
|
Announcement
|
||||||
</button>
|
|
||||||
<button class="tab-btn" data-tab="podcast" data-podcast-kind="yazilim">
|
|
||||||
Podcast Yazilim
|
|
||||||
</button>
|
|
||||||
<button class="tab-btn" data-tab="podcast" data-podcast-kind="masa-basi">
|
|
||||||
Podcast Masa Basi
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="tab-btn"
|
class="tab-btn"
|
||||||
data-tab="collection"
|
data-tab="collection"
|
||||||
@@ -223,72 +216,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="tab-podcast" class="tab-panel">
|
|
||||||
<div class="split">
|
|
||||||
<aside class="card sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h2 id="podcast-sidebar-title">Podcast Episodes</h2>
|
|
||||||
<button id="refresh-podcast-files" class="btn btn-ghost">
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="podcast-card-list" class="blog-card-list"></div>
|
|
||||||
<button id="new-podcast-file" class="btn mt-8">New Episode</button>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section class="card editor">
|
|
||||||
<div class="editor-head">
|
|
||||||
<h2 id="podcast-editor-title">Create Episode</h2>
|
|
||||||
<div class="row">
|
|
||||||
<button id="delete-podcast-file" class="btn btn-danger">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
<button id="save-podcast-file" class="btn">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="blog-form-grid">
|
|
||||||
<div>
|
|
||||||
<label for="podcast-slug">Slug</label>
|
|
||||||
<input
|
|
||||||
id="podcast-slug"
|
|
||||||
placeholder="2026-03-09-foundations-over-tools"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="podcast-title">Title</label>
|
|
||||||
<input
|
|
||||||
id="podcast-title"
|
|
||||||
placeholder="Foundations Over Tools"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="podcast-date">Date</label>
|
|
||||||
<input id="podcast-date" placeholder="2026-03-09" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="podcast-youtube-url">YouTube URL</label>
|
|
||||||
<input
|
|
||||||
id="podcast-youtube-url"
|
|
||||||
placeholder="https://www.youtube.com/watch?v=..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="full">
|
|
||||||
<label for="podcast-spotify-url">Spotify URL</label>
|
|
||||||
<input
|
|
||||||
id="podcast-spotify-url"
|
|
||||||
placeholder="https://open.spotify.com/show/..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="full">
|
|
||||||
<label for="podcast-editor">Markdown Content</label>
|
|
||||||
<textarea id="podcast-editor" spellcheck="false"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="tab-media" class="tab-panel">
|
<section id="tab-media" class="tab-panel">
|
||||||
<div class="split">
|
<div class="split">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
|
|||||||
@@ -82,40 +82,6 @@ export function BlogContent({ data }: BlogContentProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="rounded-sm border-border p-4">
|
|
||||||
<Typography variant="large">Podcast İçerikleri</Typography>
|
|
||||||
<div className="mt-3 space-y-3">
|
|
||||||
{data.podcastGroups.map((group) => (
|
|
||||||
<Card key={group.id} className="rounded-sm border-border p-3">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<Typography variant="small" className="font-semibold text-foreground">
|
|
||||||
{group.title}
|
|
||||||
</Typography>
|
|
||||||
<Link
|
|
||||||
href={group.href}
|
|
||||||
className="text-xs text-muted-foreground underline transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
Tümünü Gör
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 space-y-2">
|
|
||||||
{group.items.map((episode) => (
|
|
||||||
<div key={episode.id} className="rounded-sm border border-border p-2">
|
|
||||||
<Typography variant="small" className="font-medium text-foreground">
|
|
||||||
{episode.title}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="small" className="mt-0.5 text-muted-foreground">
|
|
||||||
{episode.date}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
import { Button, Card, Typography } from "poyraz-ui/atoms";
|
||||||
import ReactMarkdown from "react-markdown";
|
import { Modal, ModalContent, ModalTitle } from "poyraz-ui/molecules";
|
||||||
import remarkGfm from "remark-gfm";
|
|
||||||
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";
|
import { getYoutubeEmbedUrl } from "@/lib/youtube";
|
||||||
|
|
||||||
type ContentContentProps = {
|
type ContentContentProps = {
|
||||||
yazilimEpisodes: PodcastEpisode[];
|
|
||||||
masaBasiEpisodes: PodcastEpisode[];
|
|
||||||
youtubeLinks: readonly string[];
|
youtubeLinks: readonly string[];
|
||||||
pdfFiles: string[];
|
pdfFiles: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPodcastLabel(podcast: PodcastEpisode["podcast"]) {
|
|
||||||
if (podcast === "yazilim") return "Yazılım";
|
|
||||||
if (podcast === "masa-basi") return "Masa Başı";
|
|
||||||
return podcast;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
@@ -103,57 +90,10 @@ function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PodcastColumn({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
episodes,
|
|
||||||
onOpenEpisode,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
episodes: PodcastEpisode[];
|
|
||||||
onOpenEpisode: (episode: PodcastEpisode) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Card className="rounded-sm border-border p-4">
|
|
||||||
<Typography variant="large" className="text-base">
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="small" className="mt-1 text-muted-foreground">
|
|
||||||
{subtitle}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<div className="mt-3 grid gap-2">
|
|
||||||
{episodes.slice(0, 3).map((episode) => (
|
|
||||||
<button
|
|
||||||
key={`${episode.podcast}-${episode.slug}`}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onOpenEpisode(episode)}
|
|
||||||
className="cursor-pointer text-left"
|
|
||||||
>
|
|
||||||
<Card className="rounded-sm border-border p-3 transition-colors hover:border-zinc-700">
|
|
||||||
<Typography variant="small" className="font-semibold text-foreground">
|
|
||||||
{episode.title}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="small" className="mt-0.5 text-muted-foreground">
|
|
||||||
{episode.date}
|
|
||||||
</Typography>
|
|
||||||
</Card>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContentContent({
|
export function ContentContent({
|
||||||
yazilimEpisodes,
|
|
||||||
masaBasiEpisodes,
|
|
||||||
youtubeLinks,
|
youtubeLinks,
|
||||||
pdfFiles,
|
pdfFiles,
|
||||||
}: ContentContentProps) {
|
}: ContentContentProps) {
|
||||||
const [selectedEpisode, setSelectedEpisode] = useState<PodcastEpisode | null>(null);
|
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
|
||||||
const [pdfModalOpen, setPdfModalOpen] = useState(false);
|
const [pdfModalOpen, setPdfModalOpen] = useState(false);
|
||||||
const [activePdfIndex, setActivePdfIndex] = useState(0);
|
const [activePdfIndex, setActivePdfIndex] = useState(0);
|
||||||
|
|
||||||
@@ -169,11 +109,6 @@ export function ContentContent({
|
|||||||
[youtubeLinks],
|
[youtubeLinks],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openEpisode = (episode: PodcastEpisode) => {
|
|
||||||
setSelectedEpisode(episode);
|
|
||||||
setSheetOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openPdfModal = (index: number) => {
|
const openPdfModal = (index: number) => {
|
||||||
setActivePdfIndex(index);
|
setActivePdfIndex(index);
|
||||||
setPdfModalOpen(true);
|
setPdfModalOpen(true);
|
||||||
@@ -181,21 +116,6 @@ export function ContentContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex h-full flex-col gap-3 overflow-y-auto">
|
<section className="flex h-full flex-col gap-3 overflow-y-auto">
|
||||||
<section className="grid gap-3 md:grid-cols-2">
|
|
||||||
<PodcastColumn
|
|
||||||
title="Poyraz ile Yazılım"
|
|
||||||
subtitle="Düzenli yayın, her pazar."
|
|
||||||
episodes={yazilimEpisodes}
|
|
||||||
onOpenEpisode={openEpisode}
|
|
||||||
/>
|
|
||||||
<PodcastColumn
|
|
||||||
title="Poyraz ile Masa Başı"
|
|
||||||
subtitle="Düzensiz yayın, konuk odaklı."
|
|
||||||
episodes={masaBasiEpisodes}
|
|
||||||
onOpenEpisode={openEpisode}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<Typography variant="large" className="text-base">
|
<Typography variant="large" className="text-base">
|
||||||
Son YouTube Videoları
|
Son YouTube Videoları
|
||||||
@@ -247,73 +167,6 @@ export function ContentContent({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
||||||
<SheetContent side="right" className="w-full max-w-xl p-0">
|
|
||||||
<div className="border-b border-border px-4 py-3">
|
|
||||||
<SheetTitle>{selectedEpisode?.title ?? "Bölüm"}</SheetTitle>
|
|
||||||
{selectedEpisode ? (
|
|
||||||
<Typography variant="small" className="mt-1 text-muted-foreground">
|
|
||||||
{selectedEpisode.date}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex max-h-[calc(100dvh-64px)] flex-col gap-3 overflow-y-auto p-4">
|
|
||||||
{selectedEpisode ? (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Badge className="rounded-sm">{getPodcastLabel(selectedEpisode.podcast)}</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Link
|
|
||||||
href={selectedEpisode.youtubeUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-sm border border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<Icon icon="mdi:youtube" width={16} height={16} className="text-red-600" />
|
|
||||||
YouTube
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={selectedEpisode.spotifyUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-sm border border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<Icon icon="mdi:spotify" width={16} height={16} className="text-green-600" />
|
|
||||||
Spotify
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="rounded-sm border-border p-3">
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[remarkGfm]}
|
|
||||||
components={{
|
|
||||||
h2: ({ children }) => (
|
|
||||||
<Typography variant="large" className="mt-3 text-foreground first:mt-0">
|
|
||||||
{children}
|
|
||||||
</Typography>
|
|
||||||
),
|
|
||||||
p: ({ children }) => (
|
|
||||||
<Typography variant="small" className="mt-1 text-muted-foreground first:mt-0">
|
|
||||||
{children}
|
|
||||||
</Typography>
|
|
||||||
),
|
|
||||||
li: ({ children }) => (
|
|
||||||
<li className="ml-5 list-disc text-sm text-muted-foreground">{children}</li>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedEpisode.markdown}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
|
|
||||||
<Modal open={pdfModalOpen} onOpenChange={setPdfModalOpen}>
|
<Modal open={pdfModalOpen} onOpenChange={setPdfModalOpen}>
|
||||||
<ModalContent size="xl" className="rounded-sm p-4">
|
<ModalContent size="xl" className="rounded-sm p-4">
|
||||||
<ModalTitle>{activePdf ? activePdf.replace(/\.pdf$/i, "") : "PDF Notu"}</ModalTitle>
|
<ModalTitle>{activePdf ? activePdf.replace(/\.pdf$/i, "") : "PDF Notu"}</ModalTitle>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Poyraz ile Masa Başı Yakında Başlıyor"
|
|
||||||
date: "2026-03-07"
|
|
||||||
youtubeUrl: "https://youtube.com/@poyrazavsever"
|
|
||||||
spotifyUrl: "https://open.spotify.com/show/placeholder-masabasi"
|
|
||||||
podcast: "masa-basi"
|
|
||||||
---
|
|
||||||
|
|
||||||
## Durum
|
|
||||||
|
|
||||||
Poyraz ile Masa Başı serisi henüz başlamadı.
|
|
||||||
|
|
||||||
## Not
|
|
||||||
|
|
||||||
İlk bölüm yayınlandığında bu alandaki içerik güncellenecek.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Poyraz ile yazılım'a dair yakında başlıyor."
|
|
||||||
date: "2026-02-23"
|
|
||||||
youtubeUrl: "https://www.youtube.com/watch?v=XWUeVzf0t6Y&t=2s"
|
|
||||||
spotifyUrl: "https://open.spotify.com/show/placeholder-yazilim"
|
|
||||||
podcast: "yazilim"
|
|
||||||
---
|
|
||||||
|
|
||||||
## Poyraz ile yazılım'a dair yakında başlıyor.
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
import { listBlogDetails } from "@/data/blog-detail";
|
import { listBlogDetails } from "@/data/blog-detail";
|
||||||
import type { PodcastEpisode } from "@/data/content-types";
|
|
||||||
import { getPodcastCollections } from "@/lib/content-page";
|
|
||||||
|
|
||||||
export type BlogNewsItem = {
|
export type BlogNewsItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,26 +24,11 @@ export type BlogArticleItem = {
|
|||||||
author: string;
|
author: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BlogPodcastItem = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
date: string;
|
|
||||||
href: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BlogPodcastGroup = {
|
|
||||||
id: "yazilim" | "masa-basi";
|
|
||||||
title: string;
|
|
||||||
href: string;
|
|
||||||
items: BlogPodcastItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BlogPageData = {
|
export type BlogPageData = {
|
||||||
news: BlogNewsItem[];
|
news: BlogNewsItem[];
|
||||||
articles: BlogArticleItem[];
|
articles: BlogArticleItem[];
|
||||||
categories: string[];
|
categories: string[];
|
||||||
selectedCategory: string;
|
selectedCategory: string;
|
||||||
podcastGroups: BlogPodcastGroup[];
|
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
};
|
};
|
||||||
@@ -80,15 +63,6 @@ function normalizeCategory(value: string) {
|
|||||||
return value.trim().toLocaleLowerCase();
|
return value.trim().toLocaleLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapEpisodeToPodcastItem(episode: PodcastEpisode): BlogPodcastItem {
|
|
||||||
return {
|
|
||||||
id: `${episode.podcast}-${episode.slug}`,
|
|
||||||
title: episode.title,
|
|
||||||
date: episode.date,
|
|
||||||
href: "/content",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllBlogArticles(): Promise<BlogArticleItem[]> {
|
export async function getAllBlogArticles(): Promise<BlogArticleItem[]> {
|
||||||
const posts = await listBlogDetails();
|
const posts = await listBlogDetails();
|
||||||
|
|
||||||
@@ -143,8 +117,6 @@ export async function getBlogPageData(
|
|||||||
const start = (currentPage - 1) * pageSize;
|
const start = (currentPage - 1) * pageSize;
|
||||||
const paginated = filteredArticles.slice(start, start + pageSize);
|
const paginated = filteredArticles.slice(start, start + pageSize);
|
||||||
|
|
||||||
const podcastCollections = await getPodcastCollections();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
news: articles.slice(0, 4).map((item) => ({
|
news: articles.slice(0, 4).map((item) => ({
|
||||||
id: `blog-news-${item.slug}`,
|
id: `blog-news-${item.slug}`,
|
||||||
@@ -159,19 +131,5 @@ export async function getBlogPageData(
|
|||||||
selectedCategory,
|
selectedCategory,
|
||||||
totalPages,
|
totalPages,
|
||||||
currentPage,
|
currentPage,
|
||||||
podcastGroups: [
|
|
||||||
{
|
|
||||||
id: "yazilim",
|
|
||||||
title: "Poyraz ile Yazılım",
|
|
||||||
href: "/content",
|
|
||||||
items: podcastCollections.yazilim.slice(0, 4).map(mapEpisodeToPodcastItem),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "masa-basi",
|
|
||||||
title: "Poyraz ile Masa Başı",
|
|
||||||
href: "/content",
|
|
||||||
items: podcastCollections.masaBasi.slice(0, 4).map(mapEpisodeToPodcastItem),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export type PodcastKind = "yazilim" | "masa-basi";
|
|
||||||
|
|
||||||
export type PodcastEpisode = {
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
date: string;
|
|
||||||
youtubeUrl: string;
|
|
||||||
spotifyUrl: string;
|
|
||||||
podcast: PodcastKind;
|
|
||||||
markdown: string;
|
|
||||||
};
|
|
||||||
@@ -61,10 +61,10 @@ const blogItems: CommandPaletteItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "blog-content-page",
|
id: "blog-content-page",
|
||||||
label: "Podcast ve İçerikler",
|
label: "Video ve Notlar",
|
||||||
href: "/content",
|
href: "/content",
|
||||||
icon: "mdi:microphone-outline",
|
icon: "mdi:video-outline",
|
||||||
keywords: ["podcast", "yazılım", "masa başı", "içerik", "content"],
|
keywords: ["video", "youtube", "not", "pdf", "içerik", "content"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+1
-64
@@ -1,71 +1,8 @@
|
|||||||
import { readdir, readFile } from "node:fs/promises";
|
import { readdir } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import matter from "gray-matter";
|
|
||||||
import type { PodcastEpisode, PodcastKind } from "@/data/content-types";
|
|
||||||
|
|
||||||
type EpisodeFrontmatter = {
|
|
||||||
title: string;
|
|
||||||
date: string;
|
|
||||||
youtubeUrl: string;
|
|
||||||
spotifyUrl: string;
|
|
||||||
podcast: PodcastKind;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PODCAST_DIR = path.join(process.cwd(), "content", "podcasts");
|
|
||||||
const PDF_DIR = path.join(process.cwd(), "public", "pdf");
|
const PDF_DIR = path.join(process.cwd(), "public", "pdf");
|
||||||
|
|
||||||
function sortEpisodesByDateDesc(episodes: PodcastEpisode[]) {
|
|
||||||
return [...episodes].sort((a, b) => {
|
|
||||||
const aTime = Number(new Date(a.date));
|
|
||||||
const bTime = Number(new Date(b.date));
|
|
||||||
|
|
||||||
if (!Number.isNaN(aTime) && !Number.isNaN(bTime)) {
|
|
||||||
return bTime - aTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.title.localeCompare(b.title);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readPodcastEpisodes(kind: PodcastKind): Promise<PodcastEpisode[]> {
|
|
||||||
const folder = path.join(PODCAST_DIR, kind);
|
|
||||||
const files = await readdir(folder);
|
|
||||||
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
||||||
|
|
||||||
const episodes = await Promise.all(
|
|
||||||
mdFiles.map(async (file) => {
|
|
||||||
const fullPath = path.join(folder, file);
|
|
||||||
const raw = await readFile(fullPath, "utf8");
|
|
||||||
const parsed = matter(raw);
|
|
||||||
const data = parsed.data as Partial<EpisodeFrontmatter>;
|
|
||||||
|
|
||||||
return {
|
|
||||||
slug: file.replace(/\.md$/, ""),
|
|
||||||
title: data.title ?? "Başlıksız Bölüm",
|
|
||||||
date: data.date ?? "1970-01-01",
|
|
||||||
youtubeUrl: data.youtubeUrl ?? "",
|
|
||||||
spotifyUrl: data.spotifyUrl ?? "",
|
|
||||||
podcast: data.podcast ?? kind,
|
|
||||||
markdown: parsed.content.trim(),
|
|
||||||
} satisfies PodcastEpisode;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return sortEpisodesByDateDesc(episodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPodcastCollections() {
|
|
||||||
const [yazilim, masaBasi] = await Promise.all([
|
|
||||||
readPodcastEpisodes("yazilim"),
|
|
||||||
readPodcastEpisodes("masa-basi"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
yazilim,
|
|
||||||
masaBasi,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPdfNotes() {
|
export async function getPdfNotes() {
|
||||||
const files = await readdir(PDF_DIR);
|
const files = await readdir(PDF_DIR);
|
||||||
const pdfFiles = files.filter((file) => file.toLowerCase().endsWith(".pdf"));
|
const pdfFiles = files.filter((file) => file.toLowerCase().endsWith(".pdf"));
|
||||||
|
|||||||
Reference in New Issue
Block a user