feat: refactor layout and links page, add AppShell component for improved structure
This commit is contained in:
+2
-34
@@ -1,12 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
import { SiteNavbar } from "@/components/site-navbar";
|
import { AppShell } from "@/components/app-shell";
|
||||||
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";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL("https://poyrazavsever.com"),
|
metadataBase: new URL("https://poyrazavsever.com"),
|
||||||
@@ -87,37 +82,10 @@ export default function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const announcement = ANNOUNCEMENT_ITEMS[0];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="tr">
|
<html lang="tr">
|
||||||
<body className="min-h-dvh bg-background text-foreground antialiased">
|
<body className="min-h-dvh bg-background text-foreground antialiased">
|
||||||
<NekoFollower />
|
<AppShell>{children}</AppShell>
|
||||||
<div className="mx-auto flex w-full max-w-4xl flex-col overflow-hidden px-4 py-4 sm:px-6">
|
|
||||||
<SiteNavbar />
|
|
||||||
{announcement ? (
|
|
||||||
<AnnouncementBar
|
|
||||||
variant="branded"
|
|
||||||
dismissible={false}
|
|
||||||
icon={<Icon icon="mdi:sparkles" width={16} height={16} />}
|
|
||||||
action={
|
|
||||||
announcement.actionLabel && announcement.actionHref ? (
|
|
||||||
<Link
|
|
||||||
href={announcement.actionHref}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-xs font-bold underline"
|
|
||||||
>
|
|
||||||
{announcement.actionLabel}
|
|
||||||
</Link>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{announcement.text}
|
|
||||||
</AnnouncementBar>
|
|
||||||
) : null}
|
|
||||||
<main className="flex-1 overflow-hidden py-4">{children}</main>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+32
-1
@@ -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() {
|
export default function LinksPage() {
|
||||||
return <LinksContent />;
|
return <LinksContent />;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<NekoFollower />
|
||||||
|
<div className="mx-auto flex w-full max-w-4xl flex-col overflow-hidden px-4 py-4 sm:px-6">
|
||||||
|
<SiteNavbar />
|
||||||
|
{announcement ? (
|
||||||
|
<AnnouncementBar
|
||||||
|
variant="branded"
|
||||||
|
dismissible={false}
|
||||||
|
icon={<Icon icon="mdi:sparkles" width={16} height={16} />}
|
||||||
|
action={
|
||||||
|
announcement.actionLabel && announcement.actionHref ? (
|
||||||
|
<Link
|
||||||
|
href={announcement.actionHref}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-xs font-bold underline"
|
||||||
|
>
|
||||||
|
{announcement.actionLabel}
|
||||||
|
</Link>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{announcement.text}
|
||||||
|
</AnnouncementBar>
|
||||||
|
) : null}
|
||||||
|
<main className="flex-1 overflow-hidden py-4">{children}</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+213
-103
@@ -1,33 +1,52 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Avatar,
|
||||||
Button,
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
Input,
|
Input,
|
||||||
Typography,
|
Typography,
|
||||||
} from "poyraz-ui/atoms";
|
} from "poyraz-ui/atoms";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "poyraz-ui/molecules";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "poyraz-ui/molecules";
|
||||||
import {
|
import {
|
||||||
LINK_DIRECTORY,
|
LINK_DIRECTORY,
|
||||||
LINK_DIRECTORY_CATEGORIES,
|
LINK_DIRECTORY_CATEGORIES,
|
||||||
|
SOCIAL_LINKS,
|
||||||
|
TOP_ICON_LINKS,
|
||||||
type LinkDirectoryCategory,
|
type LinkDirectoryCategory,
|
||||||
} from "@/lib/links";
|
} from "@/lib/links";
|
||||||
|
|
||||||
type CategoryFilter = "all" | LinkDirectoryCategory;
|
type CategoryFilter = "all" | LinkDirectoryCategory;
|
||||||
|
|
||||||
const FILTER_ITEMS: ReadonlyArray<{ id: CategoryFilter; label: string }> = [
|
const FILTER_ITEMS: ReadonlyArray<{ id: CategoryFilter; label: string }> = [
|
||||||
{ id: "all", label: "Tümü" },
|
{ id: "all", label: "Tüm kategoriler" },
|
||||||
...LINK_DIRECTORY_CATEGORIES,
|
...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) {
|
function normalize(value: string) {
|
||||||
return value
|
return value
|
||||||
.toLocaleLowerCase("tr-TR")
|
.toLocaleLowerCase("tr-TR")
|
||||||
@@ -35,135 +54,226 @@ function normalize(value: string) {
|
|||||||
.replace(/[\u0300-\u036f]/g, "");
|
.replace(/[\u0300-\u036f]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategoryLabel(category: LinkDirectoryCategory) {
|
function formatHref(href: string) {
|
||||||
return LINK_DIRECTORY_CATEGORIES.find((item) => item.id === category)?.label ?? "Kategori";
|
if (href.startsWith("mailto:")) {
|
||||||
|
return href.replace("mailto:", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (href.startsWith("/")) {
|
||||||
|
return `poyrazavsever.com${href}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return href.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinksContent() {
|
export function LinksContent() {
|
||||||
const [activeCategory, setActiveCategory] = useState<CategoryFilter>("all");
|
const [activeCategory, setActiveCategory] = useState<CategoryFilter>("all");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const filteredLinks = useMemo(() => {
|
const filteredItems = useMemo(() => {
|
||||||
const normalizedQuery = normalize(query.trim());
|
const normalizedQuery = normalize(query.trim());
|
||||||
const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
return LINK_DIRECTORY.filter((item) => {
|
return DIRECTORY_ITEMS.filter((item) => {
|
||||||
const categoryMatch = activeCategory === "all" || item.category === activeCategory;
|
if (activeCategory !== "all" && item.category !== activeCategory) {
|
||||||
if (!categoryMatch) return false;
|
return false;
|
||||||
if (queryTokens.length === 0) return true;
|
}
|
||||||
|
|
||||||
const haystack = normalize([
|
if (queryTokens.length === 0) {
|
||||||
item.label,
|
return true;
|
||||||
item.href,
|
}
|
||||||
...item.keywords,
|
|
||||||
getCategoryLabel(item.category),
|
const haystack = normalize(
|
||||||
].join(" "));
|
[item.label, item.href, CATEGORY_LABELS[item.category], ...item.keywords].join(" "),
|
||||||
|
);
|
||||||
|
|
||||||
return queryTokens.every((token) => haystack.includes(token));
|
return queryTokens.every((token) => haystack.includes(token));
|
||||||
});
|
});
|
||||||
}, [activeCategory, query]);
|
}, [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 (
|
return (
|
||||||
<section className="flex h-full flex-col gap-3 overflow-y-auto">
|
<section className="relative isolate min-h-dvh overflow-hidden px-4 py-8 sm:px-6 sm:py-10">
|
||||||
<Card className="rounded-sm border-border p-4">
|
|
||||||
<CardHeader className="space-y-1 p-0">
|
|
||||||
<CardTitle className="text-xl">Link Merkezi</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Tüm sosyal medya, sayfa ve kaynak linklerini tek yerden ara, filtrele ve yönet.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="mt-4 space-y-3 p-0">
|
<div className="relative mx-auto flex w-full max-w-xl justify-center">
|
||||||
<div className="grid gap-2 sm:grid-cols-[1fr_auto]">
|
<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">
|
||||||
<Input
|
<div className="relative aspect-1878/410 overflow-hidden border-b border-border/70">
|
||||||
value={query}
|
<img
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
src="/logo/cover.png"
|
||||||
placeholder="Linklerde ara... (örn: github, medium, /blog)"
|
alt="Poyraz Avsever kapak görseli"
|
||||||
aria-label="Linklerde arama"
|
|
||||||
/>
|
/>
|
||||||
<Badge variant="outline" className="inline-flex h-10 items-center px-3">
|
<div className="absolute inset-0 bg-linear-to-t from-background/20 via-transparent to-transparent" />
|
||||||
{filteredLinks.length} / {LINK_DIRECTORY.length} sonuç
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<div className="px-5 pb-5 pt-0 sm:px-6 sm:pb-6">
|
||||||
value={activeCategory}
|
<div className="-mt-14 flex flex-col gap-4">
|
||||||
onValueChange={(value) => setActiveCategory(value as CategoryFilter)}
|
<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.jpeg" alt="Poyraz Avsever" />
|
||||||
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-4">
|
<AvatarFallback className="rounded-3xl bg-muted text-lg font-semibold">
|
||||||
{FILTER_ITEMS.map((item) => (
|
PA
|
||||||
<TabsTrigger key={item.id} value={item.id}>
|
</AvatarFallback>
|
||||||
{item.label}
|
</Avatar>
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{filteredLinks.length === 0 ? (
|
<div className="space-y-3">
|
||||||
<Card className="rounded-sm border-border p-5">
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
<Typography variant="h2" className="text-[1.75rem] leading-none sm:text-[2rem]">
|
||||||
Arama kriterine uygun link bulunamadı.
|
Poyraz
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
<Typography
|
||||||
) : (
|
variant="h2"
|
||||||
<div className="grid gap-2 md:grid-cols-2">
|
secondaryFont
|
||||||
{filteredLinks.map((item) => (
|
className="text-[1.75rem] leading-none text-red-600 sm:text-[2rem]"
|
||||||
<Card
|
|
||||||
key={`${item.category}-${item.id}`}
|
|
||||||
className="rounded-sm border-border p-3 transition-colors hover:border-zinc-700"
|
|
||||||
>
|
>
|
||||||
<CardHeader className="flex-row items-start justify-between gap-3 space-y-0 p-0">
|
Avsever
|
||||||
<div className="flex min-w-0 items-start gap-2">
|
</Typography>
|
||||||
<span className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-sm border border-border text-muted-foreground">
|
|
||||||
<Icon icon={item.icon} width={18} height={18} />
|
|
||||||
</span>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<CardTitle className="truncate text-base">{item.label}</CardTitle>
|
|
||||||
<CardDescription className="truncate text-xs">{item.href}</CardDescription>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<Badge variant="outline">{getCategoryLabel(item.category)}</Badge>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="mt-3 flex items-center gap-2 p-0">
|
<Typography variant="small" className="max-w-lg text-sm leading-6 text-muted-foreground">
|
||||||
<Button asChild size="sm">
|
Burada sosyal hesaplarım, portfolyo sayfalarım ve hızlı erişim linklerimin
|
||||||
|
tamamı tek yerde duruyor. Direkt açıp paylaşabilirsin.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{SOCIAL_LINKS.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
|
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} />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
|
{TOP_ICON_LINKS.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
target={item.external ? "_blank" : undefined}
|
target={item.external ? "_blank" : undefined}
|
||||||
rel={item.external ? "noreferrer" : undefined}
|
rel={item.external ? "noreferrer" : undefined}
|
||||||
|
className="block"
|
||||||
>
|
>
|
||||||
Aç
|
<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">
|
||||||
</Link>
|
<div className="flex items-center gap-3">
|
||||||
</Button>
|
<span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-border bg-background text-muted-foreground">
|
||||||
|
<Icon icon={item.icon} width={18} height={18} />
|
||||||
<Button
|
</span>
|
||||||
type="button"
|
<div className="min-w-0">
|
||||||
size="sm"
|
<Typography variant="large" className="truncate text-base">
|
||||||
variant="outline"
|
{item.label}
|
||||||
onClick={() => handleCopy(item.id, item.href)}
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="truncate text-xs text-muted-foreground"
|
||||||
>
|
>
|
||||||
{copiedId === item.id ? "Kopyalandı" : "Kopyala"}
|
{formatHref(item.href)}
|
||||||
</Button>
|
</Typography>
|
||||||
</CardContent>
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
<div className="space-y-3 pt-1">
|
||||||
|
<div>
|
||||||
|
<Typography variant="large" className="text-base">
|
||||||
|
Tüm Linkler
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
|
Sayfalar, kaynaklar ve sosyal profiller
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2 sm:grid-cols-[190px_1fr]">
|
||||||
|
<Select
|
||||||
|
value={activeCategory}
|
||||||
|
onValueChange={(value) => setActiveCategory(value as CategoryFilter)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-11 rounded-2xl border-border bg-background px-4 text-sm">
|
||||||
|
<SelectValue placeholder="Kategori seç" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FILTER_ITEMS.map((item) => (
|
||||||
|
<SelectItem key={item.id} value={item.id}>
|
||||||
|
{item.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
value={query}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
placeholder="Link ara... github, medium, /blog"
|
||||||
|
aria-label="Linklerde arama"
|
||||||
|
className="h-11 rounded-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{filteredItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={`${item.category}-${item.id}`}
|
||||||
|
href={item.href}
|
||||||
|
target={item.external ? "_blank" : undefined}
|
||||||
|
rel={item.external ? "noreferrer" : undefined}
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
<Card className="rounded-2xl border-border p-3 transition-all hover:-translate-y-0.5 hover:border-red-600/40">
|
||||||
|
<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">
|
||||||
|
<Icon icon={item.icon} width={18} height={18} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Typography variant="large" className="text-base leading-tight">
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
<span className="inline-flex rounded-full border border-border px-2.5 py-0.5 text-[11px] text-muted-foreground">
|
||||||
|
{CATEGORY_LABELS[item.category]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="mt-1 truncate text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
{formatHref(item.href)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
<Icon
|
||||||
|
icon={item.external ? "mdi:arrow-top-right" : "mdi:arrow-right"}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredItems.length === 0 ? (
|
||||||
|
<Card className="rounded-2xl border-border px-4 py-5">
|
||||||
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
|
Seçili filtreye uygun link bulunamadı.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 116 KiB |
Reference in New Issue
Block a user