feat: enhance components with new features and optimizations
- Added support for conditional rendering of the NekoFollower component in AppShell based on ENABLE_NEKO_FOLLOWER. - Introduced MarkdownImage component in BlogDetailContent for handling images in markdown, supporting both local and external assets. - Improved BlogToc component to use IntersectionObserver for better performance in tracking active headings. - Refactored ContentContent to utilize YoutubeLiteEmbed for embedding YouTube videos, simplifying the video rendering logic. - Created a new YoutubeLiteEmbed component for lazy loading YouTube videos with thumbnail previews. - Updated site-settings to include ENABLE_NEKO_FOLLOWER constant for feature toggling. - Enhanced next.config.ts with image optimization settings for better performance. - Refactored snippets-content to improve code readability and structure. - Cleaned up home-hero and home-videos-section components for better layout and performance.
This commit is contained in:
@@ -6,7 +6,7 @@ import { usePathname } from "next/navigation";
|
|||||||
import { AnnouncementBar } from "poyraz-ui/organisms";
|
import { AnnouncementBar } from "poyraz-ui/organisms";
|
||||||
import { SiteNavbar } from "@/components/site-navbar";
|
import { SiteNavbar } from "@/components/site-navbar";
|
||||||
import { NekoFollower } from "@/components/neko-follower";
|
import { NekoFollower } from "@/components/neko-follower";
|
||||||
import { ANNOUNCEMENT_ITEMS } from "@/data/site-settings";
|
import { ANNOUNCEMENT_ITEMS, ENABLE_NEKO_FOLLOWER } from "@/data/site-settings";
|
||||||
|
|
||||||
type AppShellProps = {
|
type AppShellProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -15,7 +15,8 @@ type AppShellProps = {
|
|||||||
export function AppShell({ children }: AppShellProps) {
|
export function AppShell({ children }: AppShellProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const announcement = ANNOUNCEMENT_ITEMS[0];
|
const announcement = ANNOUNCEMENT_ITEMS[0];
|
||||||
const isStandaloneLinksPage = pathname === "/links" || pathname.startsWith("/links/");
|
const isStandaloneLinksPage =
|
||||||
|
pathname === "/links" || pathname.startsWith("/links/");
|
||||||
|
|
||||||
if (isStandaloneLinksPage) {
|
if (isStandaloneLinksPage) {
|
||||||
return children;
|
return children;
|
||||||
@@ -23,7 +24,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NekoFollower />
|
{ENABLE_NEKO_FOLLOWER ? <NekoFollower /> : null}
|
||||||
<div className="mx-auto flex w-full max-w-4xl flex-col px-4 py-4 sm:px-6">
|
<div className="mx-auto flex w-full max-w-4xl flex-col px-4 py-4 sm:px-6">
|
||||||
<SiteNavbar />
|
<SiteNavbar />
|
||||||
{announcement ? (
|
{announcement ? (
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
||||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
||||||
import { Badge, Card, Typography } from "poyraz-ui/atoms";
|
import { Badge, Card, Typography } from "poyraz-ui/atoms";
|
||||||
import type { BlogDetail } from "@/data/blog-detail";
|
import type { BlogDetail } from "@/data/blog-detail";
|
||||||
import { BlogToc } from "@/components/blog-toc";
|
import { BlogToc } from "@/components/blog-toc";
|
||||||
@@ -16,6 +15,59 @@ type BlogDetailContentProps = {
|
|||||||
post: BlogDetail;
|
post: BlogDetail;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isHttpUrl(value: string) {
|
||||||
|
return value.startsWith("http://") || value.startsWith("https://");
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarkdownImage({ src, alt }: { src?: string; alt?: string }) {
|
||||||
|
if (!src) return null;
|
||||||
|
|
||||||
|
const caption = alt || "";
|
||||||
|
const isLocalAsset = src.startsWith("/");
|
||||||
|
const isExternalAsset = isHttpUrl(src);
|
||||||
|
|
||||||
|
if (!isLocalAsset && !isExternalAsset) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="my-4 overflow-hidden rounded-sm border-border">
|
||||||
|
{isLocalAsset ? (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={caption}
|
||||||
|
width={1200}
|
||||||
|
height={675}
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 900px"
|
||||||
|
loading="lazy"
|
||||||
|
className="h-auto w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={caption}
|
||||||
|
width={1200}
|
||||||
|
height={675}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority="low"
|
||||||
|
referrerPolicy="strict-origin-when-cross-origin"
|
||||||
|
className="h-auto w-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{caption ? (
|
||||||
|
<div className="px-3 py-2">
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
{caption}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function slugify(text: string) {
|
function slugify(text: string) {
|
||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -31,7 +83,10 @@ function extractText(children: React.ReactNode): string {
|
|||||||
if (typeof children === "string") return children;
|
if (typeof children === "string") return children;
|
||||||
if (Array.isArray(children)) return children.map(extractText).join("");
|
if (Array.isArray(children)) return children.map(extractText).join("");
|
||||||
if (children && typeof children === "object" && "props" in children) {
|
if (children && typeof children === "object" && "props" in children) {
|
||||||
return extractText((children as React.ReactElement<{ children?: React.ReactNode }>).props.children);
|
return extractText(
|
||||||
|
(children as React.ReactElement<{ children?: React.ReactNode }>).props
|
||||||
|
.children,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -47,7 +102,11 @@ function MermaidBlock({ chart }: { chart: string }) {
|
|||||||
const renderChart = async () => {
|
const renderChart = async () => {
|
||||||
try {
|
try {
|
||||||
const mermaid = (await import("mermaid")).default;
|
const mermaid = (await import("mermaid")).default;
|
||||||
mermaid.initialize({ startOnLoad: false, theme: "neutral", securityLevel: "loose" });
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: "neutral",
|
||||||
|
securityLevel: "loose",
|
||||||
|
});
|
||||||
const { svg: rendered } = await mermaid.render(idRef.current, chart);
|
const { svg: rendered } = await mermaid.render(idRef.current, chart);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -96,7 +155,8 @@ function MermaidBlock({ chart }: { chart: string }) {
|
|||||||
|
|
||||||
const GISCUS_REPO = process.env.NEXT_PUBLIC_GISCUS_REPO || "";
|
const GISCUS_REPO = process.env.NEXT_PUBLIC_GISCUS_REPO || "";
|
||||||
const GISCUS_REPO_ID = process.env.NEXT_PUBLIC_GISCUS_REPO_ID || "";
|
const GISCUS_REPO_ID = process.env.NEXT_PUBLIC_GISCUS_REPO_ID || "";
|
||||||
const GISCUS_CATEGORY = process.env.NEXT_PUBLIC_GISCUS_CATEGORY || "Announcements";
|
const GISCUS_CATEGORY =
|
||||||
|
process.env.NEXT_PUBLIC_GISCUS_CATEGORY || "Announcements";
|
||||||
const GISCUS_CATEGORY_ID = process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID || "";
|
const GISCUS_CATEGORY_ID = process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID || "";
|
||||||
|
|
||||||
export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
||||||
@@ -109,7 +169,9 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
const update = () => {
|
const update = () => {
|
||||||
const bar = progressBarRef.current;
|
const bar = progressBarRef.current;
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
const docHeight =
|
||||||
|
document.documentElement.scrollHeight -
|
||||||
|
document.documentElement.clientHeight;
|
||||||
const pct = docHeight <= 0 ? 100 : (window.scrollY / docHeight) * 100;
|
const pct = docHeight <= 0 ? 100 : (window.scrollY / docHeight) * 100;
|
||||||
bar.style.width = `${Math.min(100, Math.max(0, pct))}%`;
|
bar.style.width = `${Math.min(100, Math.max(0, pct))}%`;
|
||||||
};
|
};
|
||||||
@@ -140,7 +202,10 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
|
|
||||||
<header className="space-y-3">
|
<header className="space-y-3">
|
||||||
<Badge className="rounded-sm">{post.category}</Badge>
|
<Badge className="rounded-sm">{post.category}</Badge>
|
||||||
<Typography variant="small" className="block text-muted-foreground">
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="block text-muted-foreground"
|
||||||
|
>
|
||||||
{post.author} · {post.date} · {post.readTime}
|
{post.author} · {post.date} · {post.readTime}
|
||||||
</Typography>
|
</Typography>
|
||||||
</header>
|
</header>
|
||||||
@@ -153,7 +218,11 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
const text = extractText(children);
|
const text = extractText(children);
|
||||||
const id = slugify(text);
|
const id = slugify(text);
|
||||||
return (
|
return (
|
||||||
<Typography variant="h2" className="mt-10 mb-3 border-b border-border pb-3" id={id}>
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
className="mt-10 mb-3 border-b border-border pb-3"
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@@ -162,7 +231,11 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
const text = extractText(children);
|
const text = extractText(children);
|
||||||
const id = slugify(text);
|
const id = slugify(text);
|
||||||
return (
|
return (
|
||||||
<Typography variant="h3" className="mt-8 mb-2 border-b border-border pb-2" id={id}>
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
className="mt-8 mb-2 border-b border-border pb-2"
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@@ -171,7 +244,11 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
const text = extractText(children);
|
const text = extractText(children);
|
||||||
const id = slugify(text);
|
const id = slugify(text);
|
||||||
return (
|
return (
|
||||||
<Typography variant="large" className="mt-6 mb-1 text-foreground" id={id}>
|
<Typography
|
||||||
|
variant="large"
|
||||||
|
className="mt-6 mb-1 text-foreground"
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@@ -180,22 +257,35 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
const text = extractText(children);
|
const text = extractText(children);
|
||||||
const id = slugify(text);
|
const id = slugify(text);
|
||||||
return (
|
return (
|
||||||
<Typography variant="p" className="mt-4 mb-1 font-semibold text-foreground" id={id}>
|
<Typography
|
||||||
|
variant="p"
|
||||||
|
className="mt-4 mb-1 font-semibold text-foreground"
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
p: ({ node, children, ...props }) => {
|
p: ({ node, children, ...props }) => {
|
||||||
const hasImg = node?.children?.some((c: any) => c.tagName === "img");
|
const hasImg = node?.children?.some(
|
||||||
|
(c: any) => c.tagName === "img",
|
||||||
|
);
|
||||||
if (hasImg) {
|
if (hasImg) {
|
||||||
return (
|
return (
|
||||||
<div className="text-sm leading-7 text-foreground/85 [&:not(:first-child)]:mt-6" {...props}>
|
<div
|
||||||
|
className="text-sm leading-7 text-foreground/85 not-first:mt-6"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Typography variant="p" className="text-sm leading-7 text-foreground/85" {...props}>
|
<Typography
|
||||||
|
variant="p"
|
||||||
|
className="text-sm leading-7 text-foreground/85"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@@ -204,7 +294,9 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
<ul className="my-2 space-y-1.5 pl-1">{children}</ul>
|
<ul className="my-2 space-y-1.5 pl-1">{children}</ul>
|
||||||
),
|
),
|
||||||
ol: ({ children }) => (
|
ol: ({ children }) => (
|
||||||
<ol className="my-2 space-y-1.5 pl-1 list-decimal list-inside">{children}</ol>
|
<ol className="my-2 space-y-1.5 pl-1 list-decimal list-inside">
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
),
|
),
|
||||||
li: ({ children }) => (
|
li: ({ children }) => (
|
||||||
<li className="flex items-start gap-2 text-sm leading-7 text-foreground/85">
|
<li className="flex items-start gap-2 text-sm leading-7 text-foreground/85">
|
||||||
@@ -216,7 +308,11 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
target={href?.startsWith("http") ? "_blank" : undefined}
|
target={href?.startsWith("http") ? "_blank" : undefined}
|
||||||
rel={href?.startsWith("http") ? "noopener noreferrer" : undefined}
|
rel={
|
||||||
|
href?.startsWith("http")
|
||||||
|
? "noopener noreferrer"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className="text-red-600 underline decoration-red-600/30 underline-offset-2 transition-colors hover:decoration-red-600"
|
className="text-red-600 underline decoration-red-600/30 underline-offset-2 transition-colors hover:decoration-red-600"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -229,37 +325,37 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
),
|
),
|
||||||
hr: () => (
|
hr: () => <div className="my-6 border-t border-border" />,
|
||||||
<div className="my-6 border-t border-border" />
|
|
||||||
),
|
|
||||||
table: ({ children }) => (
|
table: ({ children }) => (
|
||||||
<Card className="my-4 overflow-hidden rounded-sm border-border">
|
<Card className="my-4 overflow-hidden rounded-sm border-border">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full border-collapse text-sm">{children}</table>
|
<table className="min-w-full border-collapse text-sm">
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
),
|
),
|
||||||
thead: ({ children }) => <thead className="bg-muted/50">{children}</thead>,
|
thead: ({ children }) => (
|
||||||
tr: ({ children }) => <tr className="border-b border-border">{children}</tr>,
|
<thead className="bg-muted/50">{children}</thead>
|
||||||
|
),
|
||||||
|
tr: ({ children }) => (
|
||||||
|
<tr className="border-b border-border">{children}</tr>
|
||||||
|
),
|
||||||
th: ({ children }) => (
|
th: ({ children }) => (
|
||||||
<th className="px-4 py-2.5 text-left text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<th className="px-4 py-2.5 text-left text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
{children}
|
{children}
|
||||||
</th>
|
</th>
|
||||||
),
|
),
|
||||||
td: ({ children }) => (
|
td: ({ children }) => (
|
||||||
<td className="px-4 py-2.5 align-top text-sm text-foreground">{children}</td>
|
<td className="px-4 py-2.5 align-top text-sm text-foreground">
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
),
|
),
|
||||||
img: ({ src, alt }) => (
|
img: ({ src, alt }) => (
|
||||||
<Card className="my-4 overflow-hidden rounded-sm border-border">
|
<MarkdownImage
|
||||||
<img src={src} alt={alt || ""} className="w-full object-cover" />
|
src={typeof src === "string" ? src : undefined}
|
||||||
{alt && (
|
alt={typeof alt === "string" ? alt : undefined}
|
||||||
<div className="px-3 py-2">
|
/>
|
||||||
<Typography variant="small" className="text-center text-muted-foreground">
|
|
||||||
{alt}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
),
|
),
|
||||||
code: ({ className, children }) => {
|
code: ({ className, children }) => {
|
||||||
const match = /language-(\w+)/.exec(className ?? "");
|
const match = /language-(\w+)/.exec(className ?? "");
|
||||||
@@ -279,19 +375,14 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SyntaxHighlighter
|
<Card className="my-3 overflow-hidden rounded-sm border-border">
|
||||||
language={language}
|
<div className="border-b border-border bg-muted/40 px-3 py-1.5 text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||||
style={vscDarkPlus}
|
{language || "code"}
|
||||||
customStyle={{
|
</div>
|
||||||
borderRadius: "0.25rem",
|
<pre className="overflow-x-auto bg-zinc-950/95 p-3 text-[13px] leading-6 text-zinc-100">
|
||||||
marginTop: "0.75rem",
|
<code>{codeText}</code>
|
||||||
marginBottom: "0.75rem",
|
</pre>
|
||||||
fontSize: "0.8125rem",
|
</Card>
|
||||||
}}
|
|
||||||
showLineNumbers
|
|
||||||
>
|
|
||||||
{codeText}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -330,7 +421,12 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
|
|||||||
className="fixed right-4 bottom-6 z-40 flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-border bg-background shadow-lg transition-transform hover:scale-105 active:scale-95 lg:hidden"
|
className="fixed right-4 bottom-6 z-40 flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-border bg-background shadow-lg transition-transform hover:scale-105 active:scale-95 lg:hidden"
|
||||||
aria-label="İçindekiler"
|
aria-label="İçindekiler"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:table-of-contents" width={22} height={22} className="text-red-600" />
|
<Icon
|
||||||
|
icon="mdi:table-of-contents"
|
||||||
|
width={22}
|
||||||
|
height={22}
|
||||||
|
className="text-red-600"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Mobil İçindekiler Drawer */}
|
{/* Mobil İçindekiler Drawer */}
|
||||||
|
|||||||
+47
-27
@@ -44,49 +44,66 @@ export function BlogToc({ markdown, onNavigate }: BlogTocProps) {
|
|||||||
const [activeId, setActiveId] = useState("");
|
const [activeId, setActiveId] = useState("");
|
||||||
const rafRef = useRef(0);
|
const rafRef = useRef(0);
|
||||||
|
|
||||||
const handleClick = useCallback((id: string) => {
|
const handleClick = useCallback(
|
||||||
const target = document.getElementById(id);
|
(id: string) => {
|
||||||
if (!target) return;
|
const target = document.getElementById(id);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
setActiveId(id);
|
setActiveId(id);
|
||||||
onNavigate?.();
|
onNavigate?.();
|
||||||
}, [onNavigate]);
|
},
|
||||||
|
[onNavigate],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (headings.length === 0) return;
|
if (headings.length === 0) return;
|
||||||
|
|
||||||
const OFFSET = 120;
|
const headingElements = headings
|
||||||
|
.map((heading) => ({
|
||||||
|
id: heading.id,
|
||||||
|
element: document.getElementById(heading.id),
|
||||||
|
}))
|
||||||
|
.filter((item): item is { id: string; element: HTMLElement } =>
|
||||||
|
Boolean(item.element),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (headingElements.length === 0) return;
|
||||||
|
|
||||||
const updateActive = () => {
|
const updateActive = () => {
|
||||||
let current = "";
|
let current = headingElements[0].id;
|
||||||
|
|
||||||
for (const heading of headings) {
|
for (const item of headingElements) {
|
||||||
const el = document.getElementById(heading.id);
|
if (item.element.getBoundingClientRect().top <= 120) {
|
||||||
if (!el) continue;
|
current = item.id;
|
||||||
|
continue;
|
||||||
const top = el.getBoundingClientRect().top;
|
|
||||||
if (top <= OFFSET) {
|
|
||||||
current = heading.id;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current) {
|
setActiveId(current);
|
||||||
setActiveId(current);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onScroll = () => {
|
const observer = new IntersectionObserver(
|
||||||
cancelAnimationFrame(rafRef.current);
|
() => {
|
||||||
rafRef.current = requestAnimationFrame(updateActive);
|
cancelAnimationFrame(rafRef.current);
|
||||||
};
|
rafRef.current = requestAnimationFrame(updateActive);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: null,
|
||||||
|
rootMargin: "-120px 0px -65% 0px",
|
||||||
|
threshold: [0, 1],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
window.addEventListener("scroll", onScroll, { passive: true });
|
for (const item of headingElements) {
|
||||||
|
observer.observe(item.element);
|
||||||
|
}
|
||||||
|
|
||||||
const timer = setTimeout(updateActive, 300);
|
const timer = setTimeout(updateActive, 200);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("scroll", onScroll);
|
observer.disconnect();
|
||||||
cancelAnimationFrame(rafRef.current);
|
cancelAnimationFrame(rafRef.current);
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
@@ -96,7 +113,10 @@ export function BlogToc({ markdown, onNavigate }: BlogTocProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav aria-label="İçindekiler" className="space-y-1">
|
<nav aria-label="İçindekiler" className="space-y-1">
|
||||||
<Typography variant="small" className="mb-2 font-semibold text-foreground">
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="mb-2 font-semibold text-foreground"
|
||||||
|
>
|
||||||
İçindekiler
|
İçindekiler
|
||||||
</Typography>
|
</Typography>
|
||||||
{headings.map((heading, index) => (
|
{headings.map((heading, index) => (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Button, Card, Typography } from "poyraz-ui/atoms";
|
import { Button, Card, Typography } from "poyraz-ui/atoms";
|
||||||
import { Modal, ModalContent, ModalTitle } from "poyraz-ui/molecules";
|
import { Modal, ModalContent, ModalTitle } from "poyraz-ui/molecules";
|
||||||
import { getYoutubeEmbedUrl } from "@/lib/youtube";
|
import { YoutubeLiteEmbed } from "@/components/youtube-lite-embed";
|
||||||
|
|
||||||
type ContentContentProps = {
|
type ContentContentProps = {
|
||||||
youtubeLinks: readonly string[];
|
youtubeLinks: readonly string[];
|
||||||
@@ -16,14 +16,20 @@ function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let loadingTask: { promise: Promise<unknown>; destroy?: () => void } | null = null;
|
let loadingTask: {
|
||||||
|
promise: Promise<unknown>;
|
||||||
|
destroy?: () => void;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
const render = async () => {
|
const render = async () => {
|
||||||
try {
|
try {
|
||||||
const pdfjs = await import("pdfjs-dist");
|
const pdfjs = await import("pdfjs-dist");
|
||||||
const lib = pdfjs as unknown as {
|
const lib = pdfjs as unknown as {
|
||||||
version: string;
|
version: string;
|
||||||
getDocument: (src: string) => { promise: Promise<unknown>; destroy?: () => void };
|
getDocument: (src: string) => {
|
||||||
|
promise: Promise<unknown>;
|
||||||
|
destroy?: () => void;
|
||||||
|
};
|
||||||
GlobalWorkerOptions: { workerSrc: string };
|
GlobalWorkerOptions: { workerSrc: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +38,10 @@ function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
|||||||
|
|
||||||
const pdf = (await loadingTask.promise) as {
|
const pdf = (await loadingTask.promise) as {
|
||||||
getPage: (page: number) => Promise<{
|
getPage: (page: number) => Promise<{
|
||||||
getViewport: (opts: { scale: number }) => { width: number; height: number };
|
getViewport: (opts: { scale: number }) => {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
render: (opts: {
|
render: (opts: {
|
||||||
canvasContext: CanvasRenderingContext2D;
|
canvasContext: CanvasRenderingContext2D;
|
||||||
viewport: { width: number; height: number };
|
viewport: { width: number; height: number };
|
||||||
@@ -85,7 +94,11 @@ function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white p-2">
|
<div className="w-full bg-white p-2">
|
||||||
<canvas ref={canvasRef} aria-label={title} className="block h-auto w-full" />
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
aria-label={title}
|
||||||
|
className="block h-auto w-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,10 +115,7 @@ export function ContentContent({
|
|||||||
const canGoNext = activePdfIndex < pdfFiles.length - 1;
|
const canGoNext = activePdfIndex < pdfFiles.length - 1;
|
||||||
|
|
||||||
const embeddedVideos = useMemo(
|
const embeddedVideos = useMemo(
|
||||||
() =>
|
() => youtubeLinks.slice(0, 3),
|
||||||
youtubeLinks
|
|
||||||
.map((link) => ({ link, embedUrl: getYoutubeEmbedUrl(link) }))
|
|
||||||
.slice(0, 3),
|
|
||||||
[youtubeLinks],
|
[youtubeLinks],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -121,27 +131,12 @@ export function ContentContent({
|
|||||||
Son YouTube Videoları
|
Son YouTube Videoları
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className="grid gap-2 md:grid-cols-3">
|
<div className="grid gap-2 md:grid-cols-3">
|
||||||
{embeddedVideos.map((item) => (
|
{embeddedVideos.map((link) => (
|
||||||
<Card key={item.link} className="overflow-hidden rounded-sm border-border p-0">
|
<Card
|
||||||
{item.embedUrl ? (
|
key={link}
|
||||||
<div className="aspect-video w-full">
|
className="overflow-hidden rounded-sm border-border p-0"
|
||||||
<iframe
|
>
|
||||||
src={item.embedUrl}
|
<YoutubeLiteEmbed link={link} title="YouTube video oynatici" />
|
||||||
title="YouTube video oynatıcı"
|
|
||||||
className="h-full w-full"
|
|
||||||
loading="lazy"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
||||||
referrerPolicy="strict-origin-when-cross-origin"
|
|
||||||
allowFullScreen
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="p-3">
|
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
|
||||||
Geçersiz video bağlantısı.
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -160,7 +155,10 @@ export function ContentContent({
|
|||||||
className="cursor-pointer text-left"
|
className="cursor-pointer text-left"
|
||||||
>
|
>
|
||||||
<Card className="overflow-hidden rounded-sm border-border p-0 transition-colors hover:border-zinc-700">
|
<Card className="overflow-hidden rounded-sm border-border p-0 transition-colors hover:border-zinc-700">
|
||||||
<PdfFirstPagePreview src={`/pdf/${pdf}`} title={`${pdf} önizleme`} />
|
<PdfFirstPagePreview
|
||||||
|
src={`/pdf/${pdf}`}
|
||||||
|
title={`${pdf} önizleme`}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -169,11 +167,15 @@ export function ContentContent({
|
|||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<div className="mt-3 flex flex-wrap items-center justify-between gap-2">
|
<div className="mt-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
{pdfFiles.length === 0 ? "0 / 0" : `${activePdfIndex + 1} / ${pdfFiles.length}`}
|
{pdfFiles.length === 0
|
||||||
|
? "0 / 0"
|
||||||
|
: `${activePdfIndex + 1} / ${pdfFiles.length}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -181,7 +183,9 @@ export function ContentContent({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="rounded-sm"
|
className="rounded-sm"
|
||||||
disabled={!canGoPrev}
|
disabled={!canGoPrev}
|
||||||
onClick={() => setActivePdfIndex((prev) => Math.max(0, prev - 1))}
|
onClick={() =>
|
||||||
|
setActivePdfIndex((prev) => Math.max(0, prev - 1))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Önceki
|
Önceki
|
||||||
</Button>
|
</Button>
|
||||||
@@ -190,7 +194,11 @@ export function ContentContent({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="rounded-sm"
|
className="rounded-sm"
|
||||||
disabled={!canGoNext}
|
disabled={!canGoNext}
|
||||||
onClick={() => setActivePdfIndex((prev) => Math.min(pdfFiles.length - 1, prev + 1))}
|
onClick={() =>
|
||||||
|
setActivePdfIndex((prev) =>
|
||||||
|
Math.min(pdfFiles.length - 1, prev + 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Sonraki
|
Sonraki
|
||||||
</Button>
|
</Button>
|
||||||
@@ -199,7 +207,11 @@ export function ContentContent({
|
|||||||
|
|
||||||
{activePdf ? (
|
{activePdf ? (
|
||||||
<div className="mt-3 h-[70dvh] overflow-hidden rounded-sm border border-border">
|
<div className="mt-3 h-[70dvh] overflow-hidden rounded-sm border border-border">
|
||||||
<iframe src={`/pdf/${activePdf}`} title={activePdf} className="h-full w-full" />
|
<iframe
|
||||||
|
src={`/pdf/${activePdf}`}
|
||||||
|
title={activePdf}
|
||||||
|
className="h-full w-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Card className="mt-3 rounded-sm border-border p-3">
|
<Card className="mt-3 rounded-sm border-border p-3">
|
||||||
|
|||||||
+32
-31
@@ -4,7 +4,6 @@ import Image from "next/image";
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { Card, Typography } from "poyraz-ui/atoms";
|
import { Card, Typography } from "poyraz-ui/atoms";
|
||||||
import { NewsCard } from "poyraz-ui/molecules";
|
import { NewsCard } from "poyraz-ui/molecules";
|
||||||
import { StaggerContainer, StaggerItem, FadeIn } from "@/components/motion-wrapper";
|
|
||||||
|
|
||||||
type HomeHeroProps = {
|
type HomeHeroProps = {
|
||||||
news: {
|
news: {
|
||||||
@@ -39,9 +38,9 @@ export function HomeHero({ news }: HomeHeroProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid gap-3 md:h-65 md:grid-cols-2">
|
<section className="grid gap-3 md:h-65 md:grid-cols-2">
|
||||||
<StaggerContainer className="grid gap-2 md:grid-rows-3">
|
<div className="grid gap-2 md:grid-rows-3">
|
||||||
{news.map((item) => (
|
{news.map((item) => (
|
||||||
<StaggerItem key={item.id}>
|
<div key={item.id}>
|
||||||
<NewsCard
|
<NewsCard
|
||||||
className="rounded-sm border-border md:h-full"
|
className="rounded-sm border-border md:h-full"
|
||||||
category={item.category}
|
category={item.category}
|
||||||
@@ -50,40 +49,42 @@ export function HomeHero({ news }: HomeHeroProps) {
|
|||||||
image={item.image}
|
image={item.image}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
/>
|
/>
|
||||||
</StaggerItem>
|
</div>
|
||||||
))}
|
))}
|
||||||
</StaggerContainer>
|
</div>
|
||||||
|
|
||||||
<FadeIn delay={0.15}>
|
<Card className="grid grid-cols-[112px_1fr] items-stretch gap-2 rounded-sm border-border sm:grid-cols-[168px_1fr] md:h-full">
|
||||||
<Card className="grid grid-cols-[112px_1fr] items-stretch gap-2 rounded-sm border-border sm:grid-cols-[168px_1fr] md:h-full">
|
<div className="flex flex-col justify-center gap-1 px-3 py-2">
|
||||||
<div className="flex flex-col justify-center gap-1 px-3 py-2">
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<Typography variant="h2" className="leading-tight">
|
||||||
<Typography variant="h2" className="leading-tight">
|
Poyraz{" "}
|
||||||
Poyraz <span className="font-secondary text-red-600">Avsever</span>
|
<span className="font-secondary text-red-600">Avsever</span>
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
|
||||||
Teknolojiyi merak eden bir genç. Yazılım geliştirme, yapay zeka ve teknoloji dünyasındaki gelişmeleri takip ediyor.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
|
Teknolojiyi merak eden bir genç. Yazılım geliştirme, yapay zeka ve
|
||||||
|
teknoloji dünyasındaki gelişmeleri takip ediyor.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative h-full overflow-hidden">
|
<div className="relative h-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="relative h-full w-full"
|
className="relative h-full w-full"
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={frame === 1 ? "/images/hero1.png" : "/images/hero2.png"}
|
src={frame === 1 ? "/images/hero1.png" : "/images/hero2.png"}
|
||||||
alt="Poyraz Avsever"
|
alt="Poyraz Avsever"
|
||||||
width={240}
|
width={240}
|
||||||
height={240}
|
height={240}
|
||||||
className="absolute right-0 bottom-0 h-auto w-20 object-contain md:w-48"
|
sizes="(max-width: 768px) 80px, 192px"
|
||||||
/>
|
priority
|
||||||
</div>
|
className="absolute right-0 bottom-0 h-auto w-20 object-contain md:w-48"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</FadeIn>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,19 @@
|
|||||||
import { Card, Typography } from "poyraz-ui/atoms";
|
import { Card } from "poyraz-ui/atoms";
|
||||||
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
|
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
|
||||||
import { getYoutubeEmbedUrl } from "@/lib/youtube";
|
import { YoutubeLiteEmbed } from "@/components/youtube-lite-embed";
|
||||||
|
|
||||||
export function HomeVideosSection() {
|
export function HomeVideosSection() {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<div className="grid gap-2 md:grid-cols-3">
|
<div className="grid gap-2 md:grid-cols-3">
|
||||||
{YOUTUBE_VIDEO_LINKS.map((link) => {
|
{YOUTUBE_VIDEO_LINKS.map((link) => (
|
||||||
const embedUrl = getYoutubeEmbedUrl(link);
|
<Card
|
||||||
|
key={link}
|
||||||
return (
|
className="overflow-hidden rounded-sm border-border p-0"
|
||||||
<Card key={link} className="overflow-hidden rounded-sm border-border p-0">
|
>
|
||||||
{embedUrl ? (
|
<YoutubeLiteEmbed link={link} title="YouTube video oynatici" />
|
||||||
<div className="aspect-video w-full">
|
</Card>
|
||||||
<iframe
|
))}
|
||||||
src={embedUrl}
|
|
||||||
title="YouTube video oynatıcı"
|
|
||||||
className="h-full w-full"
|
|
||||||
loading="lazy"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
||||||
referrerPolicy="strict-origin-when-cross-origin"
|
|
||||||
allowFullScreen
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="p-3">
|
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
|
||||||
Geçersiz video bağlantısı.
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage, Separator } from "poyraz-ui/atoms";
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
Separator,
|
||||||
|
} from "poyraz-ui/atoms";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -18,10 +24,14 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "poyraz-ui/molecules";
|
} from "poyraz-ui/molecules";
|
||||||
import { SearchCommand } from "@/components/search-command";
|
|
||||||
import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label";
|
import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label";
|
||||||
import { NAV_LINKS, SOCIAL_LINKS, TOP_ICON_LINKS } from "@/lib/links";
|
import { NAV_LINKS, SOCIAL_LINKS, TOP_ICON_LINKS } from "@/lib/links";
|
||||||
|
|
||||||
|
const SearchCommand = dynamic(
|
||||||
|
() => import("@/components/search-command").then((mod) => mod.SearchCommand),
|
||||||
|
{ ssr: false },
|
||||||
|
);
|
||||||
|
|
||||||
function getNavLinkClass(isActive: boolean) {
|
function getNavLinkClass(isActive: boolean) {
|
||||||
return [
|
return [
|
||||||
"inline-flex border-b-2 pb-1 text-sm transition-colors",
|
"inline-flex border-b-2 pb-1 text-sm transition-colors",
|
||||||
@@ -60,7 +70,11 @@ export function SiteNavbar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header className="flex items-center justify-between gap-3 border-b border-border pb-4">
|
<header className="flex items-center justify-between gap-3 border-b border-border pb-4">
|
||||||
<Link href="/" aria-label="Ana sayfaya git" className="inline-flex items-center">
|
<Link
|
||||||
|
href="/"
|
||||||
|
aria-label="Ana sayfaya git"
|
||||||
|
className="inline-flex items-center"
|
||||||
|
>
|
||||||
<Avatar className="h-9 w-9 rounded-sm">
|
<Avatar className="h-9 w-9 rounded-sm">
|
||||||
<AvatarImage src="/logo/logo.jpeg" alt="Poyraz Avsever" />
|
<AvatarImage src="/logo/logo.jpeg" alt="Poyraz Avsever" />
|
||||||
<AvatarFallback className="rounded-sm bg-muted text-xs font-semibold">
|
<AvatarFallback className="rounded-sm bg-muted text-xs font-semibold">
|
||||||
@@ -75,9 +89,16 @@ export function SiteNavbar() {
|
|||||||
{NAV_LINKS.map((item, index) => (
|
{NAV_LINKS.map((item, index) => (
|
||||||
<li key={item.id} className="flex items-center gap-3">
|
<li key={item.id} className="flex items-center gap-3">
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
<Separator orientation="vertical" className="h-4 bg-border" decorative />
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
className="h-4 bg-border"
|
||||||
|
decorative
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Link href={item.href} className={getNavLinkClass(isActiveLink(item.href))}>
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className={getNavLinkClass(isActiveLink(item.href))}
|
||||||
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -85,7 +106,11 @@ export function SiteNavbar() {
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<Separator orientation="vertical" className="h-4 bg-border" decorative />
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
className="h-4 bg-border"
|
||||||
|
decorative
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSearchOpen(true)}
|
onClick={() => setSearchOpen(true)}
|
||||||
@@ -144,7 +169,9 @@ export function SiteNavbar() {
|
|||||||
<Icon icon="mdi:magnify" width={16} height={16} />
|
<Icon icon="mdi:magnify" width={16} height={16} />
|
||||||
<span>Ara</span>
|
<span>Ara</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground/80">{shortcut}</span>
|
<span className="text-xs text-muted-foreground/80">
|
||||||
|
{shortcut}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</SheetClose>
|
</SheetClose>
|
||||||
|
|
||||||
@@ -193,4 +220,3 @@ export function SiteNavbar() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Badge, Card, Typography } from "poyraz-ui/atoms";
|
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";
|
import type { Snippet } from "@/data/snippets";
|
||||||
|
|
||||||
type SnippetsContentProps = {
|
type SnippetsContentProps = {
|
||||||
@@ -17,12 +14,16 @@ function extractCode(markdown: string) {
|
|||||||
return match ? match[1].trim() : markdown;
|
return match ? match[1].trim() : markdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SnippetsContent({ snippets, categories }: SnippetsContentProps) {
|
export function SnippetsContent({
|
||||||
|
snippets,
|
||||||
|
categories,
|
||||||
|
}: SnippetsContentProps) {
|
||||||
const [selected, setSelected] = useState("All");
|
const [selected, setSelected] = useState("All");
|
||||||
|
|
||||||
const filtered = selected === "All"
|
const filtered =
|
||||||
? snippets
|
selected === "All"
|
||||||
: snippets.filter((s) => s.category === selected);
|
? snippets
|
||||||
|
: snippets.filter((s) => s.category === selected);
|
||||||
|
|
||||||
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">
|
||||||
@@ -30,7 +31,8 @@ export function SnippetsContent({ snippets, categories }: SnippetsContentProps)
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h3">
|
<Typography variant="h3">
|
||||||
Kod <span className="font-secondary text-red-600">Parçacıkları</span>
|
Kod{" "}
|
||||||
|
<span className="font-secondary text-red-600">Parçacıkları</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small" className="mt-1 text-muted-foreground">
|
<Typography variant="small" className="mt-1 text-muted-foreground">
|
||||||
Sıkça kullandığım, tekrar kullanılabilir kod parçacıkları.
|
Sıkça kullandığım, tekrar kullanılabilir kod parçacıkları.
|
||||||
@@ -51,16 +53,22 @@ export function SnippetsContent({ snippets, categories }: SnippetsContentProps)
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<StaggerContainer className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
{filtered.map((snippet) => (
|
{filtered.map((snippet) => (
|
||||||
<StaggerItem key={snippet.slug}>
|
<div key={snippet.slug}>
|
||||||
<Card className="flex h-full flex-col rounded-sm border-border">
|
<Card className="flex h-full flex-col rounded-sm border-border">
|
||||||
<div className="flex items-start justify-between gap-2 p-4 pb-2">
|
<div className="flex items-start justify-between gap-2 p-4 pb-2">
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="large" className="text-base leading-tight">
|
<Typography
|
||||||
|
variant="large"
|
||||||
|
className="text-base leading-tight"
|
||||||
|
>
|
||||||
{snippet.title}
|
{snippet.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small" className="mt-1 text-muted-foreground">
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="mt-1 text-muted-foreground"
|
||||||
|
>
|
||||||
{snippet.description}
|
{snippet.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,23 +77,19 @@ export function SnippetsContent({ snippets, categories }: SnippetsContentProps)
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 px-4 pb-4">
|
<div className="flex-1 px-4 pb-4">
|
||||||
<SyntaxHighlighter
|
<Card className="overflow-hidden rounded-sm border-border p-0">
|
||||||
language={snippet.language}
|
<div className="border-b border-border bg-muted/40 px-3 py-1.5 text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||||
style={vscDarkPlus}
|
{snippet.language || "code"}
|
||||||
customStyle={{
|
</div>
|
||||||
borderRadius: "0.25rem",
|
<pre className="overflow-x-auto bg-zinc-950/95 p-3 text-[13px] leading-6 text-zinc-100">
|
||||||
margin: 0,
|
<code>{extractCode(snippet.markdown)}</code>
|
||||||
fontSize: "0.8125rem",
|
</pre>
|
||||||
}}
|
</Card>
|
||||||
showLineNumbers
|
|
||||||
>
|
|
||||||
{extractCode(snippet.markdown)}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</StaggerItem>
|
</div>
|
||||||
))}
|
))}
|
||||||
</StaggerContainer>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<Card className="rounded-sm border-border p-5">
|
<Card className="rounded-sm border-border p-5">
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Typography } from "poyraz-ui/atoms";
|
||||||
|
import { getYoutubeEmbedUrl, getYoutubeVideoId } from "@/lib/youtube";
|
||||||
|
|
||||||
|
type YoutubeLiteEmbedProps = {
|
||||||
|
link: string;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function YoutubeLiteEmbed({
|
||||||
|
link,
|
||||||
|
title = "YouTube video oynatici",
|
||||||
|
}: YoutubeLiteEmbedProps) {
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
const [useJpgThumb, setUseJpgThumb] = useState(false);
|
||||||
|
|
||||||
|
const { videoId, embedUrl } = useMemo(
|
||||||
|
() => ({
|
||||||
|
videoId: getYoutubeVideoId(link),
|
||||||
|
embedUrl: getYoutubeEmbedUrl(link),
|
||||||
|
}),
|
||||||
|
[link],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!videoId || !embedUrl) {
|
||||||
|
return (
|
||||||
|
<div className="flex aspect-video w-full items-center justify-center p-3">
|
||||||
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
|
Gecersiz video baglantisi.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoaded) {
|
||||||
|
return (
|
||||||
|
<div className="aspect-video w-full">
|
||||||
|
<iframe
|
||||||
|
src={embedUrl}
|
||||||
|
title={title}
|
||||||
|
className="h-full w-full"
|
||||||
|
loading="lazy"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
referrerPolicy="strict-origin-when-cross-origin"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailSrc = useJpgThumb
|
||||||
|
? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`
|
||||||
|
: `https://i.ytimg.com/vi_webp/${videoId}/hqdefault.webp`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsLoaded(true)}
|
||||||
|
className="group relative block aspect-video w-full cursor-pointer overflow-hidden"
|
||||||
|
aria-label={`${title} videosunu oynat`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={thumbnailSrc}
|
||||||
|
alt={title}
|
||||||
|
fill
|
||||||
|
className="object-cover transition-transform duration-200 group-hover:scale-[1.02]"
|
||||||
|
sizes="(max-width: 768px) 100vw, 33vw"
|
||||||
|
onError={() => setUseJpgThumb(true)}
|
||||||
|
/>
|
||||||
|
<span className="absolute inset-0 bg-black/20 transition-colors group-hover:bg-black/30" />
|
||||||
|
<span className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-red-600/90 text-white shadow-lg">
|
||||||
|
<span className="ml-0.5 inline-block h-0 w-0 border-y-[9px] border-y-transparent border-l-14 border-l-white" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
actionHref: string;
|
actionHref: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ENABLE_NEKO_FOLLOWER = false;
|
||||||
|
|
||||||
export const ANNOUNCEMENT_ITEMS: AnnouncementItem[] = [
|
export const ANNOUNCEMENT_ITEMS: AnnouncementItem[] = [
|
||||||
{
|
{
|
||||||
id: "main-announcement",
|
id: "main-announcement",
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
compress: true,
|
||||||
images: {
|
images: {
|
||||||
|
formats: ["image/avif", "image/webp"],
|
||||||
|
minimumCacheTTL: 60 * 60 * 24 * 30,
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "ghchart.rshah.org",
|
hostname: "ghchart.rshah.org",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "i.ytimg.com",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user