feat(layout): add animated promotional side rails

This commit is contained in:
poyrazavsever
2026-08-31 19:39:22 +03:00
parent 1fd03bf614
commit eca56fab5c
7 changed files with 737 additions and 31 deletions
+27 -5
View File
@@ -9,6 +9,7 @@ import { AppShell } from "@/components/app-shell";
import { GoogleAnalytics } from "@/components/google-analytics";
import { PoyrazBottomRightFollower } from "@/components/poyraz-bottom-right-follower";
import { listAnimationSources } from "@/data/animation-sources";
import { getHomeBlogNews, getLatestAgendaArticle } from "@/data/blog";
export async function generateMetadata({
params,
@@ -116,10 +117,13 @@ export default async function LocaleLayout({
}
// Provide messages for NextIntlClientProvider
const [messages, animationSources] = await Promise.all([
getMessages(),
listAnimationSources(locale),
]);
const [messages, animationSources, latestAgendaArticle, latestPosts] =
await Promise.all([
getMessages(),
listAnimationSources(locale),
getLatestAgendaArticle(locale),
getHomeBlogNews(locale, 1),
]);
const animationSourceSearchItems = animationSources.map((source) => ({
slug: source.slug,
title: source.title,
@@ -133,7 +137,25 @@ export default async function LocaleLayout({
<body className="min-h-dvh bg-background text-foreground antialiased">
<NextIntlClientProvider messages={messages}>
<GoogleAnalytics />
<AppShell animationSources={animationSourceSearchItems}>
<AppShell
animationSources={animationSourceSearchItems}
latestAgenda={
latestAgendaArticle
? {
title: latestAgendaArticle.title,
href: latestAgendaArticle.href,
}
: null
}
latestPost={
latestPosts[0]
? {
title: latestPosts[0].title,
href: latestPosts[0].href,
}
: null
}
>
{children}
</AppShell>
<PoyrazBottomRightFollower />
+37 -26
View File
@@ -1,7 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { Icon } from "@iconify/react";
import { usePathname } from "@/i18n/routing";
import { AnnouncementBar } from "poyraz-ui/organisms";
import { SiteNavbar } from "@/components/site-navbar";
import { NekoFollower } from "@/components/neko-follower";
@@ -10,6 +9,11 @@ import { useLocale } from "next-intl";
import { getLocalizedValue } from "@/lib/locale";
import dynamic from "next/dynamic";
import type { AnimationSourceSearchItem } from "@/lib/command-palette-links";
import {
LayoutLeftPromoRail,
LayoutRightPromoRail,
type LayoutContentPromo,
} from "@/components/layout-promo-rails";
const AtaturkWidgetModal = dynamic(
() => import("@/components/ataturk-widget-modal").then((mod) => mod.AtaturkWidgetModal),
@@ -19,6 +23,8 @@ const AtaturkWidgetModal = dynamic(
type AppShellProps = {
children: React.ReactNode;
animationSources: AnimationSourceSearchItem[];
latestAgenda: LayoutContentPromo | null;
latestPost: LayoutContentPromo | null;
};
export type ThemeMode = "light" | "dark";
@@ -32,15 +38,15 @@ function getInitialTheme(): ThemeMode {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
export function AppShell({ children, animationSources }: AppShellProps) {
const pathname = usePathname();
export function AppShell({
children,
animationSources,
latestAgenda,
latestPost,
}: AppShellProps) {
const locale = useLocale();
const announcement = ANNOUNCEMENT_ITEMS[0];
const [theme, setTheme] = useState<ThemeMode>(getInitialTheme);
const isStandaloneLinksPage =
pathname === "/links" || pathname.startsWith("/links/");
const isStandaloneMediaKitPage =
pathname === "/media-kit" || pathname.startsWith("/media-kit/");
useEffect(() => {
document.documentElement.dataset.poyrazTheme = theme;
@@ -48,32 +54,37 @@ export function AppShell({ children, animationSources }: AppShellProps) {
localStorage.setItem("poyraz-theme", theme);
}, [theme]);
if (isStandaloneLinksPage || isStandaloneMediaKitPage) {
return children;
}
const localizedText = announcement ? getLocalizedValue(announcement.text, locale) : "";
return (
<>
<AtaturkWidgetModal theme={theme} />
{ENABLE_NEKO_FOLLOWER ? <NekoFollower /> : null}
<div className="mx-auto flex w-full max-w-4xl flex-col px-4 py-4 ">
<SiteNavbar
theme={theme}
onThemeChange={setTheme}
animationSources={animationSources}
<div className="mx-auto grid w-full max-w-[1800px] grid-cols-1 gap-4 px-4 min-[1420px]:grid-cols-[220px_minmax(0,896px)_220px] min-[1420px]:justify-between">
<LayoutLeftPromoRail
latestAgenda={latestAgenda}
latestPost={latestPost}
/>
{announcement ? (
<AnnouncementBar
variant="branded"
dismissible={false}
icon={<Icon icon="mdi:sparkles" width={16} height={16} />}
>
{localizedText}
</AnnouncementBar>
) : null}
<main className="flex-1 py-4">{children}</main>
<div className="min-w-0 w-full max-w-4xl justify-self-center min-[1420px]:max-w-none">
<div className="pt-4">
<SiteNavbar
theme={theme}
onThemeChange={setTheme}
animationSources={animationSources}
/>
{announcement ? (
<AnnouncementBar
variant="branded"
dismissible={false}
icon={<Icon icon="mdi:sparkles" width={16} height={16} />}
>
{localizedText}
</AnnouncementBar>
) : null}
</div>
<main className="min-w-0 py-4">{children}</main>
</div>
<LayoutRightPromoRail />
</div>
</>
);
+342
View File
@@ -0,0 +1,342 @@
"use client";
import { useState, type KeyboardEvent } from "react";
import Image from "next/image";
import { Icon } from "@iconify/react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { useLocale, useTranslations } from "next-intl";
import {
Badge,
Button,
ButtonIcon,
ButtonLabel,
Card,
Typography,
} from "poyraz-ui/atoms";
import { Link } from "@/i18n/routing";
import {
LEFT_LAYOUT_PROMO_SLIDES,
RIGHT_LAYOUT_PROMO_SLIDES,
type LayoutPromoCardDefinition,
type LayoutPromoSlide,
} from "@/data/layout-promos";
import { SPONSORS } from "@/data/sponsors";
import { getLocalizedValue } from "@/lib/locale";
export type LayoutContentPromo = {
title: string;
href: string;
};
type PromoRailSide = "left" | "right";
function RailButton({
href,
label,
icon = "mdi:arrow-right",
variant = "outline",
external = false,
}: {
href: string;
label: string;
icon?: string;
variant?: "default" | "outline" | "secondary";
external?: boolean;
}) {
const content = (
<>
<ButtonLabel>{label}</ButtonLabel>
<ButtonIcon>
<Icon icon={icon} width={14} height={14} />
</ButtonIcon>
</>
);
return (
<Button
asChild
variant={variant}
size="xs"
radius="sm"
effect="swap"
swapTarget="both"
className="w-full justify-between"
>
{external ? (
<a href={href} target="_blank" rel="noreferrer">
{content}
</a>
) : (
<Link href={href}>{content}</Link>
)}
</Button>
);
}
function SponsorLogos() {
return (
<div className="mt-3 grid grid-cols-2 gap-1.5">
{SPONSORS.map((sponsor, index) => (
<a
key={sponsor.id}
href={sponsor.websiteUrl}
target="_blank"
rel="noreferrer"
aria-label={sponsor.name}
className={`relative flex h-10 items-center justify-center rounded-sm border border-border bg-white p-1.5 transition-colors hover:border-primary/40 ${
index === SPONSORS.length - 1 && SPONSORS.length % 2 === 1
? "col-span-2"
: ""
}`}
>
<Image
src={sponsor.logo}
alt=""
fill
sizes="100px"
className="object-contain p-1.5 grayscale transition duration-300 hover:grayscale-0"
/>
</a>
))}
</div>
);
}
function PromoIcon({
card,
className,
}: {
card: LayoutPromoCardDefinition;
className?: string;
}) {
return (
<Icon
icon={card.icon}
width={17}
height={17}
className={className}
/>
);
}
function PromoCard({
card,
latestAgenda,
latestPost,
}: {
card: LayoutPromoCardDefinition;
latestAgenda: LayoutContentPromo | null;
latestPost: LayoutContentPromo | null;
}) {
const t = useTranslations("LayoutPromos");
const locale = useLocale();
const liveContent =
card.contentSource === "latestAgenda"
? latestAgenda
: card.contentSource === "latestPost"
? latestPost
: null;
const href = liveContent?.href ?? getLocalizedValue(card.href, locale);
const description = liveContent?.title ?? t(card.descriptionKey);
const external = card.external ?? false;
const iconSurface = card.iconSurface ?? "accent";
const cardClassName =
card.surface === "primary"
? "rounded-sm border-primary/25 bg-primary/5 p-3"
: "rounded-sm border-border p-3";
const iconClassName =
iconSurface === "primary"
? "bg-primary text-primary-foreground"
: iconSurface === "foreground"
? "bg-foreground text-background"
: "bg-accent text-foreground";
return (
<Card className={cardClassName}>
{card.eyebrowKey ? (
<div className="flex items-center justify-between gap-2">
<Badge variant="secondary" radius="sm">
{t(card.eyebrowKey)}
</Badge>
<PromoIcon card={card} className="text-primary" />
</div>
) : (
<div
className={`flex size-8 items-center justify-center rounded-sm ${iconClassName}`}
>
<PromoIcon card={card} />
</div>
)}
<Typography
variant="large"
component="h2"
className="mt-3 text-base leading-5 tracking-[-0.025em]"
>
{t(card.titleKey)}
</Typography>
<Typography
variant="small"
className={`mt-2 text-xs leading-5 text-muted-foreground ${
liveContent ? "line-clamp-3" : ""
}`}
>
{description}
</Typography>
{card.kind === "sponsors" ? <SponsorLogos /> : null}
<div className="mt-3">
<RailButton
href={href}
label={t(card.ctaKey)}
icon={external ? "mdi:arrow-top-right" : "mdi:arrow-right"}
variant={card.buttonVariant}
external={external}
/>
</div>
</Card>
);
}
function PromoRail({
side,
slides,
latestAgenda = null,
latestPost = null,
}: {
side: PromoRailSide;
slides: readonly LayoutPromoSlide[];
latestAgenda?: LayoutContentPromo | null;
latestPost?: LayoutContentPromo | null;
}) {
const t = useTranslations("LayoutPromos");
const reduceMotion = useReducedMotion();
const [activeSlide, setActiveSlide] = useState(0);
const direction = side === "left" ? -1 : 1;
const activeCards = slides[activeSlide] ?? slides[0];
const railId = `${side}-promo-rail`;
const selectAdjacentSlide = (step: number) => {
setActiveSlide((current) => (current + step + slides.length) % slides.length);
};
const handleNavigationKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
event.preventDefault();
selectAdjacentSlide(event.key === "ArrowRight" ? 1 : -1);
};
return (
<aside
aria-label={t(side === "left" ? "leftRailLabel" : "rightRailLabel")}
className="relative z-50 hidden min-[1420px]:block"
>
<div className="sticky top-4 py-4">
<div
role="tablist"
aria-label={t("slideNavigationLabel")}
onKeyDown={handleNavigationKeyDown}
className="mb-3 flex h-5 items-center justify-center gap-2"
>
{slides.map((_, index) => {
const selected = index === activeSlide;
return (
<button
key={`${railId}-dot-${index}`}
type="button"
role="tab"
aria-selected={selected}
aria-controls={`${railId}-panel-${index}`}
aria-label={t("slideCta", { slide: index + 1 })}
onClick={() => setActiveSlide(index)}
className="group flex size-5 items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<span
className={`block rounded-full transition-[width,background-color,transform] duration-300 group-hover:scale-110 ${
selected
? "h-2 w-5 bg-primary"
: "size-2 bg-border group-hover:bg-muted-foreground/60"
}`}
/>
</button>
);
})}
</div>
<div className="overflow-hidden">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={`${railId}-slide-${activeSlide}`}
id={`${railId}-panel-${activeSlide}`}
role="tabpanel"
aria-label={t("slideStatus", {
current: activeSlide + 1,
total: slides.length,
})}
initial={
reduceMotion
? { opacity: 0 }
: { opacity: 0, x: -direction * 26 }
}
animate={{ opacity: 1, x: 0 }}
exit={
reduceMotion
? { opacity: 0 }
: { opacity: 0, x: direction * 26 }
}
transition={{
duration: reduceMotion ? 0.12 : 0.38,
ease: [0.22, 1, 0.36, 1],
}}
className="space-y-3"
>
{activeCards.map((card, index) => (
<motion.div
key={card.id}
initial={reduceMotion ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: reduceMotion ? 0 : 0.28,
delay: reduceMotion ? 0 : index * 0.045,
ease: "easeOut",
}}
whileHover={reduceMotion ? undefined : { y: -3 }}
>
<PromoCard
card={card}
latestAgenda={latestAgenda}
latestPost={latestPost}
/>
</motion.div>
))}
</motion.div>
</AnimatePresence>
</div>
</div>
</aside>
);
}
export function LayoutLeftPromoRail({
latestAgenda,
latestPost,
}: {
latestAgenda: LayoutContentPromo | null;
latestPost: LayoutContentPromo | null;
}) {
return (
<PromoRail
side="left"
slides={LEFT_LAYOUT_PROMO_SLIDES}
latestAgenda={latestAgenda}
latestPost={latestPost}
/>
);
}
export function LayoutRightPromoRail() {
return <PromoRail side="right" slides={RIGHT_LAYOUT_PROMO_SLIDES} />;
}
+5
View File
@@ -97,6 +97,11 @@ export async function getAllAgendaArticles(locale?: string): Promise<BlogArticle
return articles.filter((article) => isNewsletterCategory(article.category));
}
export async function getLatestAgendaArticle(locale?: string) {
const articles = await getAllAgendaArticles(locale);
return articles[0] ?? null;
}
export async function getHomeBlogNews(locale?: string, limit = 3) {
const articles = await getAllBlogArticles(locale);
+202
View File
@@ -0,0 +1,202 @@
import type { Localized } from "@/lib/locale";
export type LayoutPromoCopyKey =
| "weeklyEyebrow"
| "weeklyTitle"
| "weeklyFallback"
| "weeklyCta"
| "projectsTitle"
| "projectsDescription"
| "projectsCta"
| "latestPostEyebrow"
| "latestPostTitle"
| "latestPostFallback"
| "latestPostCta"
| "anatomyTitle"
| "anatomyDescription"
| "anatomyCta"
| "youtubeEyebrow"
| "youtubeTitle"
| "youtubeDescription"
| "youtubeCta"
| "designSystemEyebrow"
| "designSystemTitle"
| "designSystemDescription"
| "designSystemCta"
| "communityEyebrow"
| "communityTitle"
| "communityDescription"
| "communityCta"
| "referencesEyebrow"
| "referencesTitle"
| "referencesDescription"
| "referencesCta"
| "sponsorsEyebrow"
| "sponsorsTitle"
| "sponsorsDescription"
| "sponsorsCta"
| "contactTitle"
| "contactDescription"
| "contactCta"
| "linkedinEyebrow"
| "linkedinTitle"
| "linkedinDescription"
| "linkedinCta"
| "instagramEyebrow"
| "instagramTitle"
| "instagramDescription"
| "instagramCta";
export type LayoutPromoCardDefinition = {
id: string;
kind?: "standard" | "sponsors";
eyebrowKey?: LayoutPromoCopyKey;
titleKey: LayoutPromoCopyKey;
descriptionKey: LayoutPromoCopyKey;
ctaKey: LayoutPromoCopyKey;
href: string | Localized;
icon: string;
iconSurface?: "accent" | "foreground" | "primary";
surface?: "default" | "primary";
buttonVariant?: "default" | "outline" | "secondary";
external?: boolean;
contentSource?: "latestAgenda" | "latestPost";
};
export type LayoutPromoSlide = readonly LayoutPromoCardDefinition[];
export const LEFT_LAYOUT_PROMO_SLIDES: readonly LayoutPromoSlide[] = [
[
{
id: "weekly-agenda",
eyebrowKey: "weeklyEyebrow",
titleKey: "weeklyTitle",
descriptionKey: "weeklyFallback",
ctaKey: "weeklyCta",
href: "/agenda",
icon: "mdi:newspaper-variant-outline",
surface: "primary",
buttonVariant: "default",
contentSource: "latestAgenda",
},
{
id: "projects",
titleKey: "projectsTitle",
descriptionKey: "projectsDescription",
ctaKey: "projectsCta",
href: "/projects",
icon: "mdi:layers-triple-outline",
},
{
id: "latest-post",
eyebrowKey: "latestPostEyebrow",
titleKey: "latestPostTitle",
descriptionKey: "latestPostFallback",
ctaKey: "latestPostCta",
href: "/blog",
icon: "mdi:post-outline",
contentSource: "latestPost",
},
{
id: "javascript-anatomy",
titleKey: "anatomyTitle",
descriptionKey: "anatomyDescription",
ctaKey: "anatomyCta",
href: "/content",
icon: "ri:twitter-x-fill",
iconSurface: "foreground",
},
],
[
{
id: "youtube",
eyebrowKey: "youtubeEyebrow",
titleKey: "youtubeTitle",
descriptionKey: "youtubeDescription",
ctaKey: "youtubeCta",
href: "https://youtube.com/@poyrazavsever",
icon: "mdi:youtube",
iconSurface: "primary",
external: true,
},
{
id: "poyraz-ui",
eyebrowKey: "designSystemEyebrow",
titleKey: "designSystemTitle",
descriptionKey: "designSystemDescription",
ctaKey: "designSystemCta",
href: "https://ui.poyrazavsever.com",
icon: "mdi:palette-swatch-outline",
external: true,
},
{
id: "community",
eyebrowKey: "communityEyebrow",
titleKey: "communityTitle",
descriptionKey: "communityDescription",
ctaKey: "communityCta",
href: "/about/volunteer-community",
icon: "mdi:account-group-outline",
},
{
id: "references",
eyebrowKey: "referencesEyebrow",
titleKey: "referencesTitle",
descriptionKey: "referencesDescription",
ctaKey: "referencesCta",
href: "/about/references",
icon: "mdi:comment-quote-outline",
},
],
];
export const RIGHT_LAYOUT_PROMO_SLIDES: readonly LayoutPromoSlide[] = [
[
{
id: "sponsors",
kind: "sponsors",
eyebrowKey: "sponsorsEyebrow",
titleKey: "sponsorsTitle",
descriptionKey: "sponsorsDescription",
ctaKey: "sponsorsCta",
href: "/media-kit",
icon: "mdi:handshake-outline",
buttonVariant: "default",
},
{
id: "contact",
titleKey: "contactTitle",
descriptionKey: "contactDescription",
ctaKey: "contactCta",
href: "/contact",
icon: "mdi:message-text-outline",
iconSurface: "primary",
surface: "primary",
buttonVariant: "default",
},
],
[
{
id: "linkedin",
eyebrowKey: "linkedinEyebrow",
titleKey: "linkedinTitle",
descriptionKey: "linkedinDescription",
ctaKey: "linkedinCta",
href: "https://www.linkedin.com/in/poyrazavsever/",
icon: "mdi:linkedin",
external: true,
},
{
id: "instagram",
eyebrowKey: "instagramEyebrow",
titleKey: "instagramTitle",
descriptionKey: "instagramDescription",
ctaKey: "instagramCta",
href: "https://instagram.com/poyraz_avsever",
icon: "mdi:instagram",
surface: "primary",
buttonVariant: "default",
external: true,
},
],
];
+62
View File
@@ -1,5 +1,6 @@
{
"Nav": {
"home": "Home",
"about": "About",
"blog": "Blog",
"agenda": "Weekly Agenda",
@@ -12,6 +13,10 @@
"socialLinks": "Social Links",
"others": "Others",
"animationResources": "Animation Resources",
"links": "Links",
"mediaKit": "Media Kit",
"references": "References",
"volunteerCommunity": "Volunteering & Community",
"backToMenu": "Back to menu",
"menu": "Menu",
"mobileMenu": "Mobile Menu",
@@ -35,6 +40,58 @@
"sponsors": "Sponsors"
}
},
"LayoutPromos": {
"leftRailLabel": "Featured content",
"rightRailLabel": "Partnerships and contact",
"slideNavigationLabel": "Promotional slides",
"slideCta": "Go to slide {slide}",
"slideStatus": "Slide {current} of {total}",
"weeklyEyebrow": "This week",
"weeklyTitle": "Have you read this week's agenda?",
"weeklyFallback": "Weekly developments from software, technology, design, and artificial intelligence.",
"weeklyCta": "Read the agenda",
"projectsTitle": "Curious how I build my projects?",
"projectsDescription": "Explore the decisions, architecture, and outcomes behind selected products through detailed case studies.",
"projectsCta": "Explore projects",
"latestPostEyebrow": "New post",
"latestPostTitle": "What's new on the blog?",
"latestPostFallback": "Read my latest notes on software and product development.",
"latestPostCta": "Read the post",
"anatomyTitle": "Have you watched JavaScript Anatomy?",
"anatomyDescription": "Explore my series explaining JavaScript concepts through concise 1-4 minute landscape videos.",
"anatomyCta": "Watch the series",
"youtubeEyebrow": "YouTube",
"youtubeTitle": "Join a community of 8K+ people.",
"youtubeDescription": "Watch practical videos about software, artificial intelligence, and product development.",
"youtubeCta": "Visit the channel",
"designSystemEyebrow": "Poyraz UI",
"designSystemTitle": "Explore the design system behind this site.",
"designSystemDescription": "See the components, design decisions, and live examples I use across the portfolio.",
"designSystemCta": "Open the docs",
"communityEyebrow": "Community",
"communityTitle": "What do I do beyond writing code?",
"communityDescription": "Explore my workshops, hackathons, mentoring, and volunteer community work.",
"communityCta": "Community work",
"referencesEyebrow": "References",
"referencesTitle": "What do the people I work with say?",
"referencesDescription": "Read feedback from teammates and people I have built products with.",
"referencesCta": "Read references",
"sponsorsEyebrow": "Sponsors",
"sponsorsTitle": "These brands have sponsored my work so far.",
"sponsorsDescription": "Would you like to introduce your brand to my software and technology-focused audience?",
"sponsorsCta": "Become a sponsor",
"contactTitle": "Have an idea? Let's build it together.",
"contactDescription": "Reach out directly for a project, partnership, or content idea.",
"contactCta": "Get in touch",
"linkedinEyebrow": "LinkedIn",
"linkedinTitle": "Follow my professional journey.",
"linkedinDescription": "I share my projects, technical experiences, and notes from the industry on LinkedIn.",
"linkedinCta": "Visit LinkedIn",
"instagramEyebrow": "Instagram",
"instagramTitle": "Technology, in a shorter format.",
"instagramDescription": "Follow for concise technical videos, moments from events, and a look behind my creative process.",
"instagramCta": "Follow on Instagram"
},
"About": {
"title": "About me",
"contactCta": "Contact me",
@@ -134,12 +191,17 @@
"copied": "Copied"
},
"Links": {
"title": "My links",
"desc": "Here you can find all my social accounts, portfolio pages, and quick access links in one place. Open and share directly.",
"socialLinks": "Social media links",
"quickLinks": "Quick access",
"quickLinksDesc": "Frequently used projects, resources, and resume",
"allLinks": "All Links",
"allLinksDesc": "Pages, resources, and social profiles",
"selectCategory": "Select category",
"allCategories": "All categories",
"searchPlaceholder": "Search links... github, medium, /blog",
"searchAriaLabel": "Search links",
"empty": "No links found matching the selected filter.",
"categories": {
"navigation": "Pages",
+62
View File
@@ -1,5 +1,6 @@
{
"Nav": {
"home": "Ana Sayfa",
"about": "Hakkımda",
"blog": "Blog",
"agenda": "Haftalık Gündem",
@@ -12,6 +13,10 @@
"socialLinks": "Sosyal Bağlantılar",
"others": "Diğerleri",
"animationResources": "Animasyon Kaynakları",
"links": "Bağlantılar",
"mediaKit": "Medya Kiti",
"references": "Referanslar",
"volunteerCommunity": "Gönüllük ve Topluluk",
"backToMenu": "Menüye dön",
"menu": "Menü",
"mobileMenu": "Mobil Menü",
@@ -35,6 +40,58 @@
"sponsors": "Sponsorlar"
}
},
"LayoutPromos": {
"leftRailLabel": "Öne çıkan içerikler",
"rightRailLabel": "İş birlikleri ve iletişim",
"slideNavigationLabel": "Tanıtım slaytları",
"slideCta": "{slide}. slayda git",
"slideStatus": "{current} / {total}. slayt",
"weeklyEyebrow": "Bu hafta",
"weeklyTitle": "Bu haftanın gündemini okudun mu?",
"weeklyFallback": "Yazılım, teknoloji, tasarım ve yapay zekâ dünyasından haftalık gelişmeler.",
"weeklyCta": "Gündemi oku",
"projectsTitle": "Nasıl geliştirdiğimi merak ediyor musun?",
"projectsDescription": "Seçili projelerde aldığım kararları, mimariyi ve ortaya çıkan sonuçları vaka çalışmalarıyla incele.",
"projectsCta": "Projeleri incele",
"latestPostEyebrow": "Yeni yazı",
"latestPostTitle": "Blogda yeni ne var?",
"latestPostFallback": "Yazılım ve ürün geliştirme üzerine son notlarıma göz at.",
"latestPostCta": "Yazıyı oku",
"anatomyTitle": "JavaScript Anatomisi'ni izledin mi?",
"anatomyDescription": "JavaScript kavramlarını 1-4 dakikalık kısa ve yatay videolarla anlattığım seriyi keşfet.",
"anatomyCta": "Seriyi izle",
"youtubeEyebrow": "YouTube",
"youtubeTitle": "8 B+ kişilik topluluğa katıl.",
"youtubeDescription": "Yazılım, yapay zekâ ve ürün geliştirme üzerine uygulamalı videoları izle.",
"youtubeCta": "Kanala git",
"designSystemEyebrow": "Poyraz UI",
"designSystemTitle": "Bu sitenin tasarım sistemini keşfet.",
"designSystemDescription": "Kullandığım bileşenleri, tasarım kararlarını ve canlı örnekleri incele.",
"designSystemCta": "Dokümantasyonu aç",
"communityEyebrow": "Topluluk",
"communityTitle": "Kodun dışında neler yapıyorum?",
"communityDescription": "Workshop, hackathon, mentörlük ve gönüllülük çalışmalarımı incele.",
"communityCta": "Topluluk çalışmaları",
"referencesEyebrow": "Referanslar",
"referencesTitle": "Birlikte çalıştığım insanlar ne diyor?",
"referencesDescription": "Ekip arkadaşlarımdan ve birlikte ürettiğim insanlardan gelen yorumları oku.",
"referencesCta": "Referansları oku",
"sponsorsEyebrow": "Sponsorlar",
"sponsorsTitle": "Bak, bunlar bana şimdiye kadar sponsor oldu.",
"sponsorsDescription": "Sen de markanı yazılım ve teknoloji odaklı kitlemle buluşturmak ister misin?",
"sponsorsCta": "Sen de sponsor ol",
"contactTitle": "Bir fikrin mi var? Birlikte üretelim.",
"contactDescription": "Proje, iş birliği veya içerik fikrin için doğrudan iletişime geç.",
"contactCta": "İletişime geç",
"linkedinEyebrow": "LinkedIn",
"linkedinTitle": "Profesyonel yolculuğumu takip et.",
"linkedinDescription": "Projelerimi, teknik deneyimlerimi ve sektörden notlarımı LinkedIn'de paylaşıyorum.",
"linkedinCta": "LinkedIn'e git",
"instagramEyebrow": "Instagram",
"instagramTitle": "Teknolojinin daha kısa hâli burada.",
"instagramDescription": "Kısa teknik videolar, etkinliklerden anlar ve üretim sürecimin perde arkası için takip et.",
"instagramCta": "Instagram'da takip et"
},
"About": {
"title": "Hakkımda",
"contactCta": "İletişime geç",
@@ -134,12 +191,17 @@
"copied": "Kopyalandı"
},
"Links": {
"title": "Bağlantılarım",
"desc": "Burada sosyal hesaplarım, portfolyo sayfalarım ve hızlı erişim linklerimin tamamı tek yerde duruyor. Direkt açıp paylaşabilirsin.",
"socialLinks": "Sosyal medya bağlantıları",
"quickLinks": "Hızlı erişim",
"quickLinksDesc": "Sık kullanılan projeler, kaynaklar ve özgeçmiş",
"allLinks": "Tüm Linkler",
"allLinksDesc": "Sayfalar, kaynaklar ve sosyal profiller",
"selectCategory": "Kategori seç",
"allCategories": "Tüm kategoriler",
"searchPlaceholder": "Link ara... github, medium, /blog",
"searchAriaLabel": "Bağlantılarda ara",
"empty": "Seçili filtreye uygun link bulunamadı.",
"categories": {
"navigation": "Sayfalar",