From c253bad8bfba41015e1dd47a0c2457da25af78ce Mon Sep 17 00:00:00 2001 From: poyrazavsever Date: Tue, 10 Mar 2026 22:21:40 +0300 Subject: [PATCH] Add new podcast episodes and update content structure - Created new podcast episode files for "Freelance Client Stories", "Productivity Without Burnout", "Conversations with Builders", "Stable Frontend Systems", "Writing Cleaner Service Layers", and "Foundations Over Tools" with relevant metadata and episode notes. - Introduced a new TypeScript type definition for PodcastKind and PodcastEpisode to enhance type safety. - Implemented a content reading function to fetch and sort podcast episodes by date. - Updated package.json to include new dependencies: gray-matter for markdown parsing and pdfjs-dist for PDF handling. - Added PDF files for Atomic Design and personal resume to the public directory. --- app/content/page.tsx | 19 + components/content-content.tsx | 371 ++++++++++++++++++ .../2026-01-30-freelance-client-stories.md | 16 + ...2026-02-20-productivity-without-burnout.md | 16 + .../2026-03-07-conversations-with-builders.md | 16 + .../2026-02-23-stable-frontend-systems.md | 16 + ...26-03-02-writing-cleaner-service-layers.md | 16 + .../2026-03-09-foundations-over-tools.md | 17 + data/content-types.ts | 11 + lib/content-page.ts | 73 ++++ package.json | 2 + pnpm-lock.yaml | 213 ++++++++++ public/pdf/AtomicDesign.pdf | Bin 0 -> 1517198 bytes public/resume.pdf | Bin 0 -> 69045 bytes 14 files changed, 786 insertions(+) create mode 100644 app/content/page.tsx create mode 100644 components/content-content.tsx create mode 100644 content/podcasts/masa-basi/2026-01-30-freelance-client-stories.md create mode 100644 content/podcasts/masa-basi/2026-02-20-productivity-without-burnout.md create mode 100644 content/podcasts/masa-basi/2026-03-07-conversations-with-builders.md create mode 100644 content/podcasts/yazilim/2026-02-23-stable-frontend-systems.md create mode 100644 content/podcasts/yazilim/2026-03-02-writing-cleaner-service-layers.md create mode 100644 content/podcasts/yazilim/2026-03-09-foundations-over-tools.md create mode 100644 data/content-types.ts create mode 100644 lib/content-page.ts create mode 100644 public/pdf/AtomicDesign.pdf create mode 100644 public/resume.pdf diff --git a/app/content/page.tsx b/app/content/page.tsx new file mode 100644 index 0000000..a986c02 --- /dev/null +++ b/app/content/page.tsx @@ -0,0 +1,19 @@ +import { ContentContent } from "@/components/content-content"; +import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos"; +import { getPdfNotes, getPodcastCollections } from "@/lib/content-page"; + +export default async function ContentPage() { + const [{ yazilim, masaBasi }, pdfFiles] = await Promise.all([ + getPodcastCollections(), + getPdfNotes(), + ]); + + return ( + + ); +} diff --git a/components/content-content.tsx b/components/content-content.tsx new file mode 100644 index 0000000..d784dea --- /dev/null +++ b/components/content-content.tsx @@ -0,0 +1,371 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import Link from "next/link"; +import ReactMarkdown from "react-markdown"; +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"; + +type ContentContentProps = { + yazilimEpisodes: PodcastEpisode[]; + masaBasiEpisodes: PodcastEpisode[]; + youtubeLinks: readonly string[]; + pdfFiles: string[]; +}; + +function PdfFirstPagePreview({ src, title }: { src: string; title: string }) { + const canvasRef = useRef(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + let cancelled = false; + let loadingTask: { promise: Promise; destroy?: () => void } | null = null; + + const render = async () => { + try { + const pdfjs = await import("pdfjs-dist"); + const lib = pdfjs as unknown as { + version: string; + getDocument: (src: string) => { promise: Promise; destroy?: () => void }; + GlobalWorkerOptions: { workerSrc: string }; + }; + + lib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${lib.version}/build/pdf.worker.min.mjs`; + loadingTask = lib.getDocument(src); + + const pdf = (await loadingTask.promise) as { + getPage: (page: number) => Promise<{ + getViewport: (opts: { scale: number }) => { width: number; height: number }; + render: (opts: { + canvasContext: CanvasRenderingContext2D; + viewport: { width: number; height: number }; + }) => { promise: Promise }; + }>; + }; + + const page = await pdf.getPage(1); + const viewport = page.getViewport({ scale: 1.2 }); + const canvas = canvasRef.current; + if (!canvas || cancelled) return; + + const context = canvas.getContext("2d"); + if (!context) return; + + const ratio = window.devicePixelRatio || 1; + canvas.width = Math.floor(viewport.width * ratio); + canvas.height = Math.floor(viewport.height * ratio); + canvas.style.width = `${viewport.width}px`; + canvas.style.height = `${viewport.height}px`; + + context.setTransform(ratio, 0, 0, ratio, 0, 0); + await page.render({ canvasContext: context, viewport }).promise; + } catch { + if (!cancelled) { + setFailed(true); + } + } + }; + + void render(); + + return () => { + cancelled = true; + if (loadingTask?.destroy) { + loadingTask.destroy(); + } + }; + }, [src]); + + if (failed) { + return ( +
+ + Preview unavailable + +
+ ); + } + + return ( +
+ +
+ ); +} + +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, + episodes, + onOpenEpisode, +}: { + title: string; + subtitle: string; + episodes: PodcastEpisode[]; + onOpenEpisode: (episode: PodcastEpisode) => void; +}) { + return ( + + + {title} + + + {subtitle} + + +
+ {episodes.slice(0, 3).map((episode) => ( + + ))} +
+
+ ); +} + +export function ContentContent({ + yazilimEpisodes, + masaBasiEpisodes, + youtubeLinks, + pdfFiles, +}: ContentContentProps) { + const [selectedEpisode, setSelectedEpisode] = useState(null); + const [sheetOpen, setSheetOpen] = useState(false); + const [pdfModalOpen, setPdfModalOpen] = useState(false); + const [activePdfIndex, setActivePdfIndex] = useState(0); + + const activePdf = pdfFiles[activePdfIndex] ?? null; + const canGoPrev = activePdfIndex > 0; + const canGoNext = activePdfIndex < pdfFiles.length - 1; + + const embeddedVideos = useMemo( + () => + youtubeLinks + .map((link) => ({ link, embedUrl: getYoutubeEmbedUrl(link) })) + .slice(0, 3), + [youtubeLinks], + ); + + const openEpisode = (episode: PodcastEpisode) => { + setSelectedEpisode(episode); + setSheetOpen(true); + }; + + const openPdfModal = (index: number) => { + setActivePdfIndex(index); + setPdfModalOpen(true); + }; + + return ( +
+
+ + +
+ +
+ + Latest YouTube Videos + +
+ {embeddedVideos.map((item) => ( + + {item.embedUrl ? ( +
+