Files
poyraz-portfolio/components/content-content.tsx
T

262 lines
8.2 KiB
TypeScript

"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { Icon } from "@iconify/react";
import { Button, ButtonIcon, ButtonLabel, Card, Typography } from "poyraz-ui/atoms";
import { Modal, ModalContent, ModalTitle } from "poyraz-ui/molecules";
import { YoutubeLiteEmbed } from "@/components/youtube-lite-embed";
import { useTranslations } from "next-intl";
type ContentContentProps = {
youtubeLinks: readonly string[];
pdfFiles: string[];
};
function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
const t = useTranslations("Content");
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
let cancelled = false;
let loadingTask: {
promise: Promise<unknown>;
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<unknown>;
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<void> };
}>;
};
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 });
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 = "100%";
canvas.style.height = "auto";
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 (
<div className="flex h-full w-full items-center justify-center bg-muted/20 p-2">
<Typography variant="small" className="text-muted-foreground">
{t("pdfPreviewError")}
</Typography>
</div>
);
}
return (
<div className="w-full bg-white p-2">
<canvas
ref={canvasRef}
aria-label={title}
className="block h-auto w-full"
/>
</div>
);
}
export function ContentContent({
youtubeLinks,
pdfFiles,
}: ContentContentProps) {
const t = useTranslations("Content");
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.slice(0, 3),
[youtubeLinks],
);
const openPdfModal = (index: number) => {
setActivePdfIndex(index);
setPdfModalOpen(true);
};
return (
<section className="flex h-full flex-col gap-3 overflow-y-auto">
<section className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Typography variant="large" className="text-base">
{t("youtubeTitle")}
</Typography>
<Button asChild variant="outline" effect="swap" size="xs" radius="sm">
<a
href="https://youtube.com/@poyrazavsever"
target="_blank"
rel="noopener noreferrer"
aria-label={t("youtubeChannel")}
>
<ButtonIcon>
<Icon icon="mdi:youtube" width={15} height={15} />
</ButtonIcon>
<ButtonLabel>@poyrazavsever</ButtonLabel>
</a>
</Button>
</div>
<div className="grid gap-2 md:grid-cols-3">
{embeddedVideos.map((link) => (
<Card
key={link}
className="overflow-hidden rounded-sm border-border p-0"
>
<YoutubeLiteEmbed link={link} title={t("youtubeEmbedTitle")} />
</Card>
))}
</div>
</section>
<section className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Typography variant="large" className="text-base">
{t("pdfTitle")}
</Typography>
<Button asChild variant="outline" effect="swap" size="xs" radius="sm">
<a
href="https://www.linkedin.com/in/poyrazavsever/"
target="_blank"
rel="noopener noreferrer"
aria-label={t("linkedinProfile")}
>
<ButtonIcon>
<Icon icon="mdi:linkedin" width={15} height={15} />
</ButtonIcon>
<ButtonLabel>@poyrazavsever</ButtonLabel>
</a>
</Button>
</div>
<div className="grid grid-cols-2 gap-2 lg:grid-cols-3">
{pdfFiles.map((pdf, index) => (
<button
key={pdf}
type="button"
onClick={() => openPdfModal(index)}
className="cursor-pointer text-left"
>
<Card className="overflow-hidden rounded-sm border-border p-0 transition-colors hover:border-zinc-700">
<PdfFirstPagePreview
src={`/pdf/${pdf}`}
title={t("pdfPreviewTitle", { name: pdf })}
/>
</Card>
</button>
))}
</div>
</section>
<Modal open={pdfModalOpen} onOpenChange={setPdfModalOpen}>
<ModalContent size="xl" className="rounded-sm p-4">
<ModalTitle>
{activePdf ? activePdf.replace(/\.pdf$/i, "") : t("pdfModalDefaultTitle")}
</ModalTitle>
<div className="mt-3 flex flex-wrap items-center justify-between gap-2">
<Typography variant="small" className="text-muted-foreground">
{pdfFiles.length === 0
? "0 / 0"
: `${activePdfIndex + 1} / ${pdfFiles.length}`}
</Typography>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
className="rounded-sm"
disabled={!canGoPrev}
onClick={() =>
setActivePdfIndex((prev) => Math.max(0, prev - 1))
}
>
{t("prev")}
</Button>
<Button
type="button"
variant="outline"
className="rounded-sm"
disabled={!canGoNext}
onClick={() =>
setActivePdfIndex((prev) =>
Math.min(pdfFiles.length - 1, prev + 1),
)
}
>
{t("next")}
</Button>
</div>
</div>
{activePdf ? (
<div className="mt-3 h-[70dvh] overflow-hidden rounded-sm border border-border">
<iframe
src={`/pdf/${activePdf}`}
title={activePdf}
className="h-full w-full"
/>
</div>
) : (
<Card className="mt-3 rounded-sm border-border p-3">
<Typography variant="small" className="text-muted-foreground">
{t("pdfNotFound")}
</Typography>
</Card>
)}
</ModalContent>
</Modal>
</section>
);
}