From 13cafffa5d96aac6ab6c0f8f0f55ad91912d903f Mon Sep 17 00:00:00 2001 From: Poyraz <78071274+poyrazavsever@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:59:57 +0300 Subject: [PATCH] feat(navigation): add extensible others menus --- app/[locale]/links/page.tsx | 25 ++- components/links-content.tsx | 24 ++- components/site-navbar.tsx | 343 ++++++++++++++++++++++++++--------- lib/command-palette-links.ts | 18 +- lib/links.ts | 63 +++++++ messages/en.json | 3 + messages/tr.json | 3 + 7 files changed, 387 insertions(+), 92 deletions(-) diff --git a/app/[locale]/links/page.tsx b/app/[locale]/links/page.tsx index 2d2cfb3..b76da6b 100644 --- a/app/[locale]/links/page.tsx +++ b/app/[locale]/links/page.tsx @@ -1,6 +1,13 @@ import type { Metadata } from "next"; import { LinksContent } from "@/components/links-content"; +type LinksPageProps = { + searchParams?: Promise<{ + category?: string | string[]; + query?: string | string[]; + }>; +}; + export const metadata: Metadata = { title: "Links", description: @@ -31,6 +38,20 @@ export const metadata: Metadata = { }, }; -export default function LinksPage() { - return ; +function firstParam(value?: string | string[]) { + return Array.isArray(value) ? value[0] : value; +} + +export default async function LinksPage({ searchParams }: LinksPageProps) { + const resolvedSearchParams = searchParams ? await searchParams : undefined; + const initialCategory = firstParam(resolvedSearchParams?.category); + const initialQuery = firstParam(resolvedSearchParams?.query); + + return ( + + ); } diff --git a/components/links-content.tsx b/components/links-content.tsx index 8b0b251..1feb1e5 100644 --- a/components/links-content.tsx +++ b/components/links-content.tsx @@ -24,6 +24,11 @@ import { type CategoryFilter = "all" | LinkDirectoryCategory; +type LinksContentProps = { + initialCategory?: string; + initialQuery?: string; +}; + const CATEGORY_ORDER = { resources: 0, navigation: 1, @@ -58,13 +63,26 @@ function formatHref(href: string) { return href.replace(/^https?:\/\//, "").replace(/\/$/, ""); } -export function LinksContent() { +function parseCategoryFilter(value?: string): CategoryFilter { + if (value === "navigation" || value === "social" || value === "resources") { + return value; + } + + return "all"; +} + +export function LinksContent({ + initialCategory, + initialQuery = "", +}: LinksContentProps) { const t = useTranslations("Links"); const tNav = useTranslations("Nav"); const locale = useLocale(); - const [activeCategory, setActiveCategory] = useState("all"); - const [query, setQuery] = useState(""); + const [activeCategory, setActiveCategory] = useState(() => + parseCategoryFilter(initialCategory), + ); + const [query, setQuery] = useState(initialQuery); const filterItems = useMemo(() => [ { id: "all" as const, label: t("allCategories") }, diff --git a/components/site-navbar.tsx b/components/site-navbar.tsx index d08f599..045da0d 100644 --- a/components/site-navbar.tsx +++ b/components/site-navbar.tsx @@ -5,7 +5,7 @@ import dynamic from "next/dynamic"; import { Link, usePathname } from "@/i18n/routing"; import { useLocale, useTranslations } from "next-intl"; import { LanguageSwitcher } from "@/components/language-switcher"; -import { useState } from "react"; +import { Fragment, useState } from "react"; import { Button, ButtonIcon, @@ -35,7 +35,13 @@ import { } from "poyraz-ui/molecules"; import { NavbarTopBar, NavbarTopBarSection } from "poyraz-ui/organisms"; import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label"; -import { getResumeHref, NAV_LINKS, SOCIAL_LINKS, TOP_ICON_LINKS } from "@/lib/links"; +import { + getResumeHref, + NAV_DROPDOWN_GROUPS, + NAV_LINKS, + SOCIAL_LINKS, + TOP_ICON_LINKS, +} from "@/lib/links"; import type { ThemeMode } from "@/components/app-shell"; const SearchCommand = dynamic( @@ -88,6 +94,8 @@ type SiteNavbarProps = ThemeToggleProps; export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) { const pathname = usePathname(); const [searchOpen, setSearchOpen] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [mobileMenuGroupId, setMobileMenuGroupId] = useState(null); const shortcut = useKeyboardShortcutLabel(); const t = useTranslations("Nav"); const locale = useLocale(); @@ -98,14 +106,22 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) { }; const activeTab = NAV_LINKS.find((item) => isActiveLink(item.href))?.id; + const activeMobileMenuGroup = NAV_DROPDOWN_GROUPS.find( + (group) => group.id === mobileMenuGroupId, + ); + + const handleMobileMenuOpenChange = (open: boolean) => { + setMobileMenuOpen(open); + if (!open) setMobileMenuGroupId(null); + }; const languageLabel = locale === "tr" ? "Switch to English" : "Türkçe'ye geç"; return ( -
+
{TOP_ICON_LINKS.map((item) => { @@ -161,11 +177,11 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) { -
+
-
- +
+ {NAV_LINKS.map((item, index) => ( -
- {index > 0 && ( - - )} - - {t(item.id)} - -
+ +
+ {index > 0 && ( + + )} + + {t(item.id)} + +
+ + {NAV_DROPDOWN_GROUPS.filter( + (group) => group.insertAfter === item.id, + ).map((group) => ( +
+ + + + + + + {group.items.map((groupItem) => ( + + {groupItem.external ? ( + + + {t(groupItem.id)} + + ) : ( + + + {t(groupItem.id)} + + )} + + ))} + + +
+ ))} +
))}
-
+
- + - - - - - - - - -
- {SOCIAL_LINKS.map((item) => ( - +
+ + + {t(activeMobileMenuGroup.id)} + +
+ + + +
-
+ ) : ( +
+ {t("mobileMenu")} + + + + + + + + + + + +
+ {SOCIAL_LINKS.map((item) => ( + + + {item.label} + + ))} +
+
+ )}
diff --git a/lib/command-palette-links.ts b/lib/command-palette-links.ts index e145de0..38764b9 100644 --- a/lib/command-palette-links.ts +++ b/lib/command-palette-links.ts @@ -10,7 +10,7 @@ import { import { REFERENCES } from "@/data/references"; import { VOLUNTEER_COMMUNITY_ITEMS } from "@/data/volunteer-community"; import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos"; -import { NAV_LINKS, SOCIAL_LINKS } from "@/lib/links"; +import { NAV_DROPDOWN_GROUPS, NAV_LINKS, SOCIAL_LINKS } from "@/lib/links"; import { getLocalizedValue } from "@/lib/locale"; export type CommandPaletteItem = { @@ -62,6 +62,21 @@ export function getCommandPaletteGroups( ], })); + const dropdownGroups: CommandPaletteGroup[] = NAV_DROPDOWN_GROUPS.map( + (group) => ({ + id: `navigation-${group.id}`, + heading: tNav.has(group.id) ? tNav(group.id) : group.label, + items: group.items.map((item) => ({ + id: item.id, + label: tNav.has(item.id) ? tNav(item.id) : item.label, + href: item.href, + icon: item.icon, + external: item.external, + keywords: [group.label, item.label, ...item.keywords], + })), + }), + ); + const blogItems: CommandPaletteItem[] = [ { id: "blog-index", @@ -281,6 +296,7 @@ export function getCommandPaletteGroups( heading: locale === "tr" ? "İçerikler" : "Contents", items: contentItems, }, + ...dropdownGroups, { id: "social", heading: "Social", diff --git a/lib/links.ts b/lib/links.ts index 7c63a9c..dcffb0a 100644 --- a/lib/links.ts +++ b/lib/links.ts @@ -70,6 +70,52 @@ export const SOCIAL_LINKS = [ }, ] as const; +export const ANIMATION_RESOURCE_LINKS = [ + { + id: "motion", + label: "Motion", + href: "https://motion.dev/", + icon: "mdi:motion-play-outline", + }, + { + id: "gsap", + label: "GSAP", + href: "https://gsap.com/resources/", + icon: "mdi:lightning-bolt-outline", + }, + { + id: "lottiefiles", + label: "LottieFiles", + href: "https://lottiefiles.com/", + icon: "mdi:movie-open-play-outline", + }, + { + id: "rive", + label: "Rive", + href: "https://rive.app/", + icon: "mdi:vector-curve", + }, +] as const; + +export const NAV_DROPDOWN_GROUPS = [ + { + id: "others", + label: "Diğerleri", + icon: "mdi:dots-horizontal", + insertAfter: "gallery", + items: [ + { + id: "animationResources", + label: "Animasyon Kaynakları", + href: "/links?category=resources&query=animation", + icon: "mdi:motion-play-outline", + external: false, + keywords: ["animasyon", "animation", "kaynak", "resource", "motion"], + }, + ], + }, +] as const; + export const TOP_ICON_LINKS = [ { id: "ui-kit", @@ -145,6 +191,23 @@ export const LINK_DIRECTORY: LinkDirectoryItem[] = [ category: "social" as const, keywords: [item.label, item.href, "sosyal", "profile", "platform"], })), + ...ANIMATION_RESOURCE_LINKS.map((item) => ({ + id: item.id, + label: item.label, + href: item.href, + icon: item.icon, + external: true, + category: "resources" as const, + keywords: [ + item.label, + item.href, + "animasyon", + "animation", + "kaynak", + "resource", + "motion", + ], + })), ...TOP_ICON_LINKS.map((item) => ({ id: item.id, label: item.label, diff --git a/messages/en.json b/messages/en.json index 3a32138..4b60128 100644 --- a/messages/en.json +++ b/messages/en.json @@ -9,6 +9,9 @@ "search": "Search", "social": "Social", "socialLinks": "Social Links", + "others": "Others", + "animationResources": "Animation Resources", + "backToMenu": "Back to menu", "menu": "Menu", "mobileMenu": "Mobile Menu", "resume": "Resume" diff --git a/messages/tr.json b/messages/tr.json index 5af2228..8edc844 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -9,6 +9,9 @@ "search": "Ara", "social": "Sosyal", "socialLinks": "Sosyal Bağlantılar", + "others": "Diğerleri", + "animationResources": "Animasyon Kaynakları", + "backToMenu": "Menüye dön", "menu": "Menü", "mobileMenu": "Mobil Menü", "resume": "Özgeçmiş"