diff --git a/app/layout.tsx b/app/layout.tsx index 2966036..8699f75 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,12 +1,7 @@ import type { Metadata } from "next"; import "./globals.css"; -import { SiteNavbar } from "@/components/site-navbar"; -import { NekoFollower } from "@/components/neko-follower"; -import { ANNOUNCEMENT_ITEMS } from "@/data/site-settings"; -import { Icon } from "@iconify/react"; -import Link from "next/link"; -import { AnnouncementBar } from "poyraz-ui/organisms"; +import { AppShell } from "@/components/app-shell"; export const metadata: Metadata = { metadataBase: new URL("https://poyrazavsever.com"), @@ -87,37 +82,10 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const announcement = ANNOUNCEMENT_ITEMS[0]; - return ( - -
- - {announcement ? ( - } - action={ - announcement.actionLabel && announcement.actionHref ? ( - - {announcement.actionLabel} - - ) : null - } - > - {announcement.text} - - ) : null} -
{children}
-
+ {children} ); diff --git a/app/links/page.tsx b/app/links/page.tsx index 746ca09..2d2cfb3 100644 --- a/app/links/page.tsx +++ b/app/links/page.tsx @@ -1,4 +1,35 @@ -import { LinksContent } from "@/components/links-content"; +import type { Metadata } from "next"; +import { LinksContent } from "@/components/links-content"; + +export const metadata: Metadata = { + title: "Links", + description: + "Poyraz Avsever'in sosyal medya, portfolyo ve içerik bağlantılarına tek sayfadan ulaş.", + alternates: { + canonical: "/links", + }, + openGraph: { + title: "Poyraz Avsever | Links", + description: + "Poyraz Avsever'in sosyal medya, portfolyo ve içerik bağlantılarına tek sayfadan ulaş.", + url: "https://poyrazavsever.com/links", + images: [ + { + url: "/logo/cover.png", + width: 1536, + height: 512, + alt: "Poyraz Avsever Links sayfası kapak görseli", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "Poyraz Avsever | Links", + description: + "Poyraz Avsever'in sosyal medya, portfolyo ve içerik bağlantılarına tek sayfadan ulaş.", + images: ["/logo/cover.png"], + }, +}; export default function LinksPage() { return ; diff --git a/components/app-shell.tsx b/components/app-shell.tsx new file mode 100644 index 0000000..60c1ba5 --- /dev/null +++ b/components/app-shell.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { Icon } from "@iconify/react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { AnnouncementBar } from "poyraz-ui/organisms"; +import { SiteNavbar } from "@/components/site-navbar"; +import { NekoFollower } from "@/components/neko-follower"; +import { ANNOUNCEMENT_ITEMS } from "@/data/site-settings"; + +type AppShellProps = { + children: React.ReactNode; +}; + +export function AppShell({ children }: AppShellProps) { + const pathname = usePathname(); + const announcement = ANNOUNCEMENT_ITEMS[0]; + const isStandaloneLinksPage = pathname === "/links" || pathname.startsWith("/links/"); + + if (isStandaloneLinksPage) { + return children; + } + + return ( + <> + +
+ + {announcement ? ( + } + action={ + announcement.actionLabel && announcement.actionHref ? ( + + {announcement.actionLabel} + + ) : null + } + > + {announcement.text} + + ) : null} +
{children}
+
+ + ); +} diff --git a/components/links-content.tsx b/components/links-content.tsx index edd17e8..6c1f38e 100644 --- a/components/links-content.tsx +++ b/components/links-content.tsx @@ -1,33 +1,52 @@ "use client"; +import Image from "next/image"; import { Icon } from "@iconify/react"; import Link from "next/link"; import { useMemo, useState } from "react"; import { - Badge, - Button, + Avatar, + AvatarFallback, + AvatarImage, Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, Input, Typography, } from "poyraz-ui/atoms"; -import { Tabs, TabsList, TabsTrigger } from "poyraz-ui/molecules"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules"; import { LINK_DIRECTORY, LINK_DIRECTORY_CATEGORIES, + SOCIAL_LINKS, + TOP_ICON_LINKS, type LinkDirectoryCategory, } from "@/lib/links"; type CategoryFilter = "all" | LinkDirectoryCategory; const FILTER_ITEMS: ReadonlyArray<{ id: CategoryFilter; label: string }> = [ - { id: "all", label: "Tümü" }, + { id: "all", label: "Tüm kategoriler" }, ...LINK_DIRECTORY_CATEGORIES, ]; +const CATEGORY_LABELS = Object.fromEntries( + LINK_DIRECTORY_CATEGORIES.map((item) => [item.id, item.label]), +) as Record<(typeof LINK_DIRECTORY_CATEGORIES)[number]["id"], string>; + +const CATEGORY_ORDER = { + resources: 0, + navigation: 1, + social: 2, +} as const; + +const DIRECTORY_ITEMS = [...LINK_DIRECTORY].sort((left, right) => { + const categoryCompare = CATEGORY_ORDER[left.category] - CATEGORY_ORDER[right.category]; + if (categoryCompare !== 0) { + return categoryCompare; + } + + return left.label.localeCompare(right.label, "tr"); +}); + function normalize(value: string) { return value .toLocaleLowerCase("tr-TR") @@ -35,135 +54,226 @@ function normalize(value: string) { .replace(/[\u0300-\u036f]/g, ""); } -function getCategoryLabel(category: LinkDirectoryCategory) { - return LINK_DIRECTORY_CATEGORIES.find((item) => item.id === category)?.label ?? "Kategori"; +function formatHref(href: string) { + if (href.startsWith("mailto:")) { + return href.replace("mailto:", ""); + } + + if (href.startsWith("/")) { + return `poyrazavsever.com${href}`; + } + + return href.replace(/^https?:\/\//, "").replace(/\/$/, ""); } export function LinksContent() { const [activeCategory, setActiveCategory] = useState("all"); const [query, setQuery] = useState(""); - const [copiedId, setCopiedId] = useState(null); - const filteredLinks = useMemo(() => { + const filteredItems = useMemo(() => { const normalizedQuery = normalize(query.trim()); const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean); - return LINK_DIRECTORY.filter((item) => { - const categoryMatch = activeCategory === "all" || item.category === activeCategory; - if (!categoryMatch) return false; - if (queryTokens.length === 0) return true; + return DIRECTORY_ITEMS.filter((item) => { + if (activeCategory !== "all" && item.category !== activeCategory) { + return false; + } - const haystack = normalize([ - item.label, - item.href, - ...item.keywords, - getCategoryLabel(item.category), - ].join(" ")); + if (queryTokens.length === 0) { + return true; + } + + const haystack = normalize( + [item.label, item.href, CATEGORY_LABELS[item.category], ...item.keywords].join(" "), + ); return queryTokens.every((token) => haystack.includes(token)); }); }, [activeCategory, query]); - const handleCopy = async (id: string, href: string) => { - try { - await navigator.clipboard.writeText(href); - setCopiedId(id); - setTimeout(() => { - setCopiedId((current) => (current === id ? null : current)); - }, 1400); - } catch { - setCopiedId(null); - } - }; - return ( -
- - - Link Merkezi - - Tüm sosyal medya, sayfa ve kaynak linklerini tek yerden ara, filtrele ve yönet. - - +
- -
- setQuery(event.target.value)} - placeholder="Linklerde ara... (örn: github, medium, /blog)" - aria-label="Linklerde arama" +
+ +
+ Poyraz Avsever kapak görseli - - {filteredLinks.length} / {LINK_DIRECTORY.length} sonuç - +
- setActiveCategory(value as CategoryFilter)} - > - - {FILTER_ITEMS.map((item) => ( - - {item.label} - - ))} - - - - +
+
+ + + + PA + + - {filteredLinks.length === 0 ? ( - - - Arama kriterine uygun link bulunamadı. - - - ) : ( -
- {filteredLinks.map((item) => ( - - -
- - - -
- {item.label} - {item.href} -
+
+
+ + Poyraz + + + Avsever +
- {getCategoryLabel(item.category)} - - -
+ +
+ {SOCIAL_LINKS.map((item) => ( + + + ))} +
+ +
+ {TOP_ICON_LINKS.map((item) => ( + - Aç + +
+ + + +
+ + {item.label} + + + {formatHref(item.href)} + +
+
+
- + ))} +
- - - - ))} -
- )} +
+
+ + Tüm Linkler + + + Sayfalar, kaynaklar ve sosyal profiller + +
+ +
+ + + setQuery(event.target.value)} + placeholder="Link ara... github, medium, /blog" + aria-label="Linklerde arama" + className="h-11 rounded-2xl" + /> +
+ +
+ {filteredItems.map((item) => ( + + +
+ + + + +
+
+ + {item.label} + + + {CATEGORY_LABELS[item.category]} + +
+ + + {formatHref(item.href)} + +
+ + + + +
+
+ + ))} + + {filteredItems.length === 0 ? ( + + + Seçili filtreye uygun link bulunamadı. + + + ) : null} +
+
+
+
+ +
); } diff --git a/public/logo/cover.png b/public/logo/cover.png index 9e28ddc..ad0e128 100644 Binary files a/public/logo/cover.png and b/public/logo/cover.png differ