feat(links): integrate directory into the main layout

This commit is contained in:
poyrazavsever
2026-08-31 19:39:28 +03:00
parent eca56fab5c
commit d55b04d4b8
2 changed files with 311 additions and 240 deletions
+140 -115
View File
@@ -1,19 +1,26 @@
"use client"; "use client";
import { Icon } from "@iconify/react";
import Image from "next/image";
import { Link } from "@/i18n/routing";
import { useLocale, useTranslations } from "next-intl";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Icon } from "@iconify/react";
import { useLocale, useTranslations } from "next-intl";
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
AvatarImage, AvatarImage,
Badge,
Button,
Card, Card,
Input, Input,
Typography, Typography,
} from "poyraz-ui/atoms"; } from "poyraz-ui/atoms";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules"; import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "poyraz-ui/molecules";
import { Link } from "@/i18n/routing";
import { import {
getResumeHref, getResumeHref,
LINK_DIRECTORY, LINK_DIRECTORY,
@@ -37,12 +44,12 @@ const CATEGORY_ORDER = {
} as const; } as const;
const DIRECTORY_ITEMS = [...LINK_DIRECTORY].sort((left, right) => { const DIRECTORY_ITEMS = [...LINK_DIRECTORY].sort((left, right) => {
const categoryCompare = CATEGORY_ORDER[left.category] - CATEGORY_ORDER[right.category]; const categoryCompare =
if (categoryCompare !== 0) { CATEGORY_ORDER[left.category] - CATEGORY_ORDER[right.category];
return categoryCompare;
}
return left.label.localeCompare(right.label, "tr"); return categoryCompare !== 0
? categoryCompare
: left.label.localeCompare(right.label, "tr");
}); });
function normalize(value: string) { function normalize(value: string) {
@@ -53,13 +60,8 @@ function normalize(value: string) {
} }
function formatHref(href: string) { function formatHref(href: string) {
if (href.startsWith("mailto:")) { if (href.startsWith("mailto:")) return href.replace("mailto:", "");
return href.replace("mailto:", ""); if (href.startsWith("/")) return `poyrazavsever.com${href}`;
}
if (href.startsWith("/")) {
return `poyrazavsever.com${href}`;
}
return href.replace(/^https?:\/\//, "").replace(/\/$/, ""); return href.replace(/^https?:\/\//, "").replace(/\/$/, "");
} }
@@ -72,6 +74,10 @@ function parseCategoryFilter(value?: string): CategoryFilter {
return "all"; return "all";
} }
function LinkIcon({ icon, size = 18 }: { icon: string; size?: number }) {
return <Icon icon={icon} width={size} height={size} />;
}
export function LinksContent({ export function LinksContent({
initialCategory, initialCategory,
initialQuery = "", initialQuery = "",
@@ -79,19 +85,21 @@ export function LinksContent({
const t = useTranslations("Links"); const t = useTranslations("Links");
const tNav = useTranslations("Nav"); const tNav = useTranslations("Nav");
const locale = useLocale(); const locale = useLocale();
const [activeCategory, setActiveCategory] = useState<CategoryFilter>(() => const [activeCategory, setActiveCategory] = useState<CategoryFilter>(() =>
parseCategoryFilter(initialCategory), parseCategoryFilter(initialCategory),
); );
const [query, setQuery] = useState(initialQuery); const [query, setQuery] = useState(initialQuery);
const filterItems = useMemo(() => [ const filterItems = useMemo(
() => [
{ id: "all" as const, label: t("allCategories") }, { id: "all" as const, label: t("allCategories") },
...LINK_DIRECTORY_CATEGORIES.map((item) => ({ ...LINK_DIRECTORY_CATEGORIES.map((item) => ({
id: item.id, id: item.id,
label: t(`categories.${item.id}`), label: t(`categories.${item.id}`),
})), })),
], [t]); ],
[t],
);
const filteredItems = useMemo(() => { const filteredItems = useMemo(() => {
const normalizedQuery = normalize(query.trim()); const normalizedQuery = normalize(query.trim());
@@ -102,9 +110,7 @@ export function LinksContent({
return false; return false;
} }
if (queryTokens.length === 0) { if (queryTokens.length === 0) return true;
return true;
}
const haystack = normalize( const haystack = normalize(
[ [
@@ -120,116 +126,133 @@ export function LinksContent({
}, [activeCategory, query, t, tNav]); }, [activeCategory, query, t, tNav]);
return ( return (
<section className="relative isolate min-h-dvh overflow-hidden px-4 py-8 sm:px-6 sm:py-10"> <section className="flex h-full flex-col gap-10">
<header className="border-b border-border py-4 sm:py-5">
<div className="relative mx-auto flex w-full max-w-xl justify-center"> <div className="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
<Card className="w-full overflow-hidden rounded-[28px] border-border/80 bg-background/95 shadow-[0_24px_80px_rgba(15,23,42,0.16)] backdrop-blur"> <div className="flex min-w-0 items-start gap-4">
<div className="relative aspect-1878/410 overflow-hidden border-b border-border/70"> <Avatar className="size-16 shrink-0 rounded-sm border border-border bg-background sm:size-20">
<Image
src="/logo/cover.png"
alt="Poyraz Avsever kapak görseli"
fill
priority
sizes="(max-width: 640px) 100vw, 576px"
className="object-cover"
/>
<div className="absolute inset-0 bg-linear-to-t from-background/20 via-transparent to-transparent" />
</div>
<div className="px-5 pb-5 pt-0 sm:px-6 sm:pb-6">
<div className="-mt-14 flex flex-col gap-4">
<Avatar className="h-24 w-24 rounded-[28px] border-4 border-background bg-background shadow-lg sm:h-28 sm:w-28">
<AvatarImage src="/logo/logo.webp" alt="Poyraz Avsever" /> <AvatarImage src="/logo/logo.webp" alt="Poyraz Avsever" />
<AvatarFallback className="rounded-3xl bg-muted text-lg font-semibold"> <AvatarFallback className="rounded-sm bg-muted font-semibold">
PA PA
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="space-y-3"> <div className="min-w-0">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<Typography variant="h2" className="text-[1.75rem] leading-none sm:text-[2rem]">
Poyraz
</Typography>
<Typography <Typography
variant="h2" variant="h2"
secondaryFont component="h1"
className="text-[1.75rem] leading-none text-red-600 sm:text-[2rem]" className="font-secondary text-2xl font-semibold leading-none tracking-[-0.045em] text-foreground"
> >
Avsever {t("title")}
</Typography> </Typography>
</div> <Typography
variant="small"
<Typography variant="small" className="max-w-lg text-sm leading-6 text-muted-foreground"> className="mt-2 max-w-xl text-xs leading-5 text-muted-foreground sm:text-sm"
>
{t("desc")} {t("desc")}
</Typography> </Typography>
</div> </div>
<div className="flex flex-wrap gap-2">
{SOCIAL_LINKS.map((item) => (
<a
key={item.id}
href={item.href}
target="_blank"
rel="noreferrer"
aria-label={item.label}
title={item.label}
className="inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-border bg-background text-muted-foreground transition-all hover:-translate-y-0.5 hover:border-red-600/40 hover:text-foreground"
>
<Icon icon={item.icon} width={18} height={18} />
</a>
))}
</div> </div>
<div className="grid gap-2 sm:grid-cols-2"> <div
className="flex max-w-sm flex-wrap gap-2 sm:justify-end"
aria-label={t("socialLinks")}
>
{SOCIAL_LINKS.map((item) => (
<Button
key={item.id}
asChild
variant="secondary"
size="icon-sm"
radius="sm"
effect="shine"
aria-label={item.label}
>
<a
href={item.href}
target={item.id === "email" ? undefined : "_blank"}
rel={item.id === "email" ? undefined : "noreferrer"}
title={item.label}
>
<LinkIcon icon={item.icon} size={16} />
</a>
</Button>
))}
</div>
</div>
</header>
<section className="space-y-3" aria-labelledby="quick-links-title">
<div>
<Typography
id="quick-links-title"
variant="h3"
component="h2"
className="tracking-[-0.035em]"
>
{t("quickLinks")}
</Typography>
<Typography variant="small" className="mt-1 text-muted-foreground">
{t("quickLinksDesc")}
</Typography>
</div>
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
{TOP_ICON_LINKS.map((item) => { {TOP_ICON_LINKS.map((item) => {
const href = item.id === "cv" ? getResumeHref(locale) : item.href; const href = item.id === "cv" ? getResumeHref(locale) : item.href;
return ( return (
<a <a
key={item.id} key={item.id}
href={href} href={href}
target={item.id === "cv" || item.external ? "_blank" : undefined} target={item.id === "cv" || item.external ? "_blank" : undefined}
rel={item.id === "cv" || item.external ? "noreferrer" : undefined} rel={item.id === "cv" || item.external ? "noreferrer" : undefined}
className="block" className="group block"
> >
<Card className="rounded-2xl border-border bg-muted/35 p-3 transition-all hover:-translate-y-0.5 hover:border-red-600/40 hover:bg-background"> <Card className="h-full rounded-sm border-border p-3 transition-[border-color,transform] duration-200 group-hover:-translate-y-0.5 group-hover:border-primary/40">
<div className="flex items-center gap-3"> <span className="inline-flex size-9 items-center justify-center rounded-sm border border-border bg-muted/35 text-muted-foreground">
<span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-border bg-background text-muted-foreground"> <LinkIcon icon={item.icon} />
<Icon icon={item.icon} width={18} height={18} />
</span> </span>
<div className="min-w-0"> <Typography variant="large" className="mt-3 text-sm leading-tight">
<Typography variant="large" className="truncate text-base">
{tNav.has(item.id) ? tNav(item.id) : item.label} {tNav.has(item.id) ? tNav(item.id) : item.label}
</Typography> </Typography>
<Typography <Typography
variant="small" variant="small"
className="truncate text-xs text-muted-foreground" className="mt-1 truncate text-xs text-muted-foreground"
> >
{formatHref(href)} {formatHref(href)}
</Typography> </Typography>
</div>
</div>
</Card> </Card>
</a> </a>
); );
})} })}
</div> </div>
</section>
<div className="space-y-3 pt-1"> <section className="space-y-3" aria-labelledby="all-links-title">
<div className="flex flex-col gap-3 border-b border-border pb-4 md:flex-row md:items-end md:justify-between">
<div> <div>
<Typography variant="large" className="text-base"> <Typography
id="all-links-title"
variant="h3"
component="h2"
className="tracking-[-0.035em]"
>
{t("allLinks")} {t("allLinks")}
</Typography> </Typography>
<Typography variant="small" className="text-muted-foreground"> <Typography variant="small" className="mt-1 text-muted-foreground">
{t("allLinksDesc")} {t("allLinksDesc")}
</Typography> </Typography>
</div> </div>
<div className="grid gap-2 sm:grid-cols-[190px_1fr]"> <div className="grid w-full gap-2 sm:grid-cols-[180px_1fr] md:max-w-lg">
<Select <Select
value={activeCategory} value={activeCategory}
onValueChange={(value) => setActiveCategory(value as CategoryFilter)} onValueChange={(value) =>
setActiveCategory(value as CategoryFilter)
}
> >
<SelectTrigger className="h-11 rounded-2xl border-border bg-background px-4 text-sm"> <SelectTrigger className="h-10 rounded-sm border-border bg-background px-3 text-sm">
<SelectValue placeholder={t("selectCategory")} /> <SelectValue placeholder={t("selectCategory")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -245,32 +268,37 @@ export function LinksContent({
value={query} value={query}
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
placeholder={t("searchPlaceholder")} placeholder={t("searchPlaceholder")}
aria-label={t("allLinks")} aria-label={t("searchAriaLabel")}
className="h-11 rounded-2xl" className="h-10 rounded-sm"
/> />
</div> </div>
</div>
<div className="grid gap-2"> <div className="grid gap-2 md:grid-cols-2">
{filteredItems.map((item) => { {filteredItems.map((item) => {
const href = item.id === "cv" ? getResumeHref(locale) : item.href; const href = item.id === "cv" ? getResumeHref(locale) : item.href;
const isStaticOrExternal = item.external || item.id === "rss" || item.id === "cv" || href.endsWith(".xml") || href.endsWith(".pdf"); const isStaticOrExternal =
const CardContent = ( item.external ||
<Card className="rounded-2xl border-border p-3 transition-all hover:-translate-y-0.5 hover:border-red-600/40"> item.id === "rss" ||
item.id === "cv" ||
href.endsWith(".xml") ||
href.endsWith(".pdf");
const content = (
<Card className="h-full rounded-sm border-border p-3 transition-[border-color,transform] duration-200 group-hover:-translate-y-0.5 group-hover:border-primary/40">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-border bg-muted/35 text-muted-foreground"> <span className="inline-flex size-10 shrink-0 items-center justify-center rounded-sm border border-border bg-muted/35 text-muted-foreground">
<Icon icon={item.icon} width={18} height={18} /> <LinkIcon icon={item.icon} />
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Typography variant="large" className="text-base leading-tight"> <Typography variant="large" className="text-sm leading-tight">
{tNav.has(item.id) ? tNav(item.id) : item.label} {tNav.has(item.id) ? tNav(item.id) : item.label}
</Typography> </Typography>
<span className="inline-flex rounded-full border border-border px-2.5 py-0.5 text-[11px] text-muted-foreground"> <Badge variant="secondary" radius="sm" className="text-[10px]">
{t(`categories.${item.category}`)} {t(`categories.${item.category}`)}
</span> </Badge>
</div> </div>
<Typography <Typography
variant="small" variant="small"
className="mt-1 truncate text-xs text-muted-foreground" className="mt-1 truncate text-xs text-muted-foreground"
@@ -279,13 +307,16 @@ export function LinksContent({
</Typography> </Typography>
</div> </div>
<span className="text-muted-foreground">
<Icon <Icon
icon={isStaticOrExternal ? "mdi:arrow-top-right" : "mdi:arrow-right"} icon={
width={18} isStaticOrExternal
height={18} ? "mdi:arrow-top-right"
: "mdi:arrow-right"
}
width={17}
height={17}
className="shrink-0 text-muted-foreground transition-transform duration-200 group-hover:translate-x-0.5"
/> />
</span>
</div> </div>
</Card> </Card>
); );
@@ -296,36 +327,30 @@ export function LinksContent({
href={href} href={href}
target={item.id === "cv" || item.external ? "_blank" : undefined} target={item.id === "cv" || item.external ? "_blank" : undefined}
rel={item.id === "cv" || item.external ? "noreferrer" : undefined} rel={item.id === "cv" || item.external ? "noreferrer" : undefined}
className="block" className="group block"
> >
{CardContent} {content}
</a> </a>
) : ( ) : (
<Link <Link
key={`${item.category}-${item.id}`} key={`${item.category}-${item.id}`}
href={href} href={href}
target={item.external ? "_blank" : undefined} className="group block"
rel={item.external ? "noreferrer" : undefined}
className="block"
> >
{CardContent} {content}
</Link> </Link>
); );
})} })}
{filteredItems.length === 0 ? ( {filteredItems.length === 0 ? (
<Card className="rounded-2xl border-border px-4 py-5"> <Card className="rounded-sm border-border px-4 py-5 md:col-span-2">
<Typography variant="small" className="text-muted-foreground"> <Typography variant="small" className="text-muted-foreground">
{t("empty")} {t("empty")}
</Typography> </Typography>
</Card> </Card>
) : null} ) : null}
</div> </div>
</div> </section>
</div>
</div>
</Card>
</div>
</section> </section>
); );
} }
+55 -9
View File
@@ -56,12 +56,6 @@ export const SOCIAL_LINKS = [
href: "https://behance.net/poyrazavsever", href: "https://behance.net/poyrazavsever",
icon: "mdi:behance", icon: "mdi:behance",
}, },
{
id: "spotify",
label: "Spotify",
href: "https://open.spotify.com/user/3136fdjkc5p4cbzmuxhvqdd4b2hu",
icon: "mdi:spotify",
},
{ {
id: "buy-me-a-coffee", id: "buy-me-a-coffee",
label: "Bana kahve ısmarla", label: "Bana kahve ısmarla",
@@ -153,15 +147,67 @@ export const LINK_DIRECTORY_CATEGORIES: ReadonlyArray<{
{ id: "resources", label: "Kaynaklar" }, { id: "resources", label: "Kaynaklar" },
]; ];
export const LINK_DIRECTORY: LinkDirectoryItem[] = [ const STATIC_PAGE_LINKS = [
{
id: "home",
label: "Ana Sayfa",
href: "/",
icon: "mdi:home-outline",
keywords: ["ana sayfa", "home", "portfolio"],
},
...NAV_LINKS.map((item) => ({ ...NAV_LINKS.map((item) => ({
...item,
icon: "mdi:compass-outline",
keywords: [item.label, item.href, "sayfa", "navigasyon", "internal"],
})),
...NAV_DROPDOWN_GROUPS.flatMap((group) =>
group.items.map((item) => ({
id: item.id, id: item.id,
label: item.label, label: item.label,
href: item.href, href: item.href,
icon: "mdi:compass-outline", icon: item.icon,
keywords: [...item.keywords],
})),
),
{
id: "references",
label: "Referanslar",
href: "/about/references",
icon: "mdi:comment-quote-outline",
keywords: ["referans", "references", "testimonial", "yorum"],
},
{
id: "volunteerCommunity",
label: "Gönüllülük ve Topluluk",
href: "/about/volunteer-community",
icon: "mdi:account-group-outline",
keywords: ["gönüllülük", "topluluk", "volunteer", "community"],
},
{
id: "mediaKit",
label: "Medya Kiti",
href: "/media-kit",
icon: "mdi:chart-box-outline",
keywords: ["medya kiti", "media kit", "sponsor", "iş birliği"],
},
{
id: "links",
label: "Bağlantılar",
href: "/links",
icon: "mdi:link-variant",
keywords: ["bağlantılar", "links", "link in bio"],
},
] as const;
export const LINK_DIRECTORY: LinkDirectoryItem[] = [
...STATIC_PAGE_LINKS.map((item) => ({
id: item.id,
label: item.label,
href: item.href,
icon: item.icon,
external: false, external: false,
category: "navigation" as const, category: "navigation" as const,
keywords: [item.label, item.href, "sayfa", "navigasyon", "internal"], keywords: [...item.keywords],
})), })),
...SOCIAL_LINKS.map((item) => ({ ...SOCIAL_LINKS.map((item) => ({
id: item.id, id: item.id,