feat(i18n): localize static elements, navbar switcher, and search palette
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import { Link } from "@/i18n/routing";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { Card, Typography } from "poyraz-ui/atoms";
|
import { Card, Typography } from "poyraz-ui/atoms";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export const CONTACT_LINKS = [
|
export const CONTACT_LINKS = [
|
||||||
{
|
{
|
||||||
@@ -27,6 +28,8 @@ export const CONTACT_LINKS = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function ContactContent() {
|
export function ContactContent() {
|
||||||
|
const t = useTranslations("Contact");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid gap-3 md:grid-cols-3">
|
<section className="grid gap-3 md:grid-cols-3">
|
||||||
{CONTACT_LINKS.map((item) => (
|
{CONTACT_LINKS.map((item) => (
|
||||||
@@ -42,7 +45,7 @@ export function ContactContent() {
|
|||||||
<Icon icon={item.icon} width={18} height={18} />
|
<Icon icon={item.icon} width={18} height={18} />
|
||||||
</div>
|
</div>
|
||||||
<Typography variant="large" className="mt-3 text-base leading-tight">
|
<Typography variant="large" className="mt-3 text-base leading-tight">
|
||||||
{item.label}
|
{t(item.id)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small" className="mt-1 text-muted-foreground">
|
<Typography variant="small" className="mt-1 text-muted-foreground">
|
||||||
{item.value}
|
{item.value}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { YoutubeLiteEmbed } from "@/components/youtube-lite-embed";
|
import { YoutubeLiteEmbed } from "@/components/youtube-lite-embed";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
type ContentContentProps = {
|
type ContentContentProps = {
|
||||||
youtubeLinks: readonly string[];
|
youtubeLinks: readonly string[];
|
||||||
@@ -11,6 +12,7 @@ type ContentContentProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
||||||
|
const t = useTranslations("Content");
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-muted/20 p-2">
|
<div className="flex h-full w-full items-center justify-center bg-muted/20 p-2">
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
Önizleme yüklenemedi
|
{t("pdfPreviewError")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -107,6 +109,7 @@ export function ContentContent({
|
|||||||
youtubeLinks,
|
youtubeLinks,
|
||||||
pdfFiles,
|
pdfFiles,
|
||||||
}: ContentContentProps) {
|
}: ContentContentProps) {
|
||||||
|
const t = useTranslations("Content");
|
||||||
const [pdfModalOpen, setPdfModalOpen] = useState(false);
|
const [pdfModalOpen, setPdfModalOpen] = useState(false);
|
||||||
const [activePdfIndex, setActivePdfIndex] = useState(0);
|
const [activePdfIndex, setActivePdfIndex] = useState(0);
|
||||||
|
|
||||||
@@ -128,7 +131,7 @@ export function ContentContent({
|
|||||||
<section className="flex h-full flex-col gap-3 overflow-y-auto">
|
<section className="flex h-full flex-col gap-3 overflow-y-auto">
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<Typography variant="large" className="text-base">
|
<Typography variant="large" className="text-base">
|
||||||
Son YouTube Videoları
|
{t("youtubeTitle")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className="grid gap-2 md:grid-cols-3">
|
<div className="grid gap-2 md:grid-cols-3">
|
||||||
{embeddedVideos.map((link) => (
|
{embeddedVideos.map((link) => (
|
||||||
@@ -136,7 +139,7 @@ export function ContentContent({
|
|||||||
key={link}
|
key={link}
|
||||||
className="overflow-hidden rounded-sm border-border p-0"
|
className="overflow-hidden rounded-sm border-border p-0"
|
||||||
>
|
>
|
||||||
<YoutubeLiteEmbed link={link} title="YouTube video oynatici" />
|
<YoutubeLiteEmbed link={link} title={t("youtubeEmbedTitle")} />
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -144,7 +147,7 @@ export function ContentContent({
|
|||||||
|
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<Typography variant="large" className="text-base">
|
<Typography variant="large" className="text-base">
|
||||||
LinkedIn PDF Notları
|
{t("pdfTitle")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-3">
|
<div className="grid grid-cols-2 gap-2 lg:grid-cols-3">
|
||||||
{pdfFiles.map((pdf, index) => (
|
{pdfFiles.map((pdf, index) => (
|
||||||
@@ -157,7 +160,7 @@ export function ContentContent({
|
|||||||
<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
|
<PdfFirstPagePreview
|
||||||
src={`/pdf/${pdf}`}
|
src={`/pdf/${pdf}`}
|
||||||
title={`${pdf} önizleme`}
|
title={t("pdfPreviewTitle", { name: pdf })}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</button>
|
</button>
|
||||||
@@ -168,7 +171,7 @@ 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>
|
<ModalTitle>
|
||||||
{activePdf ? activePdf.replace(/\.pdf$/i, "") : "PDF Notu"}
|
{activePdf ? activePdf.replace(/\.pdf$/i, "") : t("pdfModalDefaultTitle")}
|
||||||
</ModalTitle>
|
</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">
|
||||||
@@ -187,7 +190,7 @@ export function ContentContent({
|
|||||||
setActivePdfIndex((prev) => Math.max(0, prev - 1))
|
setActivePdfIndex((prev) => Math.max(0, prev - 1))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Önceki
|
{t("prev")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -200,7 +203,7 @@ export function ContentContent({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Sonraki
|
{t("next")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +219,7 @@ export function ContentContent({
|
|||||||
) : (
|
) : (
|
||||||
<Card className="mt-3 rounded-sm border-border p-3">
|
<Card className="mt-3 rounded-sm border-border p-3">
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
`/public/pdf` içinde PDF bulunamadı.
|
{t("pdfNotFound")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { Typography } from "poyraz-ui/atoms";
|
import { Typography } from "poyraz-ui/atoms";
|
||||||
import { StaggerContainer, StaggerItem } from "@/components/motion-wrapper";
|
import { StaggerContainer, StaggerItem } from "@/components/motion-wrapper";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
type GalleryContentProps = {
|
type GalleryContentProps = {
|
||||||
images: string[];
|
images: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GalleryContent({ images }: GalleryContentProps) {
|
export function GalleryContent({ images }: GalleryContentProps) {
|
||||||
|
const t = useTranslations("Gallery");
|
||||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
// Keyboard navigation for lightbox
|
// Keyboard navigation for lightbox
|
||||||
@@ -53,7 +55,7 @@ export function GalleryContent({ images }: GalleryContentProps) {
|
|||||||
<section className="flex h-full items-center justify-center">
|
<section className="flex h-full items-center justify-center">
|
||||||
<div className="rounded-xl border border-dashed border-border py-20 px-10 text-center">
|
<div className="rounded-xl border border-dashed border-border py-20 px-10 text-center">
|
||||||
<Typography variant="muted">
|
<Typography variant="muted">
|
||||||
Henüz galeriye görsel eklenmemiş.
|
{t("empty")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -95,7 +97,7 @@ export function GalleryContent({ images }: GalleryContentProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
className="absolute top-4 right-4 z-10 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-zinc-100 text-zinc-900 transition-colors hover:bg-zinc-200 shadow-sm border border-zinc-200"
|
className="absolute top-4 right-4 z-10 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-zinc-100 text-zinc-900 transition-colors hover:bg-zinc-200 shadow-sm border border-zinc-200"
|
||||||
onClick={closeLightbox}
|
onClick={closeLightbox}
|
||||||
aria-label="Kapat"
|
aria-label={t("close")}
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:close" width={24} height={24} />
|
<Icon icon="mdi:close" width={24} height={24} />
|
||||||
</button>
|
</button>
|
||||||
@@ -109,7 +111,7 @@ export function GalleryContent({ images }: GalleryContentProps) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setLightboxIndex(lightboxIndex - 1);
|
setLightboxIndex(lightboxIndex - 1);
|
||||||
}}
|
}}
|
||||||
aria-label="Önceki"
|
aria-label={t("prev")}
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:chevron-left" width={32} height={32} />
|
<Icon icon="mdi:chevron-left" width={32} height={32} />
|
||||||
</button>
|
</button>
|
||||||
@@ -124,7 +126,7 @@ export function GalleryContent({ images }: GalleryContentProps) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setLightboxIndex(lightboxIndex + 1);
|
setLightboxIndex(lightboxIndex + 1);
|
||||||
}}
|
}}
|
||||||
aria-label="Sonraki"
|
aria-label={t("next")}
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:chevron-right" width={32} height={32} />
|
<Icon icon="mdi:chevron-right" width={32} height={32} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { useTranslations } from "next-intl";
|
||||||
|
|
||||||
type HomeHeroProps = {
|
type HomeHeroProps = {
|
||||||
news: {
|
news: {
|
||||||
@@ -19,6 +20,7 @@ type HomeHeroProps = {
|
|||||||
export function HomeHero({ news }: HomeHeroProps) {
|
export function HomeHero({ news }: HomeHeroProps) {
|
||||||
const [frame, setFrame] = useState<1 | 2>(1);
|
const [frame, setFrame] = useState<1 | 2>(1);
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const t = useTranslations("Home");
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (intervalRef.current) return;
|
if (intervalRef.current) return;
|
||||||
@@ -62,8 +64,7 @@ export function HomeHero({ news }: HomeHeroProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
Teknolojiyi merak eden bir genç. Yazılım geliştirme, yapay zeka ve
|
{t("role")}
|
||||||
teknoloji dünyasındaki gelişmeleri takip ediyor.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"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 "@/i18n/routing";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -23,15 +23,6 @@ import {
|
|||||||
|
|
||||||
type CategoryFilter = "all" | LinkDirectoryCategory;
|
type CategoryFilter = "all" | LinkDirectoryCategory;
|
||||||
|
|
||||||
const FILTER_ITEMS: ReadonlyArray<{ id: CategoryFilter; label: string }> = [
|
|
||||||
{ 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 = {
|
const CATEGORY_ORDER = {
|
||||||
resources: 0,
|
resources: 0,
|
||||||
navigation: 1,
|
navigation: 1,
|
||||||
@@ -67,9 +58,20 @@ function formatHref(href: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LinksContent() {
|
export function LinksContent() {
|
||||||
|
const t = useTranslations("Links");
|
||||||
|
const tNav = useTranslations("Nav");
|
||||||
|
|
||||||
const [activeCategory, setActiveCategory] = useState<CategoryFilter>("all");
|
const [activeCategory, setActiveCategory] = useState<CategoryFilter>("all");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const filterItems = useMemo(() => [
|
||||||
|
{ id: "all" as const, label: t("allCategories") },
|
||||||
|
...LINK_DIRECTORY_CATEGORIES.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
label: t(`categories.${item.id}`),
|
||||||
|
})),
|
||||||
|
], [t]);
|
||||||
|
|
||||||
const filteredItems = 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);
|
||||||
@@ -84,12 +86,17 @@ export function LinksContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const haystack = normalize(
|
const haystack = normalize(
|
||||||
[item.label, item.href, CATEGORY_LABELS[item.category], ...item.keywords].join(" "),
|
[
|
||||||
|
tNav.has(item.id) ? tNav(item.id) : item.label,
|
||||||
|
item.href,
|
||||||
|
t(`categories.${item.category}`),
|
||||||
|
...item.keywords,
|
||||||
|
].join(" "),
|
||||||
);
|
);
|
||||||
|
|
||||||
return queryTokens.every((token) => haystack.includes(token));
|
return queryTokens.every((token) => haystack.includes(token));
|
||||||
});
|
});
|
||||||
}, [activeCategory, query]);
|
}, [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="relative isolate min-h-dvh overflow-hidden px-4 py-8 sm:px-6 sm:py-10">
|
||||||
@@ -128,8 +135,7 @@ export function LinksContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Typography variant="small" className="max-w-lg text-sm leading-6 text-muted-foreground">
|
<Typography variant="small" className="max-w-lg text-sm leading-6 text-muted-foreground">
|
||||||
Burada sosyal hesaplarım, portfolyo sayfalarım ve hızlı erişim linklerimin
|
{t("desc")}
|
||||||
tamamı tek yerde duruyor. Direkt açıp paylaşabilirsin.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -165,7 +171,7 @@ export function LinksContent() {
|
|||||||
</span>
|
</span>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<Typography variant="large" className="truncate text-base">
|
<Typography variant="large" className="truncate text-base">
|
||||||
{item.label}
|
{tNav.has(item.id) ? tNav(item.id) : item.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="small"
|
variant="small"
|
||||||
@@ -183,10 +189,10 @@ export function LinksContent() {
|
|||||||
<div className="space-y-3 pt-1">
|
<div className="space-y-3 pt-1">
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="large" className="text-base">
|
<Typography variant="large" className="text-base">
|
||||||
Tüm Linkler
|
{t("allLinks")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
Sayfalar, kaynaklar ve sosyal profiller
|
{t("allLinksDesc")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -196,10 +202,10 @@ export function LinksContent() {
|
|||||||
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-11 rounded-2xl border-border bg-background px-4 text-sm">
|
||||||
<SelectValue placeholder="Kategori seç" />
|
<SelectValue placeholder={t("selectCategory")} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{FILTER_ITEMS.map((item) => (
|
{filterItems.map((item) => (
|
||||||
<SelectItem key={item.id} value={item.id}>
|
<SelectItem key={item.id} value={item.id}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -210,8 +216,8 @@ export function LinksContent() {
|
|||||||
<Input
|
<Input
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
placeholder="Link ara... github, medium, /blog"
|
placeholder={t("searchPlaceholder")}
|
||||||
aria-label="Linklerde arama"
|
aria-label={t("allLinks")}
|
||||||
className="h-11 rounded-2xl"
|
className="h-11 rounded-2xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,10 +240,10 @@ export function LinksContent() {
|
|||||||
<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-base leading-tight">
|
||||||
{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">
|
<span className="inline-flex rounded-full border border-border px-2.5 py-0.5 text-[11px] text-muted-foreground">
|
||||||
{CATEGORY_LABELS[item.category]}
|
{t(`categories.${item.category}`)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -264,7 +270,7 @@ export function LinksContent() {
|
|||||||
{filteredItems.length === 0 ? (
|
{filteredItems.length === 0 ? (
|
||||||
<Card className="rounded-2xl border-border px-4 py-5">
|
<Card className="rounded-2xl border-border px-4 py-5">
|
||||||
<Typography variant="small" className="text-muted-foreground">
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
Seçili filtreye uygun link bulunamadı.
|
{t("empty")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "@/i18n/routing";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import {
|
import {
|
||||||
CommandPalette,
|
CommandPalette,
|
||||||
CommandPaletteContent,
|
CommandPaletteContent,
|
||||||
@@ -15,7 +16,7 @@ import {
|
|||||||
CommandPaletteSeparator,
|
CommandPaletteSeparator,
|
||||||
} from "poyraz-ui/molecules";
|
} from "poyraz-ui/molecules";
|
||||||
import {
|
import {
|
||||||
COMMAND_PALETTE_GROUPS,
|
getCommandPaletteGroups,
|
||||||
type CommandPaletteItem as PaletteItem,
|
type CommandPaletteItem as PaletteItem,
|
||||||
} from "@/lib/command-palette-links";
|
} from "@/lib/command-palette-links";
|
||||||
import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label";
|
import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label";
|
||||||
@@ -27,8 +28,17 @@ type SearchCommandProps = {
|
|||||||
|
|
||||||
export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
|
export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const locale = useLocale();
|
||||||
|
const tLinks = useTranslations("Links");
|
||||||
|
const tNav = useTranslations("Nav");
|
||||||
|
const t = useTranslations("SearchCommand");
|
||||||
const shortcut = useKeyboardShortcutLabel();
|
const shortcut = useKeyboardShortcutLabel();
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
return getCommandPaletteGroups(locale, tLinks, tNav);
|
||||||
|
}, [locale, tLinks, tNav]);
|
||||||
|
|
||||||
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
||||||
if (!nextOpen) {
|
if (!nextOpen) {
|
||||||
setQuery("");
|
setQuery("");
|
||||||
@@ -51,11 +61,11 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
|
|||||||
|
|
||||||
const filteredGroups = useMemo(() => {
|
const filteredGroups = useMemo(() => {
|
||||||
const value = query.trim().toLowerCase();
|
const value = query.trim().toLowerCase();
|
||||||
if (!value) return COMMAND_PALETTE_GROUPS;
|
if (!value) return groups;
|
||||||
|
|
||||||
const tokens = value.split(/\s+/).filter(Boolean);
|
const tokens = value.split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
return COMMAND_PALETTE_GROUPS.map((group) => ({
|
return groups.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
items: group.items.filter((item) => {
|
items: group.items.filter((item) => {
|
||||||
const haystack = [
|
const haystack = [
|
||||||
@@ -70,7 +80,7 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
|
|||||||
return tokens.every((token) => haystack.includes(token));
|
return tokens.every((token) => haystack.includes(token));
|
||||||
}),
|
}),
|
||||||
})).filter((group) => group.items.length > 0);
|
})).filter((group) => group.items.length > 0);
|
||||||
}, [query]);
|
}, [query, groups]);
|
||||||
|
|
||||||
const handleCommandSelect = (item: PaletteItem) => {
|
const handleCommandSelect = (item: PaletteItem) => {
|
||||||
handleOpenChange(false);
|
handleOpenChange(false);
|
||||||
@@ -87,13 +97,13 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
|
|||||||
<CommandPalette open={open} onOpenChange={handleOpenChange}>
|
<CommandPalette open={open} onOpenChange={handleOpenChange}>
|
||||||
<CommandPaletteContent className=" rounded-none p-0 pt-3 w-72 sm:w-full sm:max-w-xl sm:rounded-sm sm:p-0 sm:pt-2">
|
<CommandPaletteContent className=" rounded-none p-0 pt-3 w-72 sm:w-full sm:max-w-xl sm:rounded-sm sm:p-0 sm:pt-2">
|
||||||
<CommandPaletteInput
|
<CommandPaletteInput
|
||||||
placeholder="Sayfa ve bağlantılarda ara..."
|
placeholder={t("placeholder")}
|
||||||
className="mt-2 pr-10"
|
className="mt-2 pr-10"
|
||||||
onValueChange={setQuery}
|
onValueChange={setQuery}
|
||||||
/>
|
/>
|
||||||
<CommandPaletteList>
|
<CommandPaletteList>
|
||||||
{filteredGroups.length === 0 ? (
|
{filteredGroups.length === 0 ? (
|
||||||
<CommandPaletteEmpty>Sonuç bulunamadı.</CommandPaletteEmpty>
|
<CommandPaletteEmpty>{t("empty")}</CommandPaletteEmpty>
|
||||||
) : (
|
) : (
|
||||||
filteredGroups.map((group, index) => (
|
filteredGroups.map((group, index) => (
|
||||||
<div key={group.id}>
|
<div key={group.id}>
|
||||||
@@ -115,7 +125,7 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
|
|||||||
)}
|
)}
|
||||||
</CommandPaletteList>
|
</CommandPaletteList>
|
||||||
<CommandPaletteFooter className="text-xs text-muted-foreground">
|
<CommandPaletteFooter className="text-xs text-muted-foreground">
|
||||||
Hızlıca açmak için {shortcut} kullan
|
{t("footer", { shortcut })}
|
||||||
</CommandPaletteFooter>
|
</CommandPaletteFooter>
|
||||||
</CommandPaletteContent>
|
</CommandPaletteContent>
|
||||||
</CommandPalette>
|
</CommandPalette>
|
||||||
|
|||||||
+17
-14
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import { Link, usePathname } from "@/i18n/routing";
|
||||||
import { usePathname } from "next/navigation";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -45,6 +46,7 @@ export function SiteNavbar() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
const shortcut = useKeyboardShortcutLabel();
|
const shortcut = useKeyboardShortcutLabel();
|
||||||
|
const t = useTranslations("Nav");
|
||||||
|
|
||||||
const isActiveLink = (href: string) => {
|
const isActiveLink = (href: string) => {
|
||||||
if (href === "/") return pathname === "/";
|
if (href === "/") return pathname === "/";
|
||||||
@@ -60,13 +62,14 @@ export function SiteNavbar() {
|
|||||||
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}
|
||||||
aria-label={item.label}
|
aria-label={t.has(item.id) ? t(item.id) : item.label}
|
||||||
title={item.label}
|
title={t.has(item.id) ? t(item.id) : item.label}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-sm border border-border text-muted-foreground transition-colors hover:text-foreground"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-sm border border-border text-muted-foreground transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Icon icon={item.icon} width={16} height={16} />
|
<Icon icon={item.icon} width={16} height={16} />
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
<LanguageSwitcher />
|
||||||
</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">
|
||||||
@@ -99,7 +102,7 @@ export function SiteNavbar() {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className={getNavLinkClass(isActiveLink(item.href))}
|
className={getNavLinkClass(isActiveLink(item.href))}
|
||||||
>
|
>
|
||||||
{item.label}
|
{t(item.id)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -115,11 +118,11 @@ export function SiteNavbar() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSearchOpen(true)}
|
onClick={() => setSearchOpen(true)}
|
||||||
className="inline-flex h-8 w-44 cursor-pointer items-center justify-between rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground sm:w-52"
|
className="inline-flex h-8 w-44 cursor-pointer items-center justify-between rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground sm:w-52"
|
||||||
aria-label="Komut paletini aç"
|
aria-label={t("search")}
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Icon icon="mdi:magnify" width={16} height={16} />
|
<Icon icon="mdi:magnify" width={16} height={16} />
|
||||||
<span>Ara</span>
|
<span>{t("search")}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">{shortcut}</span>
|
<span className="text-xs text-muted-foreground">{shortcut}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -127,10 +130,10 @@ export function SiteNavbar() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger className="inline-flex h-8 cursor-pointer items-center gap-2 rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground">
|
<DropdownMenuTrigger className="inline-flex h-8 cursor-pointer items-center gap-2 rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground">
|
||||||
<Icon icon="mdi:share-variant" width={16} height={16} />
|
<Icon icon="mdi:share-variant" width={16} height={16} />
|
||||||
<span>Sosyal</span>
|
<span>{t("social")}</span>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-52 rounded-sm">
|
<DropdownMenuContent align="end" className="w-52 rounded-sm">
|
||||||
<DropdownMenuLabel>Sosyal Bağlantılar</DropdownMenuLabel>
|
<DropdownMenuLabel>{t("socialLinks")}</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{SOCIAL_LINKS.map((item) => (
|
{SOCIAL_LINKS.map((item) => (
|
||||||
<DropdownMenuItem key={item.id} asChild>
|
<DropdownMenuItem key={item.id} asChild>
|
||||||
@@ -153,21 +156,21 @@ export function SiteNavbar() {
|
|||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger className="inline-flex h-8 cursor-pointer items-center gap-2 rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground">
|
<SheetTrigger className="inline-flex h-8 cursor-pointer items-center gap-2 rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground">
|
||||||
<Icon icon="mdi:menu" width={18} height={18} />
|
<Icon icon="mdi:menu" width={18} height={18} />
|
||||||
<span>Menü</span>
|
<span>{t("menu")}</span>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="right" className="w-72 p-4">
|
<SheetContent side="right" className="w-72 p-4">
|
||||||
<SheetTitle className="sr-only">Mobil Menü</SheetTitle>
|
<SheetTitle className="sr-only">{t("mobileMenu")}</SheetTitle>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<SheetClose asChild>
|
<SheetClose asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSearchOpen(true)}
|
onClick={() => setSearchOpen(true)}
|
||||||
className="inline-flex h-9 w-full cursor-pointer items-center justify-between rounded-sm border border-border px-3 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
className="inline-flex h-9 w-full cursor-pointer items-center justify-between rounded-sm border border-border px-3 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||||
aria-label="Komut paletini aç"
|
aria-label={t("search")}
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Icon icon="mdi:magnify" width={16} height={16} />
|
<Icon icon="mdi:magnify" width={16} height={16} />
|
||||||
<span>Ara</span>
|
<span>{t("search")}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground/80">
|
<span className="text-xs text-muted-foreground/80">
|
||||||
{shortcut}
|
{shortcut}
|
||||||
@@ -186,7 +189,7 @@ export function SiteNavbar() {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className={getNavLinkClass(isActiveLink(item.href))}
|
className={getNavLinkClass(isActiveLink(item.href))}
|
||||||
>
|
>
|
||||||
{item.label}
|
{t(item.id)}
|
||||||
</Link>
|
</Link>
|
||||||
</SheetClose>
|
</SheetClose>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user