Compare commits

..
33 Commits
Author SHA1 Message Date
poyrazavsever 1dfc2a7bc8 fix: fixed ataturk widget z index problem 2026-08-31 21:33:56 +03:00
poyrazavsever a2538e53c0 feat(technologies): add full stack directory and home preview 2026-08-31 20:19:42 +03:00
poyrazavsever ddeab06b45 fix(seo): correct canonicals and navigation semantics 2026-08-31 20:08:11 +03:00
poyrazavsever 9d5f347ffb perf(images): optimize cards and compact logo assets 2026-08-31 20:08:04 +03:00
poyrazavsever 340d504ac8 feat(home): add technology stack section 2026-08-31 20:07:57 +03:00
poyrazavsever 581b1d77ef docs(media-kit): document live YouTube configuration 2026-08-31 19:39:58 +03:00
poyrazavsever d55b04d4b8 feat(links): integrate directory into the main layout 2026-08-31 19:39:28 +03:00
poyrazavsever eca56fab5c feat(layout): add animated promotional side rails 2026-08-31 19:39:22 +03:00
poyrazavsever 1fd03bf614 feat(media-kit): refresh audience insights and sponsor links 2026-08-31 19:39:14 +03:00
poyrazavsever ca45b97c60 fix(projects): enforce contribution colors by theme 2026-08-31 12:40:20 +03:00
poyrazavsever d2c30da602 feat(about): add 2026 community roles 2026-08-31 12:37:29 +03:00
poyrazavsever 9cdb78b9b8 fix(projects): improve empty contribution contrast 2026-08-31 12:28:42 +03:00
poyrazavsever a2259848dc feat(projects): add interactive GitHub contribution graph 2026-08-31 12:26:04 +03:00
poyrazavsever 1522ef0355 feat: updated google analytics 2026-08-31 12:06:11 +03:00
poyrazavsever f10bc9e49c feat: added new project detail page 2026-08-31 11:39:59 +03:00
poyrazavsever 37af80c0be feat(seo): add widescreen social previews 2026-08-31 10:36:52 +03:00
poyrazavsever caf9fceb7a feat(seo): expand sitemap and structured data 2026-08-31 10:27:33 +03:00
poyrazavsever e2c33d6b29 feat(seo): add localized route metadata 2026-08-31 10:23:53 +03:00
poyrazavsever 87e6279dec perf(images): serve optimized portfolio media 2026-08-31 10:20:02 +03:00
poyrazavsever 5c48b029ca feat(content): organize platform media sections 2026-08-31 10:13:35 +03:00
poyrazavsever 79dd81240d chore(seo): index weekly agenda routes 2026-08-31 09:28:19 +03:00
poyrazavsever 36f888931d feat(agenda): add weekly agenda experience 2026-08-31 09:28:11 +03:00
poyrazavsever ea35a30198 refactor(content): separate blog and agenda feeds 2026-08-31 09:28:01 +03:00
poyrazavsever f7274e49ad feat(cat): let neko wander independently 2026-08-31 09:01:34 +03:00
poyrazavsever 6b433855e1 feat(projects): link OSTIM portals 2026-08-30 21:59:23 +03:00
poyrazavsever 4429147434 feat(theme): add sleeping dark mode portrait 2026-08-30 20:18:04 +03:00
poyrazavsever 5dc68b2d5a feat(projects): localize project titles 2026-08-30 20:13:31 +03:00
poyrazavsever ea8fcb9db4 feat: added new photos 2026-08-30 20:11:22 +03:00
poyrazavsever 4d5738343f fix(projects): correct OSTIM UI kit label 2026-08-30 20:11:03 +03:00
poyrazavsever d606196e06 fix(projects): preserve popover card width 2026-08-30 20:10:33 +03:00
poyrazavsever 738338932f feat(projects): add OSTIM portals and stack popovers 2026-08-30 20:06:51 +03:00
poyrazavsever f71aedf2b5 feat: added new blg post 2026-08-30 19:09:36 +03:00
Poyraz e2869ffb47 gitea deneme maksatlı commit 2026-08-29 20:21:59 +03:00
228 changed files with 7819 additions and 18672 deletions
+11 -1
View File
@@ -1,4 +1,4 @@
# Portfolio New
# Portfolio New (Gitea Test)
Bu monorepo, Next.js tabanlı portfolyo web uygulaması ve Electron ile geliştirilen veri yönetim panelini bir arada sunar.
@@ -75,6 +75,16 @@ pnpm build
pnpm start
```
## Canlı medya kiti verileri
Medya kiti, YouTube kanalının herkese açık abone, toplam görüntülenme ve video
sayılarını YouTube Data API üzerinden saatlik olarak yenileyebilir. Yayın ortamına
`YOUTUBE_API_KEY` eklemek yeterlidir. İsteğe bağlı olarak `YOUTUBE_CHANNEL_ID`
tanımlanabilir; tanımlanmadığında `@poyrazavsever` kullanıcı adı kullanılır.
API anahtarı yoksa veya YouTube isteği başarısız olursa sayfa, `data/media-kit.ts`
içindeki son doğrulanmış YouTube Studio verilerine otomatik olarak geri döner.
## Electron Data Panel Komutları
```bash
+29 -3
View File
@@ -1,10 +1,36 @@
import { AboutContent } from "@/components/about-content";
import { PersonJsonLd } from "@/components/json-ld";
import { ProfilePageJsonLd } from "@/components/json-ld";
import { getLocalizedUrl, getStaticPageMetadata } from "@/lib/seo";
import { getTranslations } from "next-intl/server";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({ locale, page: "about", path: "/about" });
}
export default async function AboutPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const siteLocale = locale === "en" ? "en" : "tr";
const t = await getTranslations({
locale: siteLocale,
namespace: "Seo.about",
});
export default function AboutPage() {
return (
<>
<PersonJsonLd
<ProfilePageJsonLd
pageUrl={getLocalizedUrl(siteLocale, "/about")}
pageName={t("title")}
description={t("description")}
locale={siteLocale}
name="Poyraz Avsever"
jobTitle="Fullstack Developer"
sameAs={[
+14
View File
@@ -1,4 +1,18 @@
import { ReferencesDetailContent } from "@/components/references-detail-content";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "references",
path: "/about/references",
});
}
export default function AboutReferencesPage() {
return <ReferencesDetailContent />;
@@ -1,4 +1,18 @@
import { VolunteerCommunityContent } from "@/components/volunteer-community-content";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "volunteerCommunity",
path: "/about/volunteer-community",
});
}
export default function AboutVolunteerCommunityPage() {
return <VolunteerCommunityContent />;
+100
View File
@@ -0,0 +1,100 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { BlogDetailContent } from "@/components/blog-detail-content";
import { ArticleJsonLd } from "@/components/json-ld";
import { getBlogDetailBySlug, getBlogTranslations } from "@/data/blog-detail";
import { isNewsletterCategory } from "@/data/blog";
import {
createAlternates,
getLocalizedUrl,
type SiteLocale,
} from "@/lib/seo";
type AgendaDetailPageProps = {
params: Promise<{ locale: string; slug: string }>;
};
export async function generateMetadata({
params,
}: AgendaDetailPageProps): Promise<Metadata> {
const { locale, slug } = await params;
const post = await getBlogDetailBySlug(slug);
if (!post || post.lang !== locale || !isNewsletterCategory(post.category)) {
return { title: locale === "en" ? "Agenda post not found" : "Gündem yazısı bulunamadı" };
}
const siteLocale = locale as SiteLocale;
const translations = await getBlogTranslations(post);
const paths = Object.fromEntries(
translations.map((translation) => [
translation.lang,
`/agenda/${translation.slug}`,
]),
);
const url = getLocalizedUrl(siteLocale, `/agenda/${post.slug}`);
return {
title: post.title,
description: post.excerpt,
alternates: createAlternates(siteLocale, paths),
openGraph: {
title: post.title,
description: post.excerpt,
url,
type: "article",
siteName: "Poyraz Avsever",
locale: locale === "en" ? "en_US" : "tr_TR",
alternateLocale:
translations.length > 1
? [locale === "en" ? "tr_TR" : "en_US"]
: undefined,
publishedTime: post.date,
authors: [post.author],
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.coverImage],
creator: "@poyrazavsever",
},
};
}
export default async function AgendaDetailPage({ params }: AgendaDetailPageProps) {
const { locale, slug } = await params;
const post = await getBlogDetailBySlug(slug);
if (!post || post.lang !== locale || !isNewsletterCategory(post.category)) {
notFound();
}
const url = getLocalizedUrl(
locale as SiteLocale,
`/agenda/${post.slug}`,
);
return (
<>
<ArticleJsonLd
title={post.title}
description={post.excerpt}
url={url}
image={post.coverImage}
datePublished={post.date}
authorName={post.author}
locale={locale as SiteLocale}
/>
<BlogDetailContent post={post} section="agenda" />
</>
);
}
+39
View File
@@ -0,0 +1,39 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { BlogContent } from "@/components/blog-content";
import { getAgendaPageData } from "@/data/blog";
import { createPageMetadata } from "@/lib/seo";
type AgendaPageProps = {
params: Promise<{ locale: string }>;
searchParams?: Promise<{
page?: string | string[];
search?: string | string[];
}>;
};
export async function generateMetadata({ params }: AgendaPageProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Agenda" });
return createPageMetadata({
locale: locale === "en" ? "en" : "tr",
title: t("title"),
description: t("description"),
path: "/agenda",
});
}
export default async function AgendaPage({ params, searchParams }: AgendaPageProps) {
const { locale } = await params;
const resolved = searchParams ? await searchParams : undefined;
const pageParam = Array.isArray(resolved?.page) ? resolved.page[0] : resolved?.page;
const searchParam = Array.isArray(resolved?.search)
? resolved.search[0]
: resolved?.search;
const page = Number(pageParam ?? "1");
const currentPage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
const data = await getAgendaPageData(locale, currentPage, 12, searchParam);
return <BlogContent data={data} section="agenda" />;
}
+17 -21
View File
@@ -6,19 +6,17 @@ import {
getAnimationSourceBySlug,
listAnimationSources,
} from "@/data/animation-sources";
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
import {
createAlternates,
getAbsoluteUrl,
getLocalizedUrl,
type SiteLocale,
} from "@/lib/seo";
type AnimationSourceDetailPageProps = {
params: Promise<{ locale: string; slug: string }>;
};
function getLocalizedPath(locale: string, slug: string) {
const localePrefix = locale === "tr" ? "" : `/${locale}`;
return `${localePrefix}/animation-sources/${slug}`;
}
export async function generateStaticParams() {
const sources = await listAnimationSources();
return sources.map((source) => ({
@@ -39,21 +37,15 @@ export async function generateMetadata({
return { title: locale === "en" ? "Source not found" : "Kaynak bulunamadı" };
}
const url = `${SITE_URL}${getLocalizedPath(locale, source.slug)}`;
const socialImageUrl = new URL(source.coverImage, SITE_URL).toString();
const turkishUrl = `${SITE_URL}${getLocalizedPath("tr", source.slug)}`;
const englishUrl = `${SITE_URL}${getLocalizedPath("en", source.slug)}`;
const siteLocale = locale as SiteLocale;
const path = `/animation-sources/${source.slug}`;
const url = getLocalizedUrl(siteLocale, path);
const socialImageUrl = getAbsoluteUrl(source.coverImage);
return {
title: source.title,
description: source.excerpt,
alternates: {
canonical: url,
languages: {
"tr-TR": turkishUrl,
"en-US": englishUrl,
},
},
alternates: createAlternates(siteLocale, { tr: path, en: path }),
openGraph: {
title: source.title,
description: source.excerpt,
@@ -67,7 +59,7 @@ export async function generateMetadata({
images: [
{
url: socialImageUrl,
type: "image/gif",
type: "image/webp",
width: 480,
height: 480,
alt: source.title,
@@ -97,7 +89,10 @@ export default async function AnimationSourceDetailPage({
if (!source) notFound();
const url = `${SITE_URL}${getLocalizedPath(locale, source.slug)}`;
const url = getLocalizedUrl(
locale as SiteLocale,
`/animation-sources/${source.slug}`,
);
return (
<>
@@ -108,6 +103,7 @@ export default async function AnimationSourceDetailPage({
image={source.coverImage}
datePublished={source.date}
authorName={source.author}
locale={locale === "en" ? "en" : "tr"}
/>
<AnimationSourceDetailContent source={source} />
</>
+17 -22
View File
@@ -2,9 +2,12 @@ import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { AnimationSourcesContent } from "@/components/animation-sources-content";
import { listAnimationSources } from "@/data/animation-sources";
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
import {
createAlternates,
getAbsoluteUrl,
getLocalizedUrl,
type SiteLocale,
} from "@/lib/seo";
type AnimationSourcesPageProps = {
params: Promise<{ locale: string }>;
@@ -14,26 +17,18 @@ export async function generateMetadata({
params,
}: AnimationSourcesPageProps): Promise<Metadata> {
const { locale } = await params;
const [t, sources] = await Promise.all([
getTranslations({ locale, namespace: "AnimationSources" }),
listAnimationSources(locale),
]);
const localizedPath = locale === "en" ? "/en/animation-sources" : "/animation-sources";
const url = `${SITE_URL}${localizedPath}`;
const socialImagePath = sources[0]?.coverImage ?? "/logo/logo.png";
const socialImageUrl = new URL(socialImagePath, SITE_URL).toString();
const isGif = socialImagePath.toLowerCase().endsWith(".gif");
const t = await getTranslations({ locale, namespace: "AnimationSources" });
const siteLocale = locale as SiteLocale;
const url = getLocalizedUrl(siteLocale, "/animation-sources");
const socialImageUrl = getAbsoluteUrl("/og.png");
return {
title: t("title"),
description: t("description"),
alternates: {
canonical: url,
languages: {
"tr-TR": `${SITE_URL}/animation-sources`,
"en-US": `${SITE_URL}/en/animation-sources`,
},
},
alternates: createAlternates(siteLocale, {
tr: "/animation-sources",
en: "/animation-sources",
}),
openGraph: {
title: t("title"),
description: t("description"),
@@ -44,9 +39,9 @@ export async function generateMetadata({
images: [
{
url: socialImageUrl,
type: isGif ? "image/gif" : "image/png",
width: isGif ? 480 : 1200,
height: isGif ? 480 : 1200,
type: "image/png",
width: 1200,
height: 630,
alt: t("title"),
},
],
+31 -6
View File
@@ -1,10 +1,14 @@
import { notFound } from "next/navigation";
import { notFound, permanentRedirect } from "next/navigation";
import { Metadata } from "next";
import { BlogDetailContent } from "@/components/blog-detail-content";
import { ArticleJsonLd } from "@/components/json-ld";
import { getBlogDetailBySlug } from "@/data/blog-detail";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
import { getBlogDetailBySlug, getBlogTranslations } from "@/data/blog-detail";
import { isNewsletterCategory } from "@/data/blog";
import {
createAlternates,
getLocalizedUrl,
type SiteLocale,
} from "@/lib/seo";
type BlogDetailPageProps = {
params: Promise<{ locale: string; slug: string }>;
@@ -20,16 +24,31 @@ export async function generateMetadata({ params }: BlogDetailPageProps): Promise
};
}
const url = `${SITE_URL}/blog/${post.slug}`;
const siteLocale = locale as SiteLocale;
const translations = await getBlogTranslations(post);
const paths = Object.fromEntries(
translations.map((translation) => [
translation.lang,
`/blog/${translation.slug}`,
]),
);
const url = getLocalizedUrl(siteLocale, `/blog/${post.slug}`);
return {
title: post.title,
description: post.excerpt,
alternates: createAlternates(siteLocale, paths),
openGraph: {
title: post.title,
description: post.excerpt,
url,
type: "article",
siteName: "Poyraz Avsever",
locale: locale === "en" ? "en_US" : "tr_TR",
alternateLocale:
translations.length > 1
? [locale === "en" ? "tr_TR" : "en_US"]
: undefined,
publishedTime: post.date,
authors: [post.author],
images: [
@@ -46,6 +65,7 @@ export async function generateMetadata({ params }: BlogDetailPageProps): Promise
title: post.title,
description: post.excerpt,
images: [post.coverImage],
creator: "@poyrazavsever",
},
};
}
@@ -58,15 +78,20 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
notFound();
}
if (isNewsletterCategory(post.category)) {
permanentRedirect(`/${locale}/agenda/${post.slug}`);
}
return (
<>
<ArticleJsonLd
title={post.title}
description={post.excerpt}
url={`${SITE_URL}/blog/${post.slug}`}
url={getLocalizedUrl(locale as SiteLocale, `/blog/${post.slug}`)}
image={post.coverImage}
datePublished={post.date}
authorName={post.author}
locale={locale as SiteLocale}
/>
<BlogDetailContent post={post} />
</>
+6
View File
@@ -1,11 +1,17 @@
import { BlogContent } from "@/components/blog-content";
import { getBlogPageData } from "@/data/blog";
import { getStaticPageMetadata } from "@/lib/seo";
type BlogPageProps = {
params: Promise<{ locale: string }>;
searchParams?: Promise<{ page?: string | string[]; category?: string | string[]; search?: string | string[] }>;
};
export async function generateMetadata({ params }: BlogPageProps) {
const { locale } = await params;
return getStaticPageMetadata({ locale, page: "blog", path: "/blog" });
}
export default async function BlogPage({ params, searchParams }: BlogPageProps) {
const { locale } = await params;
const resolved = searchParams ? await searchParams : undefined;
+14
View File
@@ -1,4 +1,18 @@
import { ContactContent } from "@/components/contact-content";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "contact",
path: "/contact",
});
}
export default function ContactPage() {
return <ContactContent />;
+16
View File
@@ -1,6 +1,21 @@
import { ContentContent } from "@/components/content-content";
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
import { X_JAVASCRIPT_ANATOMY_VIDEOS } from "@/data/x-videos";
import { getPdfNotes } from "@/lib/content-page";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "content",
path: "/content",
});
}
export default async function ContentPage() {
const pdfFiles = await getPdfNotes();
@@ -9,6 +24,7 @@ export default async function ContentPage() {
<ContentContent
youtubeLinks={YOUTUBE_VIDEO_LINKS}
pdfFiles={pdfFiles}
xVideos={X_JAVASCRIPT_ANATOMY_VIDEOS}
/>
);
}
+13 -5
View File
@@ -1,11 +1,19 @@
import { GalleryContent } from "@/components/gallery-content";
import { GALLERY_IMAGES } from "@/data/gallery";
import type { Metadata } from "next";
import { getStaticPageMetadata } from "@/lib/seo";
export const metadata: Metadata = {
title: "Galeri",
description: "Poyraz Avsever'in tasarımları, projeleri ve görsel içeriklerinden oluşan galeri portföyü.",
};
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "gallery",
path: "/gallery",
});
}
export default function GalleryPage() {
return <GalleryContent images={GALLERY_IMAGES} />;
+46 -16
View File
@@ -9,6 +9,8 @@ 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";
import { SITE_URL } from "@/lib/seo";
export async function generateMetadata({
params,
@@ -23,40 +25,47 @@ export async function generateMetadata({
const description = t("description");
return {
metadataBase: new URL("https://poyrazavsever.com"),
metadataBase: new URL(SITE_URL),
title: {
default: title,
template: titleTemplate,
},
description: description,
applicationName: "Poyraz Avsever Portfolyo",
alternates: {
canonical: "/",
},
icons: {
icon: [
{ url: "/favicon.ico", sizes: "any" },
{ url: "/logo/logo.png", type: "image/jpeg" },
{ url: "/logo/logo-96.webp", type: "image/webp", sizes: "96x96" },
],
shortcut: "/favicon.ico",
apple: "/logo/logo.png",
apple: "/logo/apple-touch-icon.png",
},
authors: [{ name: "Poyraz Avsever" }],
creator: "Poyraz Avsever",
publisher: "Poyraz Avsever",
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
openGraph: {
type: "website",
locale: locale === "tr" ? "tr_TR" : "en_US",
url: "https://poyrazavsever.com",
siteName: title,
title: title,
description: description,
images: [
{
url: "/logo/logo.png",
url: "/og.png",
width: 1200,
height: 1200,
alt: "Poyraz Avsever Logo",
height: 630,
alt: t("socialImageAlt"),
},
],
},
@@ -64,7 +73,7 @@ export async function generateMetadata({
card: "summary_large_image",
title: title,
description: description,
images: ["/logo/logo.png"],
images: ["/og.png"],
creator: "@poyrazavsever",
},
keywords: [
@@ -109,10 +118,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,
@@ -126,7 +138,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 />
+9 -30
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { LinksContent } from "@/components/links-content";
import { getStaticPageMetadata } from "@/lib/seo";
type LinksPageProps = {
searchParams?: Promise<{
@@ -8,35 +8,14 @@ type LinksPageProps = {
}>;
};
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 async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({ locale, page: "links", path: "/links" });
}
function firstParam(value?: string | string[]) {
return Array.isArray(value) ? value[0] : value;
+14 -16
View File
@@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { MediaKitContent } from "@/components/media-kit-content";
import type { MediaKitLocale } from "@/data/media-kit";
import { createPageMetadata } from "@/lib/seo";
import { getYouTubeChannelStats } from "@/lib/youtube-channel-stats";
type MediaKitPageProps = {
params: Promise<{ locale: string }>;
@@ -13,25 +15,14 @@ export async function generateMetadata({
const { locale } = await params;
const isTurkish = locale === "tr";
return {
return createPageMetadata({
locale: isTurkish ? "tr" : "en",
title: isTurkish ? "Medya Kiti" : "Media Kit",
description: isTurkish
? "Poyraz Avsever sponsorluk ve marka iş birlikleri medya kiti."
: "Poyraz Avsever media kit for sponsorships and brand partnerships.",
robots: {
index: false,
follow: false,
nocache: true,
googleBot: {
index: false,
follow: false,
noimageindex: true,
},
},
alternates: {
canonical: null,
},
};
path: "/media-kit",
});
}
export default async function MediaKitPage({ params }: MediaKitPageProps) {
@@ -41,5 +32,12 @@ export default async function MediaKitPage({ params }: MediaKitPageProps) {
notFound();
}
return <MediaKitContent locale={locale as MediaKitLocale} />;
const youtubeStats = await getYouTubeChannelStats();
return (
<MediaKitContent
locale={locale as MediaKitLocale}
youtubeStats={youtubeStats}
/>
);
}
+19 -1
View File
@@ -1,8 +1,24 @@
import { HomeHero } from "@/components/home-hero";
import { HomeProjectsSection } from "@/components/home-projects-section";
import { HomeTechnologyStack } from "@/components/home-technology-stack";
import { HomeVideosSection } from "@/components/home-videos-section";
import { ReferencesSection } from "@/components/references-section";
import { getHomeBlogNews } from "@/data/blog";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "home",
path: "/",
absoluteTitle: true,
});
}
export default async function Home({
params,
@@ -14,7 +30,9 @@ export default async function Home({
return (
<section className="flex h-full flex-col overflow-y-auto overflow-x-hidden">
<HomeHero news={homeNews} />
<HomeHero news={homeNews}>
<HomeTechnologyStack />
</HomeHero>
<HomeProjectsSection />
<ReferencesSection />
<HomeVideosSection />
+103
View File
@@ -0,0 +1,103 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { ProjectCaseStudyContent } from "@/components/project-case-study-content";
import { ProjectCaseStudyJsonLd } from "@/components/json-ld";
import {
getProjectCaseStudy,
PROJECT_CASE_STUDY_SLUGS,
type ProjectCaseStudyLocale,
} from "@/data/project-case-studies";
import {
createAlternates,
getAbsoluteUrl,
getLocalizedUrl,
} from "@/lib/seo";
type ProjectCaseStudyPageProps = {
params: Promise<{ locale: string; slug: string }>;
};
export function generateStaticParams() {
return (["tr", "en"] as ProjectCaseStudyLocale[]).flatMap((locale) =>
PROJECT_CASE_STUDY_SLUGS.map((slug) => ({ locale, slug })),
);
}
export const dynamicParams = false;
export async function generateMetadata({
params,
}: ProjectCaseStudyPageProps): Promise<Metadata> {
const { locale, slug } = await params;
const siteLocale: ProjectCaseStudyLocale = locale === "en" ? "en" : "tr";
const project = getProjectCaseStudy(slug, siteLocale);
if (!project) {
return {
title: siteLocale === "en" ? "Project not found" : "Proje bulunamadı",
};
}
const path = `/projects/${project.slug}`;
const url = getLocalizedUrl(siteLocale, path);
const socialImageUrl = getAbsoluteUrl(project.image);
return {
title: project.title,
description: project.summary,
alternates: createAlternates(siteLocale, { tr: path, en: path }),
openGraph: {
title: project.title,
description: project.summary,
url,
siteName: "Poyraz Avsever",
type: "website",
locale: siteLocale === "en" ? "en_US" : "tr_TR",
alternateLocale: siteLocale === "en" ? ["tr_TR"] : ["en_US"],
images: [
{
url: socialImageUrl,
width: 1080,
height: 1080,
alt: project.screenshotAlt,
},
],
},
twitter: {
card: "summary_large_image",
title: project.title,
description: project.summary,
creator: "@poyrazavsever",
images: [{ url: socialImageUrl, alt: project.screenshotAlt }],
},
};
}
export default async function ProjectCaseStudyPage({
params,
}: ProjectCaseStudyPageProps) {
const { locale, slug } = await params;
const siteLocale: ProjectCaseStudyLocale = locale === "en" ? "en" : "tr";
const project = getProjectCaseStudy(slug, siteLocale);
if (!project) notFound();
const url = getLocalizedUrl(siteLocale, `/projects/${project.slug}`);
return (
<>
<ProjectCaseStudyJsonLd
name={project.title}
description={project.summary}
url={url}
liveUrl={project.liveUrl}
image={project.image}
locale={siteLocale}
applicationCategory={project.applicationCategory}
technologies={project.technologies}
features={project.results.map((result) => result.description)}
/>
<ProjectCaseStudyContent project={project} />
</>
);
}
+59 -2
View File
@@ -1,5 +1,62 @@
import { ProjectsContent } from "@/components/projects-content";
import { ProjectsJsonLd } from "@/components/json-ld";
import {
EXTENSIONS,
FIGMA_TEMPLATES,
MOBILE_APPS,
WEB_APPS,
} from "@/data/projects";
import { getLocalizedValue } from "@/lib/locale";
import { getLocalizedUrl, getStaticPageMetadata } from "@/lib/seo";
import { getTranslations } from "next-intl/server";
export default function ProjectsPage() {
return <ProjectsContent />;
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "projects",
path: "/projects",
});
}
export default async function ProjectsPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const siteLocale = locale === "en" ? "en" : "tr";
const t = await getTranslations({
locale: siteLocale,
namespace: "Seo.projects",
});
const projects = [
...WEB_APPS,
...MOBILE_APPS,
...EXTENSIONS,
...FIGMA_TEMPLATES,
].map((project) => ({
name: getLocalizedValue(project.title, siteLocale),
description: getLocalizedValue(project.description, siteLocale),
image: project.image,
url: project.href,
technologies: project.technologies,
}));
return (
<>
<ProjectsJsonLd
name={t("title")}
description={t("description")}
url={getLocalizedUrl(siteLocale, "/projects")}
locale={siteLocale}
projects={projects}
/>
<ProjectsContent />
</>
);
}
+20
View File
@@ -0,0 +1,20 @@
import { TechnologiesContent } from "@/components/technologies-content";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "technologies",
path: "/technologies",
});
}
export default function TechnologiesPage() {
return <TechnologiesContent />;
}
+45 -4
View File
@@ -10,6 +10,12 @@
@layer base {
:root {
--poyraz-font-secondary: "Nunito", ui-rounded, "Avenir Next", system-ui, sans-serif;
--github-contribution-empty: #ebedf0;
}
.dark,
[data-poyraz-theme="dark"] {
--github-contribution-empty: #27272a;
}
}
@@ -22,11 +28,11 @@ html[data-poyraz-theme="light"] body {
background-color: #ffffff;
}
html[data-poyraz-theme="dark"] [data-cursor-portrait] {
display: none;
}
@layer utilities {
.github-contribution-empty {
fill: var(--github-contribution-empty);
}
@keyframes marquee {
0% { transform: translateX(0%); }
100% { transform: translateX(-50%); }
@@ -40,4 +46,39 @@ html[data-poyraz-theme="dark"] [data-cursor-portrait] {
animation-play-state: paused;
}
@keyframes sleepy-z {
0%, 12% {
opacity: 0;
transform: translate3d(0, 0.45rem, 0) scale(0.82) rotate(-8deg);
}
28%, 68% {
opacity: 0.95;
}
100% {
opacity: 0;
transform: translate3d(-0.3rem, -0.85rem, 0) scale(1.12) rotate(4deg);
}
}
.animate-sleepy-z {
animation: sleepy-z 2.8s ease-in-out infinite;
opacity: 0;
will-change: transform, opacity;
}
[data-sleepy-z="2"] {
animation-delay: 0.5s;
}
[data-sleepy-z="3"] {
animation-delay: 1s;
}
@media (prefers-reduced-motion: reduce) {
.animate-sleepy-z {
animation: none;
opacity: 0.8;
}
}
}
+2 -3
View File
@@ -1,6 +1,5 @@
import type { MetadataRoute } from "next";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
import { SITE_URL } from "@/lib/seo";
export default function robots(): MetadataRoute.Robots {
return {
@@ -8,7 +7,7 @@ export default function robots(): MetadataRoute.Robots {
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/_next/", "/media-kit", "/tr/media-kit", "/en/media-kit"],
disallow: ["/api/", "/_next/"],
},
],
sitemap: `${SITE_URL}/sitemap.xml`,
+1 -1
View File
@@ -1,6 +1,6 @@
import { getAllBlogArticles } from "@/data/blog";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
import { SITE_URL } from "@/lib/seo";
const FEED_PATH = "/rss.xml";
const FEED_TITLE = "Poyraz Avsever Blog";
const FEED_DESCRIPTION =
+140 -79
View File
@@ -1,9 +1,57 @@
import { listBlogDetails } from "@/data/blog-detail";
import { listAnimationSources } from "@/data/animation-sources";
import type { MetadataRoute } from "next";
import { listAnimationSources } from "@/data/animation-sources";
import { isNewsletterCategory } from "@/data/blog";
import { listBlogDetails } from "@/data/blog-detail";
import { listProjectCaseStudies } from "@/data/project-case-studies";
import {
getAbsoluteUrl,
getLocalizedUrl,
type SiteLocale,
} from "@/lib/seo";
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
const LOCALES: SiteLocale[] = ["tr", "en"];
const STATIC_ROUTES = [
{ path: "/", changeFrequency: "weekly", priority: 1 },
{ path: "/about", changeFrequency: "monthly", priority: 0.8 },
{ path: "/about/references", changeFrequency: "monthly", priority: 0.5 },
{
path: "/about/volunteer-community",
changeFrequency: "monthly",
priority: 0.5,
},
{ path: "/blog", changeFrequency: "weekly", priority: 0.9 },
{ path: "/agenda", changeFrequency: "weekly", priority: 0.9 },
{ path: "/projects", changeFrequency: "monthly", priority: 0.8 },
{ path: "/content", changeFrequency: "weekly", priority: 0.7 },
{ path: "/gallery", changeFrequency: "monthly", priority: 0.6 },
{ path: "/technologies", changeFrequency: "monthly", priority: 0.7 },
{ path: "/contact", changeFrequency: "yearly", priority: 0.5 },
{ path: "/media-kit", changeFrequency: "monthly", priority: 0.6 },
{ path: "/links", changeFrequency: "monthly", priority: 0.4 },
{
path: "/animation-sources",
changeFrequency: "monthly",
priority: 0.7,
},
] as const;
function toValidDate(value: string) {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? undefined : date;
}
function getLanguageLinks(paths: Partial<Record<SiteLocale, string>>) {
const languages: Record<string, string> = {};
if (paths.tr) languages["tr-TR"] = getLocalizedUrl("tr", paths.tr);
if (paths.en) languages["en-US"] = getLocalizedUrl("en", paths.en);
languages["x-default"] = paths.tr
? getLocalizedUrl("tr", paths.tr)
: getLocalizedUrl("en", paths.en!);
return languages;
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const [posts, animationSources] = await Promise.all([
@@ -11,84 +59,97 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
listAnimationSources(),
]);
const staticRoutes: MetadataRoute.Sitemap = [
{
url: SITE_URL,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1.0,
},
{
url: `${SITE_URL}/about`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${SITE_URL}/about/references`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.5,
},
{
url: `${SITE_URL}/about/volunteer-community`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.5,
},
{
url: `${SITE_URL}/blog`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
url: `${SITE_URL}/projects`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${SITE_URL}/content`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.7,
},
{
url: `${SITE_URL}/contact`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.5,
},
{
url: `${SITE_URL}/links`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.4,
},
{
url: `${SITE_URL}/animation-sources`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
},
];
const staticRoutes: MetadataRoute.Sitemap = STATIC_ROUTES.flatMap((route) => {
const paths = { tr: route.path, en: route.path };
const languages = getLanguageLinks(paths);
const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${SITE_URL}/blog/${post.slug}`,
lastModified: post.date ? new Date(post.date) : new Date(),
changeFrequency: "monthly",
priority: 0.7,
}));
return LOCALES.map((locale) => ({
url: getLocalizedUrl(locale, route.path),
changeFrequency: route.changeFrequency,
priority: route.priority,
alternates: { languages },
}));
});
const postGroups = new Map<string, typeof posts>();
for (const post of posts) {
const key = `${post.category.toLocaleLowerCase()}:${post.coverImage}`;
postGroups.set(key, [...(postGroups.get(key) ?? []), post]);
}
const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => {
const section = isNewsletterCategory(post.category) ? "agenda" : "blog";
const key = `${post.category.toLocaleLowerCase()}:${post.coverImage}`;
const translations = postGroups.get(key) ?? [post];
const paths = Object.fromEntries(
translations.map((translation) => [
translation.lang,
`/${section}/${translation.slug}`,
]),
);
return {
url: getLocalizedUrl(post.lang, `/${section}/${post.slug}`),
lastModified: toValidDate(post.date),
changeFrequency: "monthly",
priority: 0.7,
alternates: { languages: getLanguageLinks(paths) },
images: [getAbsoluteUrl(post.coverImage)],
};
});
const animationGroups = new Map<string, typeof animationSources>();
for (const source of animationSources) {
animationGroups.set(source.slug, [
...(animationGroups.get(source.slug) ?? []),
source,
]);
}
const animationSourceRoutes: MetadataRoute.Sitemap = animationSources.map(
(source) => ({
url: `${SITE_URL}${source.lang === "en" ? "/en" : ""}/animation-sources/${source.slug}`,
lastModified: source.date ? new Date(source.date) : new Date(),
changeFrequency: "monthly",
priority: 0.6,
}),
(source) => {
const translations = animationGroups.get(source.slug) ?? [source];
const paths = Object.fromEntries(
translations.map((translation) => [
translation.lang,
`/animation-sources/${translation.slug}`,
]),
);
return {
url: getLocalizedUrl(
source.lang,
`/animation-sources/${source.slug}`,
),
lastModified: toValidDate(source.date),
changeFrequency: "monthly",
priority: 0.6,
alternates: { languages: getLanguageLinks(paths) },
images: [getAbsoluteUrl(source.coverImage)],
};
},
);
return [...staticRoutes, ...blogRoutes, ...animationSourceRoutes];
const projectCaseStudyRoutes: MetadataRoute.Sitemap = LOCALES.flatMap(
(locale) =>
listProjectCaseStudies(locale).map((project) => {
const path = `/projects/${project.slug}`;
const paths = { tr: path, en: path };
return {
url: getLocalizedUrl(locale, path),
changeFrequency: "monthly" as const,
priority: 0.8,
alternates: { languages: getLanguageLinks(paths) },
images: [getAbsoluteUrl(project.image)],
};
}),
);
return [
...staticRoutes,
...projectCaseStudyRoutes,
...blogRoutes,
...animationSourceRoutes,
];
}
+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>
</>
);
+4 -3
View File
@@ -14,21 +14,22 @@ type AtaturkWidgetModalProps = {
export function AtaturkWidgetModal({ theme }: AtaturkWidgetModalProps) {
const [isLoading, setIsLoading] = useState(true);
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations("AtaturkWidget");
const locale = useLocale();
return (
<Modal>
<Modal open={isOpen} onOpenChange={setIsOpen}>
<ModalTrigger asChild>
<button
type="button"
title={t("title")}
className="fixed bottom-4 left-4 z-50 flex h-14 w-14 items-center justify-center overflow-hidden rounded-full border-2 border-red-600 bg-background shadow-lg transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
className={`fixed bottom-4 left-4 flex h-14 w-14 items-center justify-center overflow-hidden rounded-full border-2 border-red-600 bg-background shadow-lg transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${isOpen ? "z-40" : "z-[80]"}`}
aria-label={t("title")}
>
<div className="relative h-full w-full">
<Image
src="/ataturk.png"
src="/ataturk.webp"
alt={t("alt")}
fill
sizes="56px"
+67 -44
View File
@@ -19,9 +19,13 @@ import type { BlogPageData } from "@/data/blog";
type BlogContentProps = {
data: BlogPageData;
section?: "blog" | "agenda";
};
function buildHref(params: { page?: number; category?: string; search?: string }) {
function buildHref(
params: { page?: number; category?: string; search?: string },
section: "blog" | "agenda",
) {
const qs = new URLSearchParams();
if (params.page && params.page > 1) qs.set("page", String(params.page));
@@ -29,12 +33,15 @@ function buildHref(params: { page?: number; category?: string; search?: string }
if (params.search) qs.set("search", params.search);
const query = qs.toString();
return query ? `/blog?${query}` : "/blog";
const basePath = section === "agenda" ? "/agenda" : "/blog";
return query ? `${basePath}?${query}` : basePath;
}
export function BlogContent({ data }: BlogContentProps) {
export function BlogContent({ data, section = "blog" }: BlogContentProps) {
const router = useRouter();
const t = useTranslations("Blog");
const agendaT = useTranslations("Agenda");
const isAgenda = section === "agenda";
const [searchInput, setSearchInput] = useState(data.searchQuery);
const pageNumbers = Array.from({ length: data.totalPages }, (_, i) => i + 1);
const hasArticles = data.articles.length > 0;
@@ -42,15 +49,15 @@ export function BlogContent({ data }: BlogContentProps) {
const submitSearch = useCallback(
(value: string) => {
const trimmed = value.trim();
router.push(buildHref({ category: data.selectedCategory, search: trimmed }));
router.push(buildHref({ category: data.selectedCategory, search: trimmed }, section));
},
[router, data.selectedCategory],
[router, data.selectedCategory, section],
);
const clearFilters = useCallback(() => {
setSearchInput("");
router.push("/blog");
}, [router]);
router.push(section === "agenda" ? "/agenda" : "/blog");
}, [router, section]);
return (
<section className="flex h-full flex-col gap-4 overflow-y-auto">
@@ -58,24 +65,26 @@ export function BlogContent({ data }: BlogContentProps) {
<Card className="rounded-sm border-border p-4">
<div className="flex flex-col gap-3">
{/* Kategoriler */}
<div className="flex flex-wrap items-center gap-2">
<Typography variant="small" className="mr-1 text-muted-foreground">
{t("categories")}:
</Typography>
{data.categories.map((category) => (
<Link
key={category}
href={buildHref({ category, search: data.searchQuery })}
>
<Badge
variant={category === data.selectedCategory ? "default" : "outline"}
className="cursor-pointer rounded-sm transition-colors"
{!isAgenda && (
<div className="flex flex-wrap items-center gap-2">
<Typography variant="small" className="mr-1 text-muted-foreground">
{t("categories")}:
</Typography>
{data.categories.map((category) => (
<Link
key={category}
href={buildHref({ category, search: data.searchQuery }, section)}
>
{category}
</Badge>
</Link>
))}
</div>
<Badge
variant={category === data.selectedCategory ? "default" : "outline"}
className="cursor-pointer rounded-sm transition-colors"
>
{category}
</Badge>
</Link>
))}
</div>
)}
{/* Arama */}
<div className="relative">
@@ -86,14 +95,15 @@ export function BlogContent({ data }: BlogContentProps) {
className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-muted-foreground"
/>
<input
id="blog-search"
id={isAgenda ? "agenda-search" : "blog-search"}
aria-label={isAgenda ? agendaT("searchPlaceholder") : t("searchPlaceholder")}
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") submitSearch(searchInput);
}}
placeholder={t("searchPlaceholder")}
placeholder={isAgenda ? agendaT("searchPlaceholder") : t("searchPlaceholder")}
className="w-full rounded-sm border border-border bg-background py-2 pr-10 pl-9 text-sm text-foreground placeholder:text-muted-foreground focus:border-red-600 focus:ring-1 focus:ring-red-600/30 focus:outline-none"
/>
{searchInput && (
@@ -146,7 +156,7 @@ export function BlogContent({ data }: BlogContentProps) {
readTime={post.readTime}
href={post.href}
className="rounded-sm border-border [&_h3]:line-clamp-2 [&_h3]:min-h-[2.5rem] [&_p]:line-clamp-3"
author={{ name: post.author, avatar: "/logo/logo.png" }}
author={{ name: post.author, avatar: "/logo/logo-96.webp" }}
/>
</StaggerItem>
))}
@@ -161,9 +171,13 @@ export function BlogContent({ data }: BlogContentProps) {
/>
<Typography variant="p" className="text-muted-foreground">
{data.searchQuery
? t("noSearch", { search: data.searchQuery })
? isAgenda
? agendaT("noSearch", { search: data.searchQuery })
: t("noSearch", { search: data.searchQuery })
: data.selectedCategory === "All"
? t("empty")
? isAgenda
? agendaT("empty")
: t("empty")
: t("emptyCategory", { category: data.selectedCategory })}
</Typography>
{(data.searchQuery || data.selectedCategory !== "All") && (
@@ -185,11 +199,14 @@ export function BlogContent({ data }: BlogContentProps) {
<PaginationContent className="justify-start">
<PaginationItem>
<PaginationPrevious
href={buildHref({
page: Math.max(1, data.currentPage - 1),
category: data.selectedCategory,
search: data.searchQuery,
})}
href={buildHref(
{
page: Math.max(1, data.currentPage - 1),
category: data.selectedCategory,
search: data.searchQuery,
},
section,
)}
aria-disabled={data.currentPage <= 1}
/>
</PaginationItem>
@@ -197,11 +214,14 @@ export function BlogContent({ data }: BlogContentProps) {
{pageNumbers.map((page) => (
<PaginationItem key={page}>
<PaginationLink
href={buildHref({
page,
category: data.selectedCategory,
search: data.searchQuery,
})}
href={buildHref(
{
page,
category: data.selectedCategory,
search: data.searchQuery,
},
section,
)}
isActive={page === data.currentPage}
>
{page}
@@ -211,11 +231,14 @@ export function BlogContent({ data }: BlogContentProps) {
<PaginationItem>
<PaginationNext
href={buildHref({
page: Math.min(data.totalPages, data.currentPage + 1),
category: data.selectedCategory,
search: data.searchQuery,
})}
href={buildHref(
{
page: Math.min(data.totalPages, data.currentPage + 1),
category: data.selectedCategory,
search: data.searchQuery,
},
section,
)}
aria-disabled={data.currentPage >= data.totalPages}
/>
</PaginationItem>
+10 -9
View File
@@ -7,7 +7,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useId, useRef, useState } from "react";
import { Icon } from "@iconify/react";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
import type { BlogDetail } from "@/data/blog-detail";
@@ -16,6 +16,7 @@ import { GiscusComments } from "@/components/giscus-comments";
type BlogDetailContentProps = {
post: BlogDetail;
section?: "blog" | "agenda";
};
function isHttpUrl(value: string) {
@@ -44,15 +45,13 @@ function MarkdownImage({ src, alt }: { src?: string; alt?: string }) {
className="h-auto w-full object-cover"
/>
) : (
<img
<Image
src={src}
alt={caption}
width={1200}
height={675}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 900px"
loading="lazy"
decoding="async"
fetchPriority="low"
referrerPolicy="strict-origin-when-cross-origin"
className="h-auto w-full object-cover"
/>
)}
@@ -98,7 +97,8 @@ function MermaidBlock({ chart }: { chart: string }) {
const t = useTranslations("Blog");
const [svg, setSvg] = useState("");
const [error, setError] = useState("");
const idRef = useRef(`mermaid-${Math.random().toString(36).slice(2)}`);
const generatedId = useId();
const idRef = useRef(`mermaid-${generatedId.replace(/:/g, "")}`);
useEffect(() => {
let mounted = true;
@@ -163,8 +163,9 @@ const GISCUS_CATEGORY =
process.env.NEXT_PUBLIC_GISCUS_CATEGORY || "Announcements";
const GISCUS_CATEGORY_ID = process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID || "";
export function BlogDetailContent({ post }: BlogDetailContentProps) {
export function BlogDetailContent({ post, section = "blog" }: BlogDetailContentProps) {
const t = useTranslations("Blog");
const agendaT = useTranslations("Agenda");
const progressBarRef = useRef<HTMLDivElement | null>(null);
const [tocOpen, setTocOpen] = useState(false);
@@ -199,10 +200,10 @@ export function BlogDetailContent({ post }: BlogDetailContentProps) {
<div className="min-w-0 flex-1">
<article className="space-y-5 rounded-sm border border-border p-5 md:p-8">
<Link
href="/blog"
href={section === "agenda" ? "/agenda" : "/blog"}
className="inline-flex items-center rounded-sm border border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{t("backToBlog")}
{section === "agenda" ? agendaT("backToAgenda") : t("backToBlog")}
</Link>
<header className="space-y-3">
+140 -144
View File
@@ -1,107 +1,72 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useState } from "react";
import Image from "next/image";
import { Icon } from "@iconify/react";
import { Button, ButtonIcon, ButtonLabel, Card, Typography } from "poyraz-ui/atoms";
import { Modal, ModalContent, ModalTitle } from "poyraz-ui/molecules";
import { YoutubeLiteEmbed } from "@/components/youtube-lite-embed";
import { useTranslations } from "next-intl";
import { YoutubeLiteEmbed } from "@/components/youtube-lite-embed";
import type { XVideo } from "@/data/x-videos";
import { X_JAVASCRIPT_ANATOMY_URL } from "@/data/x-videos";
import type { PdfNote } from "@/lib/content-page";
type ContentContentProps = {
youtubeLinks: readonly string[];
pdfFiles: string[];
pdfFiles: PdfNote[];
xVideos: readonly XVideo[];
};
function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
const t = useTranslations("Content");
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
let cancelled = false;
let loadingTask: {
promise: Promise<unknown>;
destroy?: () => void;
} | null = null;
const render = async () => {
try {
const pdfjs = await import("pdfjs-dist");
const lib = pdfjs as unknown as {
version: string;
getDocument: (src: string) => {
promise: Promise<unknown>;
destroy?: () => void;
};
GlobalWorkerOptions: { workerSrc: string };
};
lib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${lib.version}/build/pdf.worker.min.mjs`;
loadingTask = lib.getDocument(src);
const pdf = (await loadingTask.promise) as {
getPage: (page: number) => Promise<{
getViewport: (opts: { scale: number }) => {
width: number;
height: number;
};
render: (opts: {
canvasContext: CanvasRenderingContext2D;
viewport: { width: number; height: number };
}) => { promise: Promise<void> };
}>;
};
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 });
const canvas = canvasRef.current;
if (!canvas || cancelled) return;
const context = canvas.getContext("2d");
if (!context) return;
const ratio = window.devicePixelRatio || 1;
canvas.width = Math.floor(viewport.width * ratio);
canvas.height = Math.floor(viewport.height * ratio);
canvas.style.width = "100%";
canvas.style.height = "auto";
context.setTransform(ratio, 0, 0, ratio, 0, 0);
await page.render({ canvasContext: context, viewport }).promise;
} catch {
if (!cancelled) {
setFailed(true);
}
}
};
void render();
return () => {
cancelled = true;
if (loadingTask?.destroy) {
loadingTask.destroy();
}
};
}, [src]);
if (failed) {
return (
<div className="flex h-full w-full items-center justify-center bg-muted/20 p-2">
<Typography variant="small" className="text-muted-foreground">
{t("pdfPreviewError")}
</Typography>
</div>
);
}
type SectionHeadingProps = {
title: string;
titlePrefix?: string;
titleIcon: string;
titleIconClassName?: string;
href: string;
label: string;
handle: string;
icon: string;
};
function SectionHeading({
title,
titlePrefix,
titleIcon,
titleIconClassName,
href,
label,
handle,
icon,
}: SectionHeadingProps) {
return (
<div className="w-full bg-white p-2">
<canvas
ref={canvasRef}
aria-label={title}
className="block h-auto w-full"
/>
<div className="flex items-center justify-between gap-3">
<Typography
variant="large"
className="inline-flex items-center gap-1.5 text-base"
>
{titlePrefix ? <span>{titlePrefix}</span> : null}
<Icon
icon={titleIcon}
width={18}
height={18}
aria-hidden="true"
className={titleIconClassName}
/>
<span>{title}</span>
</Typography>
<Button asChild variant="outline" effect="swap" size="xs" radius="sm">
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={label}
>
<ButtonIcon>
<Icon icon={icon} width={15} height={15} />
</ButtonIcon>
<ButtonLabel>{handle}</ButtonLabel>
</a>
</Button>
</div>
);
}
@@ -109,6 +74,7 @@ function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
export function ContentContent({
youtubeLinks,
pdfFiles,
xVideos,
}: ContentContentProps) {
const t = useTranslations("Content");
const [pdfModalOpen, setPdfModalOpen] = useState(false);
@@ -117,7 +83,6 @@ export function ContentContent({
const activePdf = pdfFiles[activePdfIndex] ?? null;
const canGoPrev = activePdfIndex > 0;
const canGoNext = activePdfIndex < pdfFiles.length - 1;
const embeddedVideos = useMemo(
() => youtubeLinks.slice(0, 3),
[youtubeLinks],
@@ -129,25 +94,19 @@ export function ContentContent({
};
return (
<section className="flex h-full flex-col gap-3 overflow-y-auto">
<section className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Typography variant="large" className="text-base">
{t("youtubeTitle")}
</Typography>
<Button asChild variant="outline" effect="swap" size="xs" radius="sm">
<a
href="https://youtube.com/@poyrazavsever"
target="_blank"
rel="noopener noreferrer"
aria-label={t("youtubeChannel")}
>
<ButtonIcon>
<Icon icon="mdi:youtube" width={15} height={15} />
</ButtonIcon>
<ButtonLabel>@poyrazavsever</ButtonLabel>
</a>
</Button>
<section className="flex h-full flex-col gap-4 overflow-y-auto">
<section className="space-y-2" aria-labelledby="youtube-section-title">
<div id="youtube-section-title">
<SectionHeading
title={t("youtubeTitle")}
titlePrefix={t("youtubeTitlePrefix")}
titleIcon="mdi:youtube"
titleIconClassName="text-red-600"
href="https://youtube.com/@poyrazavsever"
label={t("youtubeChannel")}
handle="@poyrazavsever"
icon="mdi:youtube"
/>
</div>
<div className="grid gap-2 md:grid-cols-3">
{embeddedVideos.map((link) => (
@@ -161,50 +120,86 @@ export function ContentContent({
</div>
</section>
<section className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Typography variant="large" className="text-base">
{t("pdfTitle")}
</Typography>
<Button asChild variant="outline" effect="swap" size="xs" radius="sm">
<a
href="https://www.linkedin.com/in/poyrazavsever/"
target="_blank"
rel="noopener noreferrer"
aria-label={t("linkedinProfile")}
>
<ButtonIcon>
<Icon icon="mdi:linkedin" width={15} height={15} />
</ButtonIcon>
<ButtonLabel>@poyrazavsever</ButtonLabel>
</a>
</Button>
<section className="space-y-2" aria-labelledby="linkedin-section-title">
<div id="linkedin-section-title">
<SectionHeading
title={t("pdfTitle")}
titleIcon="mdi:linkedin"
titleIconClassName="text-[#0a66c2]"
href="https://www.linkedin.com/in/poyrazavsever/"
label={t("linkedinProfile")}
handle="@poyrazavsever"
icon="mdi:linkedin"
/>
</div>
<div className="grid grid-cols-2 gap-2 lg:grid-cols-3">
{pdfFiles.map((pdf, index) => (
<button
key={pdf}
key={pdf.fileName}
type="button"
onClick={() => openPdfModal(index)}
className="cursor-pointer text-left"
className="cursor-pointer text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label={t("openPdf", { name: pdf.title })}
>
<Card className="overflow-hidden rounded-sm border-border p-0 transition-colors hover:border-zinc-700">
<PdfFirstPagePreview
src={`/pdf/${pdf}`}
title={t("pdfPreviewTitle", { name: pdf })}
/>
<Card className="relative aspect-4/5 overflow-hidden rounded-sm border-border bg-muted/20 p-0 transition-colors hover:border-zinc-700">
{pdf.thumbnailSrc ? (
<Image
src={pdf.thumbnailSrc}
alt={t("pdfPreviewTitle", { name: pdf.title })}
fill
className="object-cover"
sizes="(max-width: 768px) 50vw, 33vw"
/>
) : (
<span className="flex h-full w-full flex-col items-center justify-center gap-2 p-4 text-center text-muted-foreground">
<Icon icon="mdi:file-pdf-box" width={34} height={34} />
<Typography variant="small">{pdf.title}</Typography>
</span>
)}
</Card>
</button>
))}
</div>
</section>
<section className="space-y-2" aria-labelledby="x-section-title">
<div id="x-section-title">
<SectionHeading
title={t("xTitle")}
titleIcon="ri:twitter-x-fill"
href={X_JAVASCRIPT_ANATOMY_URL}
label={t("xSeries")}
handle="@poyrazavsever"
icon="ri:twitter-x-fill"
/>
</div>
<div className="grid gap-2 md:grid-cols-2">
{xVideos.map((video) => (
<Card
key={video.src}
className="relative aspect-video overflow-hidden rounded-sm border-border bg-black p-0"
>
<video
controls
playsInline
preload="metadata"
className="absolute inset-0 h-full w-full object-cover"
aria-label={t("xVideoTitle", { episode: video.episode })}
>
<source src={video.src} type="video/mp4" />
{t("videoUnsupported")}
</video>
</Card>
))}
</div>
</section>
<Modal open={pdfModalOpen} onOpenChange={setPdfModalOpen}>
<ModalContent size="xl" className="rounded-sm p-4">
<ModalTitle>
{activePdf ? activePdf.replace(/\.pdf$/i, "") : t("pdfModalDefaultTitle")}
{activePdf?.title ?? t("pdfModalDefaultTitle")}
</ModalTitle>
<div className="mt-3 flex flex-wrap items-center justify-between gap-2">
<Typography variant="small" className="text-muted-foreground">
{pdfFiles.length === 0
@@ -218,7 +213,7 @@ export function ContentContent({
className="rounded-sm"
disabled={!canGoPrev}
onClick={() =>
setActivePdfIndex((prev) => Math.max(0, prev - 1))
setActivePdfIndex((previous) => Math.max(0, previous - 1))
}
>
{t("prev")}
@@ -229,8 +224,8 @@ export function ContentContent({
className="rounded-sm"
disabled={!canGoNext}
onClick={() =>
setActivePdfIndex((prev) =>
Math.min(pdfFiles.length - 1, prev + 1),
setActivePdfIndex((previous) =>
Math.min(pdfFiles.length - 1, previous + 1),
)
}
>
@@ -238,13 +233,14 @@ export function ContentContent({
</Button>
</div>
</div>
{activePdf ? (
<div className="mt-3 h-[70dvh] overflow-hidden rounded-sm border border-border">
<iframe
src={`/pdf/${activePdf}`}
title={activePdf}
src={activePdf.href}
title={activePdf.title}
className="h-full w-full"
loading="lazy"
/>
</div>
) : (
+230
View File
@@ -0,0 +1,230 @@
"use client";
import { useMemo, useState } from "react";
import type { GithubContributionDay } from "@/lib/project-feeds";
const CELL_SIZE = 10;
const CELL_GAP = 2;
const CELL_STEP = CELL_SIZE + CELL_GAP;
const GRID_LEFT = 27;
const GRID_TOP = 20;
const GRAPH_HEIGHT = 104;
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const LEVEL_CLASSES: Record<GithubContributionDay["level"], string> = {
0: "github-contribution-empty",
1: "fill-[#ff7373]",
2: "fill-[#ff5959]",
3: "fill-[#dc2626]",
4: "fill-[#b01e1e]",
};
type ContributionGraphLabels = {
calendar: string;
unavailable: string;
none: string;
singular: string;
plural: string;
};
type PositionedDay = GithubContributionDay & {
dayIndex: number;
weekIndex: number;
x: number;
y: number;
};
type ActiveDay = PositionedDay & {
label: string;
};
function parseDate(date: string) {
return new Date(`${date}T00:00:00Z`);
}
export function GithubContributionGraph({
days,
labels,
locale,
}: {
days: GithubContributionDay[];
labels: ContributionGraphLabels;
locale: string;
}) {
const [activeDay, setActiveDay] = useState<ActiveDay | null>(null);
const graph = useMemo(() => {
if (days.length === 0) return null;
const firstDate = parseDate(days[0].date);
const positionedDays: PositionedDay[] = days.map((day) => {
const date = parseDate(day.date);
const weekIndex = Math.floor(
(date.getTime() - firstDate.getTime()) / (7 * DAY_IN_MS),
);
const dayIndex = date.getUTCDay();
return {
...day,
dayIndex,
weekIndex,
x: GRID_LEFT + weekIndex * CELL_STEP,
y: GRID_TOP + dayIndex * CELL_STEP,
};
});
const weekCount =
Math.max(...positionedDays.map((day) => day.weekIndex)) + 1;
const width = GRID_LEFT + weekCount * CELL_STEP + CELL_STEP;
const monthFormatter = new Intl.DateTimeFormat(
locale === "tr" ? "tr-TR" : "en-US",
{ month: "short", timeZone: "UTC" },
);
const dateFormatter = new Intl.DateTimeFormat(
locale === "tr" ? "tr-TR" : "en-US",
{ day: "numeric", month: "long", year: "numeric", timeZone: "UTC" },
);
const months: Array<{ label: string; x: number }> = [];
let previousMonth = firstDate.getUTCMonth();
positionedDays.forEach((day, index) => {
const date = parseDate(day.date);
const month = date.getUTCMonth();
if (index > 0 && month !== previousMonth) {
months.push({
label: monthFormatter.format(date).replace(".", ""),
x: day.x,
});
}
previousMonth = month;
});
return { positionedDays, months, width, dateFormatter };
}, [days, locale]);
if (!graph) {
return (
<div className="flex min-h-28 items-center justify-center text-sm text-muted-foreground">
{labels.unavailable}
</div>
);
}
const weekdayLabels =
locale === "tr"
? [
{ index: 1, label: "Pzt" },
{ index: 3, label: "Çar" },
{ index: 5, label: "Cum" },
]
: [
{ index: 1, label: "Mon" },
{ index: 3, label: "Wed" },
{ index: 5, label: "Fri" },
];
function getTooltipLabel(day: GithubContributionDay) {
const date = graph?.dateFormatter.format(parseDate(day.date)) ?? day.date;
if (day.count === 0) return `${date}: ${labels.none}`;
return `${date}: ${day.count} ${
day.count === 1 ? labels.singular : labels.plural
}`;
}
const tooltipX = activeDay
? Math.max(100, Math.min(graph.width - 100, activeDay.x + CELL_SIZE / 2))
: 0;
const tooltipBelow = Boolean(activeDay && activeDay.y < 48);
return (
<div className="overflow-x-auto rounded-sm">
<div
className="relative min-w-[740px]"
style={{ aspectRatio: `${graph.width} / ${GRAPH_HEIGHT}` }}
>
<svg
viewBox={`0 0 ${graph.width} ${GRAPH_HEIGHT}`}
aria-label={labels.calendar}
role="grid"
className="absolute inset-0 h-full w-full"
onMouseLeave={() => setActiveDay(null)}
>
{weekdayLabels.map((day) => (
<text
key={day.index}
x="0"
y={GRID_TOP + day.index * CELL_STEP + 8}
className="fill-muted-foreground text-[9px]"
>
{day.label}
</text>
))}
{graph.months.map((month) => (
<text
key={`${month.label}-${month.x}`}
x={month.x}
y="10"
className="fill-muted-foreground text-[10px]"
>
{month.label}
</text>
))}
{Array.from({ length: 7 }, (_, dayIndex) => (
<g key={dayIndex} role="row">
{graph.positionedDays
.filter((day) => day.dayIndex === dayIndex)
.map((day) => {
const label = getTooltipLabel(day);
return (
<rect
key={day.date}
x={day.x}
y={day.y}
width={CELL_SIZE}
height={CELL_SIZE}
rx="1"
role="gridcell"
tabIndex={0}
aria-label={label}
className={`${LEVEL_CLASSES[day.level]} cursor-default outline-none transition-opacity hover:opacity-75 focus-visible:stroke-foreground focus-visible:stroke-2`}
onMouseEnter={() =>
setActiveDay({ ...day, label })
}
onFocus={() => setActiveDay({ ...day, label })}
onBlur={() => setActiveDay(null)}
/>
);
})}
</g>
))}
</svg>
{activeDay ? (
<div
role="tooltip"
className="pointer-events-none absolute z-10 max-w-56 -translate-x-1/2 rounded-sm border border-border bg-popover px-2 py-1 text-center text-xs text-popover-foreground shadow-md"
style={{
left: `${(tooltipX / graph.width) * 100}%`,
top: `${
((activeDay.y + (tooltipBelow ? CELL_SIZE + 3 : -3)) /
GRAPH_HEIGHT) *
100
}%`,
transform: tooltipBelow
? "translateX(-50%)"
: "translate(-50%, -100%)",
}}
>
{activeDay.label}
</div>
) : null}
</div>
</div>
);
}
+2 -3
View File
@@ -1,10 +1,9 @@
import Script from "next/script";
const GA_ID = process.env.NEXT_PUBLIC_GA_ID;
const DEFAULT_GA_ID = "G-TJBKHZLR7J";
const GA_ID = process.env.NEXT_PUBLIC_GA_ID?.trim() || DEFAULT_GA_ID;
export function GoogleAnalytics() {
if (!GA_ID) return null;
return (
<>
<Script
+9 -5
View File
@@ -1,5 +1,6 @@
"use client";
import type { ReactNode } from "react";
import { Icon } from "@iconify/react";
import { useLocale, useTranslations } from "next-intl";
import { useRouter } from "@/i18n/routing";
@@ -10,10 +11,11 @@ import {
TextEffect,
Typography,
} from "poyraz-ui/atoms";
import { NewsCard } from "poyraz-ui/molecules";
import { getResumeHref } from "@/lib/links";
import { HomeNewsCard } from "@/components/home-news-card";
type HomeHeroProps = {
children?: ReactNode;
news: {
id: string;
category: string;
@@ -24,7 +26,7 @@ type HomeHeroProps = {
}[];
};
export function HomeHero({ news }: HomeHeroProps) {
export function HomeHero({ children, news }: HomeHeroProps) {
const t = useTranslations("Home");
const locale = useLocale();
const router = useRouter();
@@ -89,6 +91,8 @@ export function HomeHero({ news }: HomeHeroProps) {
</div>
{children}
<section className="space-y-3 pt-12 md:pt-14">
<div className="flex items-center justify-between gap-4">
<Typography
@@ -116,15 +120,15 @@ export function HomeHero({ news }: HomeHeroProps) {
<div className="relative overflow-hidden py-1">
<div className="flex w-max items-stretch gap-3">
{news.map((item) => (
<NewsCard
{news.map((item, index) => (
<HomeNewsCard
key={item.id}
className="w-72 shrink-0 rounded-sm border-border"
category={item.category}
title={item.title}
date={item.date}
image={item.image}
href={item.href}
priority={index === 0}
/>
))}
</div>
+52
View File
@@ -0,0 +1,52 @@
import Image from "next/image";
import { Badge, Card } from "poyraz-ui/atoms";
import { Link } from "@/i18n/routing";
type HomeNewsCardProps = {
category: string;
title: string;
date: string;
image: string;
href: string;
priority?: boolean;
};
export function HomeNewsCard({
category,
title,
date,
image,
href,
priority = false,
}: HomeNewsCardProps) {
return (
<Link href={href} className="block w-72 shrink-0 text-inherit no-underline">
<Card
variant="interactive"
className="group h-fit self-start overflow-hidden rounded-sm border-border"
>
<div className="flex min-h-28">
<div className="relative w-28 shrink-0 overflow-hidden border-r border-border">
<Image
src={image}
alt=""
fill
sizes="112px"
preload={priority}
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
<div className="flex min-w-0 flex-col justify-center gap-2 p-4">
<Badge size="sm" variant="outline">
{category}
</Badge>
<h3 className="line-clamp-2 text-sm font-semibold leading-snug">
{title}
</h3>
<span className="text-xs text-muted-foreground">{date}</span>
</div>
</div>
</Card>
</Link>
);
}
+12 -5
View File
@@ -3,13 +3,14 @@
import { Icon } from "@iconify/react";
import { useLocale, useTranslations } from "next-intl";
import { Button, ButtonIcon, ButtonLabel, Typography } from "poyraz-ui/atoms";
import { ImageCard } from "poyraz-ui/molecules";
import { useRouter } from "@/i18n/routing";
import { WEB_APPS } from "@/data/projects";
import { getLocalizedValue } from "@/lib/locale";
import { ProjectCardWithPopover } from "@/components/project-card-with-popover";
export function HomeProjectsSection() {
const t = useTranslations("Home");
const tProjects = useTranslations("Projects");
const locale = useLocale();
const router = useRouter();
const projects = WEB_APPS.slice(0, 5);
@@ -42,11 +43,11 @@ export function HomeProjectsSection() {
<div className="relative overflow-hidden py-1">
<div className="flex w-max items-stretch gap-2">
{projects.map((project) => (
<ImageCard
{projects.map((project, index) => (
<ProjectCardWithPopover
key={project.id}
image={project.image}
title={project.title}
title={getLocalizedValue(project.title, locale)}
description={getLocalizedValue(project.description, locale)}
badge={
project.badge
@@ -54,7 +55,13 @@ export function HomeProjectsSection() {
: undefined
}
href={project.href}
className="aspect-square w-56 shrink-0 rounded-sm border-border md:w-[calc((100vw-8rem)/4)] md:max-w-56"
technologies={project.technologies}
architecture={getLocalizedValue(project.architecture, locale)}
technologiesLabel={tProjects("technologies")}
architectureLabel={tProjects("architecture")}
triggerClassName="w-56 shrink-0 md:w-[calc((100vw-8rem)/4)] md:max-w-56"
className="aspect-square rounded-sm border-border"
priority={index === 0}
/>
))}
</div>
+63
View File
@@ -0,0 +1,63 @@
"use client";
import { Icon } from "@iconify/react";
import { useTranslations } from "next-intl";
import {
Button,
ButtonIcon,
ButtonLabel,
Typography,
} from "poyraz-ui/atoms";
import { TECHNOLOGY_STACK } from "@/data/technology-stack";
import { Link } from "@/i18n/routing";
import { TechnologyBadge } from "@/components/technology-badge";
export function HomeTechnologyStack() {
const t = useTranslations("Home");
const technologies = TECHNOLOGY_STACK.flatMap((group) => group.items);
return (
<section
className="space-y-4 pt-12 md:pt-14"
aria-labelledby="home-technology-stack-title"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<Typography
id="home-technology-stack-title"
variant="h3"
component="h2"
className="tracking-[-0.035em]"
>
{t("technologiesTitle")}
</Typography>
<Button
asChild
size="sm"
radius="sm"
effect="swap"
swapTarget="both"
>
<Link href="/technologies">
<ButtonLabel>{t("allTechnologies")}</ButtonLabel>
<ButtonIcon>
<Icon icon="mdi:arrow-right" width={15} height={15} />
</ButtonIcon>
</Link>
</Button>
</div>
<div className="relative max-h-14 overflow-hidden border-t border-border pt-3">
<div className="flex flex-wrap gap-2" aria-label={t("technologiesTitle")}>
{technologies.map((technology) => (
<TechnologyBadge key={technology.id} technology={technology} />
))}
</div>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background via-background/85 to-transparent backdrop-blur-[2px]"
/>
</div>
</section>
);
}
+173 -8
View File
@@ -1,4 +1,8 @@
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
import { SITE_URL } from "@/lib/seo";
function serializeJsonLd(data: object) {
return JSON.stringify(data).replace(/</g, "\\u003c");
}
export type PersonJsonLdProps = {
name: string;
@@ -11,7 +15,7 @@ export type PersonJsonLdProps = {
export function PersonJsonLd({
name,
url = SITE_URL,
image = `${SITE_URL}/logo/logo.png`,
image = `${SITE_URL}/logo/logo.webp`,
jobTitle = "Fullstack Developer",
sameAs = [],
}: PersonJsonLdProps) {
@@ -28,7 +32,165 @@ export function PersonJsonLd({
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}
export type ProfilePageJsonLdProps = PersonJsonLdProps & {
pageUrl: string;
pageName: string;
description: string;
locale: "tr" | "en";
};
export function ProfilePageJsonLd({
pageUrl,
pageName,
description,
locale,
name,
url = SITE_URL,
image = `${SITE_URL}/logo/logo.webp`,
jobTitle = "Fullstack Developer",
sameAs = [],
}: ProfilePageJsonLdProps) {
const data = {
"@context": "https://schema.org",
"@type": "ProfilePage",
"@id": `${pageUrl}#profile-page`,
url: pageUrl,
name: pageName,
description,
inLanguage: locale === "tr" ? "tr-TR" : "en-US",
mainEntity: {
"@type": "Person",
"@id": `${SITE_URL}/#person`,
name,
url,
image,
jobTitle,
sameAs,
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}
export type ProjectsJsonLdItem = {
name: string;
description: string;
image: string;
url?: string;
technologies: string[];
};
export function ProjectsJsonLd({
name,
description,
url,
locale,
projects,
}: {
name: string;
description: string;
url: string;
locale: "tr" | "en";
projects: ProjectsJsonLdItem[];
}) {
const data = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"@id": `${url}#projects`,
url,
name,
description,
inLanguage: locale === "tr" ? "tr-TR" : "en-US",
mainEntity: {
"@type": "ItemList",
numberOfItems: projects.length,
itemListElement: projects.map((project, index) => ({
"@type": "ListItem",
position: index + 1,
item: {
"@type": "CreativeWork",
name: project.name,
description: project.description,
image: project.image.startsWith("http")
? project.image
: `${SITE_URL}${project.image}`,
url: project.url,
keywords: project.technologies,
creator: {
"@type": "Person",
"@id": `${SITE_URL}/#person`,
name: "Poyraz Avsever",
},
},
})),
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}
export function ProjectCaseStudyJsonLd({
name,
description,
url,
liveUrl,
image,
locale,
applicationCategory,
technologies,
features,
}: {
name: string;
description: string;
url: string;
liveUrl: string;
image: string;
locale: "tr" | "en";
applicationCategory: string;
technologies: string[];
features: string[];
}) {
const data = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"@id": `${url}#software-application`,
name,
description,
url,
sameAs: liveUrl,
image: image.startsWith("http") ? image : `${SITE_URL}${image}`,
inLanguage: locale === "tr" ? "tr-TR" : "en-US",
applicationCategory,
operatingSystem: "Web",
keywords: technologies,
featureList: features,
author: {
"@type": "Person",
"@id": `${SITE_URL}/#person`,
name: "Poyraz Avsever",
url: SITE_URL,
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}
@@ -40,6 +202,7 @@ export type ArticleJsonLdProps = {
image: string;
datePublished: string;
authorName?: string;
locale?: "tr" | "en";
};
export function ArticleJsonLd({
@@ -49,6 +212,7 @@ export function ArticleJsonLd({
image,
datePublished,
authorName = "Poyraz Avsever",
locale = "tr",
}: ArticleJsonLdProps) {
const data = {
"@context": "https://schema.org",
@@ -56,8 +220,11 @@ export function ArticleJsonLd({
headline: title,
description,
url,
mainEntityOfPage: url,
image: image.startsWith("http") ? image : `${SITE_URL}${image}`,
datePublished,
dateModified: datePublished,
inLanguage: locale === "tr" ? "tr-TR" : "en-US",
author: {
"@type": "Person",
name: authorName,
@@ -66,17 +233,15 @@ export function ArticleJsonLd({
publisher: {
"@type": "Person",
name: authorName,
logo: {
"@type": "ImageObject",
url: `${SITE_URL}/logo/logo.png`,
},
url: SITE_URL,
image: `${SITE_URL}/logo/logo.webp`,
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}
+347
View File
@@ -0,0 +1,347 @@
"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="group"
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"
aria-pressed={selected}
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>
<span className="sr-only" aria-live="polite">
{t("slideStatus", {
current: activeSlide + 1,
total: slides.length,
})}
</span>
<div className="overflow-hidden">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={`${railId}-slide-${activeSlide}`}
id={`${railId}-panel-${activeSlide}`}
role="group"
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} />;
}
+256 -226
View File
@@ -1,18 +1,26 @@
"use client";
import { Icon } from "@iconify/react";
import { Link } from "@/i18n/routing";
import { useLocale, useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { Icon } from "@iconify/react";
import { useLocale, useTranslations } from "next-intl";
import {
Avatar,
AvatarFallback,
AvatarImage,
Badge,
Button,
Card,
Input,
Typography,
} 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 {
getResumeHref,
LINK_DIRECTORY,
@@ -36,12 +44,12 @@ const CATEGORY_ORDER = {
} 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;
}
const categoryCompare =
CATEGORY_ORDER[left.category] - CATEGORY_ORDER[right.category];
return left.label.localeCompare(right.label, "tr");
return categoryCompare !== 0
? categoryCompare
: left.label.localeCompare(right.label, "tr");
});
function normalize(value: string) {
@@ -52,13 +60,8 @@ function normalize(value: string) {
}
function formatHref(href: string) {
if (href.startsWith("mailto:")) {
return href.replace("mailto:", "");
}
if (href.startsWith("/")) {
return `poyrazavsever.com${href}`;
}
if (href.startsWith("mailto:")) return href.replace("mailto:", "");
if (href.startsWith("/")) return `poyrazavsever.com${href}`;
return href.replace(/^https?:\/\//, "").replace(/\/$/, "");
}
@@ -71,6 +74,10 @@ function parseCategoryFilter(value?: string): CategoryFilter {
return "all";
}
function LinkIcon({ icon, size = 18 }: { icon: string; size?: number }) {
return <Icon icon={icon} width={size} height={size} />;
}
export function LinksContent({
initialCategory,
initialQuery = "",
@@ -78,19 +85,21 @@ export function LinksContent({
const t = useTranslations("Links");
const tNav = useTranslations("Nav");
const locale = useLocale();
const [activeCategory, setActiveCategory] = useState<CategoryFilter>(() =>
parseCategoryFilter(initialCategory),
);
const [query, setQuery] = useState(initialQuery);
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 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 normalizedQuery = normalize(query.trim());
@@ -101,9 +110,7 @@ export function LinksContent({
return false;
}
if (queryTokens.length === 0) {
return true;
}
if (queryTokens.length === 0) return true;
const haystack = normalize(
[
@@ -119,208 +126,231 @@ export function LinksContent({
}, [activeCategory, query, t, tNav]);
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="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 items-start gap-4">
<Avatar className="size-16 shrink-0 rounded-sm border border-border bg-background sm:size-20">
<AvatarImage src="/logo/logo-96.webp" alt="Poyraz Avsever" />
<AvatarFallback className="rounded-sm bg-muted font-semibold">
PA
</AvatarFallback>
</Avatar>
<div className="relative mx-auto flex w-full max-w-xl justify-center">
<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="relative aspect-1878/410 overflow-hidden border-b border-border/70">
<img
src="/logo/cover.png"
alt="Poyraz Avsever kapak görseli"
/>
<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.png" alt="Poyraz Avsever" />
<AvatarFallback className="rounded-3xl bg-muted text-lg font-semibold">
PA
</AvatarFallback>
</Avatar>
<div className="space-y-3">
<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
variant="h2"
secondaryFont
className="text-[1.75rem] leading-none text-red-600 sm:text-[2rem]"
>
Avsever
</Typography>
</div>
<Typography variant="small" className="max-w-lg text-sm leading-6 text-muted-foreground">
{t("desc")}
</Typography>
</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 className="grid gap-2 sm:grid-cols-2">
{TOP_ICON_LINKS.map((item) => {
const href = item.id === "cv" ? getResumeHref(locale) : item.href;
return (
<a
key={item.id}
href={href}
target={item.id === "cv" || item.external ? "_blank" : undefined}
rel={item.id === "cv" || item.external ? "noreferrer" : undefined}
className="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">
<div className="flex items-center gap-3">
<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} />
</span>
<div className="min-w-0">
<Typography variant="large" className="truncate text-base">
{tNav.has(item.id) ? tNav(item.id) : item.label}
</Typography>
<Typography
variant="small"
className="truncate text-xs text-muted-foreground"
>
{formatHref(href)}
</Typography>
</div>
</div>
</Card>
</a>
);
})}
</div>
<div className="space-y-3 pt-1">
<div>
<Typography variant="large" className="text-base">
{t("allLinks")}
</Typography>
<Typography variant="small" className="text-muted-foreground">
{t("allLinksDesc")}
</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={t("selectCategory")} />
</SelectTrigger>
<SelectContent>
{filterItems.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={t("searchPlaceholder")}
aria-label={t("allLinks")}
className="h-11 rounded-2xl"
/>
</div>
<div className="grid gap-2">
{filteredItems.map((item) => {
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 CardContent = (
<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">
{tNav.has(item.id) ? tNav(item.id) : item.label}
</Typography>
<span className="inline-flex rounded-full border border-border px-2.5 py-0.5 text-[11px] text-muted-foreground">
{t(`categories.${item.category}`)}
</span>
</div>
<Typography
variant="small"
className="mt-1 truncate text-xs text-muted-foreground"
>
{formatHref(href)}
</Typography>
</div>
<span className="text-muted-foreground">
<Icon
icon={isStaticOrExternal ? "mdi:arrow-top-right" : "mdi:arrow-right"}
width={18}
height={18}
/>
</span>
</div>
</Card>
);
return isStaticOrExternal ? (
<a
key={`${item.category}-${item.id}`}
href={href}
target={item.id === "cv" || item.external ? "_blank" : undefined}
rel={item.id === "cv" || item.external ? "noreferrer" : undefined}
className="block"
>
{CardContent}
</a>
) : (
<Link
key={`${item.category}-${item.id}`}
href={href}
target={item.external ? "_blank" : undefined}
rel={item.external ? "noreferrer" : undefined}
className="block"
>
{CardContent}
</Link>
);
})}
{filteredItems.length === 0 ? (
<Card className="rounded-2xl border-border px-4 py-5">
<Typography variant="small" className="text-muted-foreground">
{t("empty")}
</Typography>
</Card>
) : null}
</div>
</div>
<div className="min-w-0">
<Typography
variant="h2"
component="h1"
className="font-secondary text-2xl font-semibold leading-none tracking-[-0.045em] text-foreground"
>
{t("title")}
</Typography>
<Typography
variant="small"
className="mt-2 max-w-xl text-xs leading-5 text-muted-foreground sm:text-sm"
>
{t("desc")}
</Typography>
</div>
</div>
</Card>
</div>
<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) => {
const href = item.id === "cv" ? getResumeHref(locale) : item.href;
return (
<a
key={item.id}
href={href}
target={item.id === "cv" || item.external ? "_blank" : undefined}
rel={item.id === "cv" || item.external ? "noreferrer" : undefined}
className="group block"
>
<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">
<span className="inline-flex size-9 items-center justify-center rounded-sm border border-border bg-muted/35 text-muted-foreground">
<LinkIcon icon={item.icon} />
</span>
<Typography variant="large" className="mt-3 text-sm leading-tight">
{tNav.has(item.id) ? tNav(item.id) : item.label}
</Typography>
<Typography
variant="small"
className="mt-1 truncate text-xs text-muted-foreground"
>
{formatHref(href)}
</Typography>
</Card>
</a>
);
})}
</div>
</section>
<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>
<Typography
id="all-links-title"
variant="h3"
component="h2"
className="tracking-[-0.035em]"
>
{t("allLinks")}
</Typography>
<Typography variant="small" className="mt-1 text-muted-foreground">
{t("allLinksDesc")}
</Typography>
</div>
<div className="grid w-full gap-2 sm:grid-cols-[180px_1fr] md:max-w-lg">
<Select
value={activeCategory}
onValueChange={(value) =>
setActiveCategory(value as CategoryFilter)
}
>
<SelectTrigger className="h-10 rounded-sm border-border bg-background px-3 text-sm">
<SelectValue placeholder={t("selectCategory")} />
</SelectTrigger>
<SelectContent>
{filterItems.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={t("searchPlaceholder")}
aria-label={t("searchAriaLabel")}
className="h-10 rounded-sm"
/>
</div>
</div>
<div className="grid gap-2 md:grid-cols-2">
{filteredItems.map((item) => {
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 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">
<span className="inline-flex size-10 shrink-0 items-center justify-center rounded-sm border border-border bg-muted/35 text-muted-foreground">
<LinkIcon icon={item.icon} />
</span>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<Typography variant="large" className="text-sm leading-tight">
{tNav.has(item.id) ? tNav(item.id) : item.label}
</Typography>
<Badge variant="secondary" radius="sm" className="text-[10px]">
{t(`categories.${item.category}`)}
</Badge>
</div>
<Typography
variant="small"
className="mt-1 truncate text-xs text-muted-foreground"
>
{formatHref(href)}
</Typography>
</div>
<Icon
icon={
isStaticOrExternal
? "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"
/>
</div>
</Card>
);
return isStaticOrExternal ? (
<a
key={`${item.category}-${item.id}`}
href={href}
target={item.id === "cv" || item.external ? "_blank" : undefined}
rel={item.id === "cv" || item.external ? "noreferrer" : undefined}
className="group block"
>
{content}
</a>
) : (
<Link
key={`${item.category}-${item.id}`}
href={href}
className="group block"
>
{content}
</Link>
);
})}
{filteredItems.length === 0 ? (
<Card className="rounded-sm border-border px-4 py-5 md:col-span-2">
<Typography variant="small" className="text-muted-foreground">
{t("empty")}
</Typography>
</Card>
) : null}
</div>
</section>
</section>
);
}
File diff suppressed because it is too large Load Diff
+76 -22
View File
@@ -13,6 +13,10 @@ const ALERT_TIME = 3;
const IDLE_THRESHOLD = 3;
const IDLE_ANIMATION_CHANCE = 1 / 20;
const MIN_DISTANCE = 10;
const ROAM_MARGIN = 24;
const MIN_ROAM_DISTANCE = 160;
const MIN_IDLE_FRAMES = 8;
const MAX_IDLE_FRAMES = 20;
const SPRITE_GAP = 1;
const BACKGROUND_TARGET_COLOR: [number, number, number] = [0, 174, 240];
@@ -21,8 +25,9 @@ type SpriteSet = Record<string, [number, number][]>;
class Neko {
private posX: number;
private posY: number;
private mouseX: number;
private mouseY: number;
private targetX: number;
private targetY: number;
private idleFramesRemaining: number;
private frameCount: number;
private idleTime: number;
private idleAnimation: string | null;
@@ -43,8 +48,9 @@ class Neko {
this.nekoImageUrl = nekoImageUrl;
this.posX = Math.max(NEKO_HALF_WIDTH, window.innerWidth - NEKO_HALF_WIDTH - margin);
this.posY = Math.max(NEKO_HALF_HEIGHT, window.innerHeight - NEKO_HALF_HEIGHT - margin);
this.mouseX = this.posX;
this.mouseY = this.posY;
this.targetX = this.posX;
this.targetY = this.posY;
this.idleFramesRemaining = this.randomIdleDuration();
this.frameCount = 0;
this.idleTime = 0;
this.idleAnimation = null;
@@ -186,18 +192,13 @@ class Neko {
this.render();
}
private handleMouseMove = (event: MouseEvent) => {
this.mouseX = event.clientX;
this.mouseY = event.clientY;
};
private handleResize = () => {
this.clampToViewport();
this.clampTargetToViewport();
this.render();
};
private addEventListeners() {
document.addEventListener("mousemove", this.handleMouseMove);
window.addEventListener("resize", this.handleResize);
}
@@ -222,21 +223,34 @@ class Neko {
private updateState() {
this.frameCount += 1;
this.followMouse();
}
private followMouse() {
const diffX = this.posX - this.mouseX;
const diffY = this.posY - this.mouseY;
const distance = Math.hypot(diffX, diffY);
if (distance < MIN_DISTANCE) {
if (this.idleFramesRemaining > 0 || this.idleAnimation !== null) {
this.idleBehavior();
if (this.idleAnimation === null) {
this.idleFramesRemaining -= 1;
}
if (this.idleFramesRemaining <= 0 && this.idleAnimation === null) {
this.chooseNewTarget();
}
return;
}
if (this.idleTime > IDLE_THRESHOLD && this.alertTimeRemaining === 0) {
this.alertTimeRemaining = ALERT_TIME;
this.wander();
}
private wander() {
const diffX = this.posX - this.targetX;
const diffY = this.posY - this.targetY;
const distance = Math.hypot(diffX, diffY);
if (distance < MIN_DISTANCE) {
this.posX = this.targetX;
this.posY = this.targetY;
this.idleFramesRemaining = this.randomIdleDuration();
this.idleBehavior();
return;
}
if (this.alertTimeRemaining > 0) {
@@ -263,6 +277,37 @@ class Neko {
this.clampToViewport();
}
private chooseNewTarget() {
const minX = Math.min(NEKO_HALF_WIDTH + ROAM_MARGIN, window.innerWidth / 2);
const maxX = Math.max(minX, window.innerWidth - NEKO_HALF_WIDTH - ROAM_MARGIN);
const minY = Math.min(NEKO_HALF_HEIGHT + ROAM_MARGIN, window.innerHeight / 2);
const maxY = Math.max(minY, window.innerHeight - NEKO_HALF_HEIGHT - ROAM_MARGIN);
let nextX = this.posX;
let nextY = this.posY;
for (let attempt = 0; attempt < 8; attempt += 1) {
nextX = minX + Math.random() * (maxX - minX);
nextY = minY + Math.random() * (maxY - minY);
if (Math.hypot(nextX - this.posX, nextY - this.posY) >= MIN_ROAM_DISTANCE) {
break;
}
}
this.targetX = nextX;
this.targetY = nextY;
this.alertTimeRemaining = ALERT_TIME;
this.idleTime = 0;
}
private randomIdleDuration() {
return (
MIN_IDLE_FRAMES +
Math.floor(Math.random() * (MAX_IDLE_FRAMES - MIN_IDLE_FRAMES + 1))
);
}
private idleBehavior() {
this.idleTime += 1;
@@ -320,6 +365,17 @@ class Neko {
);
}
private clampTargetToViewport() {
this.targetX = Math.min(
Math.max(NEKO_HALF_WIDTH, this.targetX),
window.innerWidth - NEKO_HALF_WIDTH,
);
this.targetY = Math.min(
Math.max(NEKO_HALF_HEIGHT, this.targetY),
window.innerHeight - NEKO_HALF_HEIGHT,
);
}
private render() {
if (!this.nekoElement) return;
this.nekoElement.style.left = `${this.posX - NEKO_HALF_WIDTH}px`;
@@ -345,7 +401,6 @@ class Neko {
this.animationFrameId = null;
}
document.removeEventListener("mousemove", this.handleMouseMove);
window.removeEventListener("resize", this.handleResize);
if (this.nekoElement) {
@@ -368,4 +423,3 @@ export function NekoFollower() {
return null;
}
+41 -13
View File
@@ -12,6 +12,7 @@ import {
const VIDEO_SRC = "/media/cursor-portrait/poyraz-bottom-right.mp4";
const POSTER_SRC = "/media/cursor-portrait/poyraz-bottom-right-poster.webp";
const NIGHT_SRC = "/media/cursor-portrait/gece.webp";
const SEEK_INTERVAL_MS = 1000 / 60;
const MIN_TIME_DELTA = 0.002;
@@ -21,8 +22,8 @@ type DisplayMode =
| "pending"
| "interactive"
| "poster-reduced"
| "hidden-mobile"
| "hidden-dark";
| "night-static"
| "hidden-mobile";
function subscribeToMediaQuery(query: MediaQueryList, listener: () => void) {
if (typeof query.addEventListener === "function") {
@@ -52,13 +53,13 @@ export function PoyrazBottomRightFollower() {
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const updateDisplayMode = () => {
if (document.documentElement.dataset.poyrazTheme === "dark") {
setDisplayMode("hidden-dark");
if (!desktopQuery.matches || !finePointerQuery.matches) {
setDisplayMode("hidden-mobile");
return;
}
if (!desktopQuery.matches || !finePointerQuery.matches) {
setDisplayMode("hidden-mobile");
if (document.documentElement.dataset.poyrazTheme === "dark") {
setDisplayMode("night-static");
return;
}
@@ -252,12 +253,9 @@ export function PoyrazBottomRightFollower() {
}, [displayMode, videoFailed]);
const showVideo = displayMode === "interactive" && !videoFailed;
const showNight = displayMode === "night-static";
if (
displayMode === "pending" ||
displayMode === "hidden-mobile" ||
displayMode === "hidden-dark"
) {
if (displayMode === "pending" || displayMode === "hidden-mobile") {
return null;
}
@@ -265,9 +263,39 @@ export function PoyrazBottomRightFollower() {
<div
aria-hidden="true"
data-cursor-portrait
className="pointer-events-none fixed right-6 bottom-0 z-40 aspect-square w-[clamp(110px,11vw,170px)] select-none bg-white"
data-portrait-mode={showNight ? "night" : "day"}
className={`pointer-events-none fixed right-6 bottom-0 z-40 aspect-square w-[clamp(110px,11vw,170px)] select-none ${showNight ? "bg-transparent" : "bg-white"}`}
>
{showVideo ? (
{showNight ? (
<>
<Image
src={NIGHT_SRC}
alt=""
fill
sizes="(max-width: 1545px) 11vw, 170px"
draggable={false}
className="object-contain"
/>
<span
data-sleepy-z="1"
className="animate-sleepy-z absolute top-[22%] left-[31%] z-10 font-secondary text-[clamp(11px,1vw,15px)] font-bold text-red-100 drop-shadow-[0_0_5px_rgba(248,113,113,0.75)]"
>
Z
</span>
<span
data-sleepy-z="2"
className="animate-sleepy-z absolute top-[13%] left-[22%] z-10 font-secondary text-[clamp(13px,1.15vw,18px)] font-bold text-red-100 drop-shadow-[0_0_6px_rgba(248,113,113,0.8)]"
>
Z
</span>
<span
data-sleepy-z="3"
className="animate-sleepy-z absolute top-[3%] left-[12%] z-10 font-secondary text-[clamp(15px,1.3vw,21px)] font-bold text-red-100 drop-shadow-[0_0_7px_rgba(248,113,113,0.85)]"
>
Z
</span>
</>
) : showVideo ? (
<video
ref={videoRef}
src={VIDEO_SRC}
+199
View File
@@ -0,0 +1,199 @@
"use client";
import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { Icon } from "@iconify/react";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "poyraz-ui/molecules";
import { Link } from "@/i18n/routing";
type ProjectCardWithPopoverProps = {
title: string;
description: string;
image: string;
badge?: string;
href?: string;
caseStudyHref?: string;
caseStudyLabel?: string;
technologies: string[];
architecture: string;
technologiesLabel: string;
architectureLabel: string;
className?: string;
triggerClassName?: string;
priority?: boolean;
};
export function ProjectCardWithPopover({
title,
description,
image,
badge,
href,
caseStudyHref,
caseStudyLabel,
technologies,
architecture,
technologiesLabel,
architectureLabel,
className,
triggerClassName,
priority = false,
}: ProjectCardWithPopoverProps) {
const [open, setOpen] = useState(false);
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const cancelClose = () => {
if (closeTimer.current) {
clearTimeout(closeTimer.current);
closeTimer.current = null;
}
};
const showPopover = () => {
cancelClose();
setOpen(true);
};
const scheduleClose = () => {
cancelClose();
closeTimer.current = setTimeout(() => setOpen(false), 120);
};
useEffect(() => {
return () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
};
}, []);
const card = (
<Card
variant="interactive"
className={`group relative aspect-[4/3] overflow-hidden ${className ?? ""}`}
>
<Image
src={image}
alt=""
fill
sizes="(max-width: 767px) 224px, 224px"
preload={priority}
fetchPriority={priority ? "high" : "auto"}
className="object-cover transition-transform duration-500 ease-[var(--poyraz-motion-ease-out)] group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-overlay via-overlay/20 to-transparent transition-opacity group-hover:opacity-90" />
{badge ? (
<Badge className="absolute left-3 top-3 z-10">{badge}</Badge>
) : null}
<div className="absolute inset-x-0 bottom-0 z-10 p-4 text-primary-foreground">
<h3 className="font-semibold leading-tight">{title}</h3>
{description ? (
<p className="mt-1 line-clamp-2 text-xs opacity-80">{description}</p>
) : null}
</div>
</Card>
);
const triggerClasses = `block ${triggerClassName ?? ""} text-inherit outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{caseStudyHref ? (
<Link
href={caseStudyHref}
className={`${triggerClasses} no-underline`}
onPointerEnter={showPopover}
onPointerLeave={scheduleClose}
onFocus={showPopover}
onBlur={scheduleClose}
>
{card}
</Link>
) : href ? (
<a
href={href}
className={`${triggerClasses} no-underline`}
onPointerEnter={showPopover}
onPointerLeave={scheduleClose}
onFocus={showPopover}
onBlur={scheduleClose}
>
{card}
</a>
) : (
<button
type="button"
className={`${triggerClasses} border-0 bg-transparent p-0 text-left`}
onPointerEnter={showPopover}
onPointerLeave={scheduleClose}
onFocus={showPopover}
onBlur={scheduleClose}
>
{card}
</button>
)}
</PopoverTrigger>
<PopoverContent
side="top"
align="center"
sideOffset={10}
radius="sm"
padding="sm"
className="w-80 max-w-[calc(100vw-1rem)]"
onPointerEnter={showPopover}
onPointerLeave={scheduleClose}
onOpenAutoFocus={(event) => event.preventDefault()}
onCloseAutoFocus={(event) => event.preventDefault()}
>
<div className="space-y-3">
<Typography variant="large" className="text-sm leading-tight">
{title}
</Typography>
<div className="space-y-1.5">
<Typography
variant="small"
className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
>
{technologiesLabel}
</Typography>
<div className="flex flex-wrap gap-1.5">
{technologies.map((technology) => (
<Badge key={technology} size="sm" variant="outline" className="rounded-sm">
{technology}
</Badge>
))}
</div>
</div>
<div className="space-y-1">
<Typography
variant="small"
className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
>
{architectureLabel}
</Typography>
<Typography variant="small" className="text-xs leading-relaxed text-muted-foreground">
{architecture}
</Typography>
</div>
{caseStudyHref && caseStudyLabel ? (
<Link
href={caseStudyHref}
onClick={() => setOpen(false)}
className="flex items-center justify-between border-t border-border pt-3 text-xs font-semibold text-foreground transition-colors hover:text-red-600"
>
<span>{caseStudyLabel}</span>
<Icon icon="mdi:arrow-right" width={15} height={15} aria-hidden="true" />
</Link>
) : null}
</div>
</PopoverContent>
</Popover>
);
}
+433
View File
@@ -0,0 +1,433 @@
import Image from "next/image";
import { Icon } from "@iconify/react";
import { getTranslations } from "next-intl/server";
import {
Badge,
Button,
ButtonIcon,
ButtonLabel,
Card,
Typography,
} from "poyraz-ui/atoms";
import type { ProjectCaseStudy } from "@/data/project-case-studies";
import { Link } from "@/i18n/routing";
type ProjectCaseStudyContentProps = {
project: ProjectCaseStudy;
};
const TECHNOLOGY_ICONS: Record<string, string> = {
".NET 10": "logos:dotnet",
"Minimal APIs": "mdi:api",
"EF Core": "logos:dotnet",
PostgreSQL: "logos:postgresql",
"Angular 20": "logos:angular-icon",
"Angular Material": "simple-icons:angular",
"Tailwind CSS": "logos:tailwindcss-icon",
Astro: "logos:astro-icon",
Liquid: "mdi:code-braces",
Fluid: "mdi:water-outline",
Docker: "logos:docker-icon",
"Docker Compose": "logos:docker-icon",
"Next.js": "logos:nextjs-icon",
React: "logos:react",
"React Native": "logos:react",
TypeScript: "logos:typescript-icon",
"Express.js": "skill-icons:expressjs-light",
"AI Integrations": "mdi:robot-outline",
"Self-hosting": "mdi:server-outline",
Vite: "logos:vitejs",
"Better Auth": "mdi:shield-account-outline",
SQLite: "logos:sqlite",
"Drizzle ORM": "simple-icons:drizzle",
pnpm: "logos:pnpm",
Turborepo: "logos:turborepo-icon",
nginx: "logos:nginx",
Dokploy: "simple-icons:dokploy",
Supabase: "logos:supabase-icon",
"İŞKUR API": "mdi:briefcase-search-outline",
"Gemini AI": "logos:google-gemini",
};
function getTechnologyIcon(technology: string) {
return TECHNOLOGY_ICONS[technology] ?? "mdi:code-tags";
}
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<Typography variant="h3" className="border-b border-border pb-3">
{children}
</Typography>
);
}
function BulletList({ items }: { items: string[] }) {
return (
<ul className="space-y-2.5">
{items.map((item) => (
<li key={item} className="flex gap-2.5 text-sm leading-7 text-foreground/80">
<Icon
icon="mdi:check-circle-outline"
width={18}
height={18}
className="mt-1.5 shrink-0 text-red-600"
aria-hidden="true"
/>
<span>{item}</span>
</li>
))}
</ul>
);
}
export async function ProjectCaseStudyContent({
project,
}: ProjectCaseStudyContentProps) {
const t = await getTranslations({
locale: project.locale,
namespace: "ProjectCaseStudy",
});
return (
<article className="h-full overflow-y-auto pb-12">
<div className="mx-auto max-w-6xl space-y-10">
<Link
href="/projects"
className="inline-flex items-center gap-1.5 rounded-sm border border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
>
<Icon icon="mdi:arrow-left" width={16} height={16} aria-hidden="true" />
{t("back")}
</Link>
<header className="grid items-center gap-6 lg:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
<div className="space-y-5">
<div className="flex flex-wrap gap-2">
<Badge className="rounded-sm">{project.eyebrow}</Badge>
<Badge variant="outline" className="rounded-sm">
{project.context}
</Badge>
</div>
<div className="space-y-3">
<Typography variant="h2" className="text-3xl md:text-5xl">
{project.title}
</Typography>
<Typography
variant="p"
className="max-w-3xl text-base leading-8 text-muted-foreground md:text-lg"
>
{project.summary}
</Typography>
</div>
<div className="flex flex-wrap gap-2">
<Button asChild radius="sm" effect="swap" swapTarget="both">
<a href={project.liveUrl} target="_blank" rel="noopener noreferrer">
<ButtonIcon>
<Icon icon="mdi:open-in-new" width={17} height={17} />
</ButtonIcon>
<ButtonLabel>{t("liveDemo")}</ButtonLabel>
</a>
</Button>
{project.sourceUrl ? (
<Button
asChild
variant="outline"
radius="sm"
effect="swap"
swapTarget="both"
>
<a
href={project.sourceUrl}
target="_blank"
rel="noopener noreferrer"
>
<ButtonIcon>
<Icon icon="mdi:github" width={17} height={17} />
</ButtonIcon>
<ButtonLabel>{t("sourceCode")}</ButtonLabel>
</a>
</Button>
) : null}
</div>
</div>
<Card className="overflow-hidden rounded-sm border-border bg-muted/20 p-2">
<Image
src={project.image}
alt={project.screenshotAlt}
width={1080}
height={1080}
priority
sizes="(max-width: 1024px) 100vw, 420px"
className="aspect-square h-auto w-full rounded-sm object-cover"
/>
</Card>
</header>
<section className="space-y-4">
<SectionTitle>{t("roleAndTeam")}</SectionTitle>
<div className="grid gap-2 md:grid-cols-3">
{[
{
label: t("role"),
value: project.role,
icon: "mdi:account-hard-hat",
},
{
label: t("team"),
value: project.team,
icon: "mdi:account-group-outline",
href: project.teamUrl,
},
{
label: t("context"),
value: project.context,
icon: "mdi:briefcase-outline",
},
].map((item) => (
<Card key={item.label} className="rounded-sm border-border p-4">
<div className="mb-3 flex h-8 w-8 items-center justify-center rounded-sm bg-red-600/10 text-red-600">
<Icon icon={item.icon} width={18} height={18} aria-hidden="true" />
</div>
<Typography
variant="small"
className="block text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
>
{item.label}
</Typography>
{item.href ? (
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-flex items-start gap-1.5 text-sm font-semibold leading-6 text-foreground underline decoration-border underline-offset-4 transition-colors hover:text-red-600 hover:decoration-red-600"
>
<span>{item.value}</span>
<Icon
icon="mdi:open-in-new"
width={14}
height={14}
className="mt-1 shrink-0"
aria-hidden="true"
/>
</a>
) : (
<Typography variant="large" className="mt-1 text-sm leading-6">
{item.value}
</Typography>
)}
</Card>
))}
</div>
</section>
<section className="space-y-4">
<SectionTitle>{t("overview")}</SectionTitle>
<Card className="space-y-4 rounded-sm border-border p-5 md:p-6">
{project.overview.map((paragraph) => (
<Typography
key={paragraph}
variant="p"
className="text-sm leading-7 text-foreground/85"
>
{paragraph}
</Typography>
))}
</Card>
</section>
<section className="space-y-4">
<SectionTitle>{t("technologies")}</SectionTitle>
<Card className="rounded-sm border-border p-5">
<div className="flex flex-wrap gap-2">
{project.technologies.map((technology) => (
<Badge
key={technology}
size="sm"
variant="outline"
className="rounded-sm px-2.5 py-1"
>
<span className="inline-flex items-center gap-1.5">
<Icon
icon={getTechnologyIcon(technology)}
width={15}
height={15}
aria-hidden="true"
/>
<span>{technology}</span>
</span>
</Badge>
))}
</div>
</Card>
</section>
<section className="space-y-4">
<SectionTitle>{t("problemConstraints")}</SectionTitle>
<div className="grid gap-3 lg:grid-cols-2">
<Card className="rounded-sm border-border p-5 md:p-6">
<Typography variant="p" className="text-sm leading-7 text-foreground/85">
{project.problem}
</Typography>
</Card>
<Card className="rounded-sm border-border p-5 md:p-6">
<Typography variant="large" className="mb-4 text-sm">
{t("constraints")}
</Typography>
<BulletList items={project.constraints} />
</Card>
</div>
</section>
<section className="space-y-4">
<SectionTitle>{t("architectureDecisions")}</SectionTitle>
<div className="grid gap-3 md:grid-cols-2">
{project.decisions.map((decision, index) => (
<Card key={decision.title} className="rounded-sm border-border p-5 md:p-6">
<div className="mb-4 flex h-8 w-8 items-center justify-center rounded-sm bg-red-600 font-mono text-xs font-semibold text-white">
{String(index + 1).padStart(2, "0")}
</div>
<Typography variant="large" className="text-base">
{decision.title}
</Typography>
<Typography
variant="p"
className="mt-2 text-sm leading-7 text-muted-foreground"
>
{decision.description}
</Typography>
</Card>
))}
</div>
</section>
<section className="space-y-4">
<SectionTitle>{t("designProcess")}</SectionTitle>
<Card className="grid gap-6 rounded-sm border-border p-5 md:grid-cols-[1fr_1.1fr] md:p-6">
<Typography variant="p" className="text-sm leading-7 text-foreground/85">
{project.designProcess}
</Typography>
<ol className="space-y-3 border-border md:border-l md:pl-6">
{project.designSteps.map((step, index) => (
<li key={step} className="flex gap-3 text-sm leading-6 text-foreground/80">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-red-600/40 bg-red-600/10 font-mono text-[10px] font-semibold text-red-600">
{index + 1}
</span>
<span>{step}</span>
</li>
))}
</ol>
</Card>
</section>
<section className="grid gap-3 lg:grid-cols-2">
<Card className="rounded-sm border-border p-5 md:p-6">
<div className="mb-4 flex items-center gap-2 text-amber-600">
<Icon icon="mdi:alert-decagram-outline" width={21} height={21} />
<Typography variant="large" className="text-base text-foreground">
{t("challenge")}
</Typography>
</div>
<Typography variant="p" className="text-sm leading-7 text-muted-foreground">
{project.challenge}
</Typography>
</Card>
<Card className="rounded-sm border-border p-5 md:p-6">
<div className="mb-4 flex items-center gap-2 text-emerald-600">
<Icon icon="mdi:lightbulb-on-outline" width={21} height={21} />
<Typography variant="large" className="text-base text-foreground">
{t("solution")}
</Typography>
</div>
<Typography variant="p" className="text-sm leading-7 text-muted-foreground">
{project.solution}
</Typography>
</Card>
</section>
<section className="space-y-4">
<SectionTitle>{t("results")}</SectionTitle>
<div className="grid gap-2 md:grid-cols-3">
{project.results.map((result) => (
<Card key={result.label} className="rounded-sm border-border p-5">
<Typography className="font-mono text-3xl font-semibold text-red-600">
{result.value}
</Typography>
<Typography variant="large" className="mt-2 text-sm">
{result.label}
</Typography>
<Typography
variant="small"
className="mt-1 block leading-6 text-muted-foreground"
>
{result.description}
</Typography>
</Card>
))}
</div>
<Typography
variant="small"
className="block rounded-sm border border-dashed border-border px-4 py-3 leading-6 text-muted-foreground"
>
{project.metricsNote}
</Typography>
</section>
<section className="space-y-4">
<SectionTitle>{t("screenshots")}</SectionTitle>
<div className="grid gap-3 md:grid-cols-2">
{project.screenshots.map((screenshot) => (
<a
key={screenshot.src}
href={screenshot.src}
target="_blank"
rel="noopener noreferrer"
aria-label={`${screenshot.alt}${t("openScreenshot")}`}
className="group block no-underline text-inherit"
>
<Card className="h-full overflow-hidden rounded-sm border-border transition-colors group-hover:border-red-600/40">
<div className="relative aspect-video overflow-hidden bg-muted/20">
<Image
src={screenshot.src}
alt={screenshot.alt}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover object-top transition-transform duration-500 ease-out group-hover:scale-[1.015]"
/>
<span className="absolute top-3 right-3 inline-flex h-8 w-8 items-center justify-center rounded-sm border border-white/20 bg-black/65 text-white opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
<Icon icon="mdi:arrow-expand" width={17} height={17} aria-hidden="true" />
</span>
</div>
<Typography
variant="small"
className="block border-t border-border px-4 py-3 leading-6 text-muted-foreground"
>
{screenshot.caption}
</Typography>
</Card>
</a>
))}
</div>
</section>
<section className="space-y-4">
<SectionTitle>{t("repository")}</SectionTitle>
<Card className="flex items-start gap-3 rounded-sm border-border p-5">
<Icon
icon={project.sourceUrl ? "mdi:source-repository" : "mdi:lock-outline"}
width={20}
height={20}
className="mt-0.5 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
<Typography variant="small" className="leading-6 text-muted-foreground">
{project.sourceNote}
</Typography>
</Card>
</section>
</div>
</article>
);
}
+71 -19
View File
@@ -1,4 +1,3 @@
import Image from "next/image";
import { Link } from "@/i18n/routing";
import { Icon } from "@iconify/react";
import {
@@ -9,8 +8,9 @@ import {
Card,
Typography,
} from "poyraz-ui/atoms";
import { ImageCard } from "poyraz-ui/molecules";
import { StaggerContainer, StaggerItem } from "@/components/motion-wrapper";
import { GithubContributionGraph } from "@/components/github-contribution-graph";
import { ProjectCardWithPopover } from "@/components/project-card-with-popover";
import {
EXTENSIONS,
FIGMA_TEMPLATES,
@@ -18,7 +18,11 @@ import {
WEB_APPS,
type ProjectItem,
} from "@/data/projects";
import { getGithubRepos, getNpmPackages } from "@/lib/project-feeds";
import {
getGithubContributions,
getGithubRepos,
getNpmPackages,
} from "@/lib/project-feeds";
import { getTranslations, getLocale } from "next-intl/server";
import { getLocalizedValue } from "@/lib/locale";
@@ -64,16 +68,25 @@ type LocalizedProjectItem = {
title: string;
description: string;
image: string;
technologies: string[];
architecture: string;
badge?: string;
href?: string;
caseStudySlug?: string;
};
function ProjectSection({
title,
items,
technologiesLabel,
architectureLabel,
caseStudyLabel,
}: {
title: string;
items: LocalizedProjectItem[];
technologiesLabel: string;
architectureLabel: string;
caseStudyLabel: string;
}) {
return (
<section className="space-y-3">
@@ -83,12 +96,23 @@ function ProjectSection({
<StaggerContainer className="grid grid-cols-2 gap-2 lg:grid-cols-4">
{items.map((item) => (
<StaggerItem key={item.id}>
<ImageCard
<ProjectCardWithPopover
image={item.image}
title={item.title}
description={item.description}
badge={item.badge}
href={item.href}
caseStudyHref={
item.caseStudySlug
? `/projects/${item.caseStudySlug}`
: undefined
}
caseStudyLabel={caseStudyLabel}
technologies={item.technologies}
architecture={item.architecture}
technologiesLabel={technologiesLabel}
architectureLabel={architectureLabel}
triggerClassName="w-full"
className="aspect-square rounded-sm border-border"
/>
</StaggerItem>
@@ -102,15 +126,18 @@ export async function ProjectsContent() {
const t = await getTranslations("Projects");
const locale = await getLocale();
const [repos, npmPackages] = await Promise.all([
const [repos, npmPackages, contributions] = await Promise.all([
getGithubRepos(),
getNpmPackages(),
getGithubContributions(),
]);
const localizeItems = (items: ProjectItem[]): LocalizedProjectItem[] => {
return items.map((item) => ({
...item,
title: getLocalizedValue(item.title, locale),
description: getLocalizedValue(item.description, locale),
architecture: getLocalizedValue(item.architecture, locale),
badge: item.badge ? getLocalizedValue(item.badge, locale) : undefined,
}));
};
@@ -118,22 +145,47 @@ export async function ProjectsContent() {
return (
<section className="flex h-full flex-col gap-8 overflow-y-auto md:gap-10">
<Card className="rounded-sm border-border bg-background p-2">
<div className="overflow-x-auto rounded-sm">
<Image
src="https://ghchart.rshah.org/dc2626/poyrazavsever"
alt="poyrazavsever için GitHub katkı grafiği"
width={820}
height={120}
className="h-auto w-full min-w-[740px]"
unoptimized
/>
</div>
<GithubContributionGraph
days={contributions}
locale={locale}
labels={{
calendar: t("contributionCalendar"),
unavailable: t("contributionUnavailable"),
none: t("noContributions"),
singular: t("contributionSingular"),
plural: t("contributionPlural"),
}}
/>
</Card>
<ProjectSection title={t("sections.mobileApps")} items={localizeItems(MOBILE_APPS)} />
<ProjectSection title={t("sections.webApps")} items={localizeItems(WEB_APPS)} />
<ProjectSection title={t("sections.extensions")} items={localizeItems(EXTENSIONS)} />
<ProjectSection title={t("sections.figmaTemplates")} items={localizeItems(FIGMA_TEMPLATES)} />
<ProjectSection
title={t("sections.webApps")}
items={localizeItems(WEB_APPS)}
technologiesLabel={t("technologies")}
architectureLabel={t("architecture")}
caseStudyLabel={t("viewCaseStudy")}
/>
<ProjectSection
title={t("sections.mobileApps")}
items={localizeItems(MOBILE_APPS)}
technologiesLabel={t("technologies")}
architectureLabel={t("architecture")}
caseStudyLabel={t("viewCaseStudy")}
/>
<ProjectSection
title={t("sections.extensions")}
items={localizeItems(EXTENSIONS)}
technologiesLabel={t("technologies")}
architectureLabel={t("architecture")}
caseStudyLabel={t("viewCaseStudy")}
/>
<ProjectSection
title={t("sections.figmaTemplates")}
items={localizeItems(FIGMA_TEMPLATES)}
technologiesLabel={t("technologies")}
architectureLabel={t("architecture")}
caseStudyLabel={t("viewCaseStudy")}
/>
<section className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
+38 -9
View File
@@ -1,5 +1,7 @@
import { TestimonialCard } from "poyraz-ui/molecules";
import Image from "next/image";
import { Card, CardContent } from "poyraz-ui/atoms";
import { StarRating } from "poyraz-ui/molecules";
import { REFERENCES } from "@/data/references";
import { useLocale } from "next-intl";
import { getLocalizedValue } from "@/lib/locale";
@@ -27,14 +29,41 @@ export function ReferenceCards({
const quoteText = getLocalizedValue(item.quote, locale);
const card = (
<TestimonialCard
quote={quoteText}
author={item.author}
role={getLocalizedValue(item.role, locale)}
avatar={item.avatar}
rating={showRating ? item.rating : undefined}
className={`flex flex-col rounded-sm ${lineClamp ? "[&_blockquote]:line-clamp-4" : ""} ${hoverClassName} ${cardClassName || "w-70 shrink-0"}`}
/>
<Card
className={`group flex h-full flex-col rounded-sm ${hoverClassName} ${cardClassName || "w-70 shrink-0"}`}
>
<CardContent className="flex h-full flex-1 flex-col p-5">
<span className="font-secondary text-4xl leading-none text-primary">
</span>
<blockquote
className={`mt-1 text-sm leading-relaxed text-secondary-foreground ${lineClamp ? "line-clamp-4" : ""}`}
>
{quoteText}
</blockquote>
{showRating && item.rating != null ? (
<StarRating rating={item.rating} className="mt-3" />
) : null}
<div className="mt-auto flex items-center gap-3 border-t border-border pt-4">
<Image
src={item.avatar}
alt=""
width={36}
height={36}
sizes="36px"
className="size-9 rounded-full object-cover"
/>
<div className="min-w-0">
<div className="truncate text-sm font-semibold">
{item.author}
</div>
<div className="truncate text-xs text-muted-foreground">
{getLocalizedValue(item.role, locale)}
</div>
</div>
</div>
</CardContent>
</Card>
);
const href = item.documentHref || item.profileHref;
+64 -36
View File
@@ -25,9 +25,6 @@ import {
SheetContent,
SheetTitle,
SheetTrigger,
Tabs,
TabsList,
TabsTrigger,
Tooltip,
TooltipContent,
TooltipProvider,
@@ -51,6 +48,10 @@ const SearchCommand = dynamic(
);
const slowShineClassName = "[--poyraz-motion-duration-deliberate:1100ms]";
const dropdownItemLinkClassName =
"grid w-full grid-cols-[1.25rem_minmax(0,1fr)] items-center gap-2";
const mobileDropdownItemLinkClassName =
"grid min-h-10 w-full grid-cols-[1.25rem_minmax(0,1fr)_1rem] items-center gap-3 rounded-sm px-2 py-2 text-sm text-foreground/70 transition-colors hover:bg-muted hover:text-foreground";
function getNavLinkClass(isActive: boolean) {
return [
@@ -70,6 +71,18 @@ function getDesktopDropdownTriggerClass(isActive: boolean) {
].join(" ");
}
function getDesktopNavLinkClass(isActive: boolean) {
return [
"relative inline-flex h-9 shrink-0 items-center justify-center whitespace-nowrap rounded-sm px-2.5 text-xs font-medium outline-none",
"transition-[color,background-color] duration-[var(--poyraz-motion-duration-fast)] ease-[var(--poyraz-motion-ease-out)]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"after:absolute after:inset-x-2.5 after:bottom-0 after:h-0.5 after:rounded-full after:transition-colors",
isActive
? "text-foreground after:bg-primary"
: "text-muted-foreground after:bg-transparent hover:text-foreground",
].join(" ");
}
type ThemeToggleProps = {
theme: ThemeMode;
onThemeChange: (theme: ThemeMode) => void;
@@ -121,7 +134,6 @@ export function SiteNavbar({
return pathname === href || pathname.startsWith(`${href}/`);
};
const activeTab = NAV_LINKS.find((item) => isActiveLink(item.href))?.id;
const activeMobileMenuGroup = NAV_DROPDOWN_GROUPS.find(
(group) => group.id === mobileMenuGroupId,
);
@@ -200,7 +212,7 @@ export function SiteNavbar({
className="inline-flex shrink-0 items-center"
>
<Logo
src="/logo/logo.png"
src="/logo/logo-96.webp"
alt="Poyraz Avsever"
width={40}
height={40}
@@ -212,13 +224,10 @@ export function SiteNavbar({
</Link>
<div className="hidden min-w-0 flex-1 items-center justify-end gap-2 min-[840px]:flex">
<Tabs value={activeTab ?? ""} className="w-auto shrink-0">
<TabsList
variant="line"
radius="sm"
className="h-9 gap-1 bg-transparent p-0"
aria-label="Ana navigasyon"
>
<nav
aria-label={locale === "tr" ? "Ana navigasyon" : "Main navigation"}
className="flex h-9 w-auto shrink-0 items-center gap-1"
>
{NAV_LINKS.map((item, index) => (
<Fragment key={item.id}>
<div className="flex items-center gap-1.5">
@@ -229,15 +238,15 @@ export function SiteNavbar({
decorative
/>
)}
<TabsTrigger
value={item.id}
asChild
size="sm"
radius="sm"
className="cursor-pointer px-2.5"
<Link
href={item.href}
aria-current={isActiveLink(item.href) ? "page" : undefined}
className={getDesktopNavLinkClass(
isActiveLink(item.href),
)}
>
<Link href={item.href}>{t(item.id)}</Link>
</TabsTrigger>
{t(item.id)}
</Link>
</div>
{NAV_DROPDOWN_GROUPS.filter(
@@ -280,18 +289,28 @@ export function SiteNavbar({
href={groupItem.href}
target="_blank"
rel="noreferrer"
className="flex items-center gap-2"
className={dropdownItemLinkClassName}
>
<Icon icon={groupItem.icon} width={16} height={16} />
<span>{t(groupItem.id)}</span>
<Icon
icon={groupItem.icon}
width={16}
height={16}
className="block justify-self-center"
/>
<span className="min-w-0 leading-5">{t(groupItem.id)}</span>
</a>
) : (
<Link
href={groupItem.href}
className="flex items-center gap-2"
className={dropdownItemLinkClassName}
>
<Icon icon={groupItem.icon} width={16} height={16} />
<span>{t(groupItem.id)}</span>
<Icon
icon={groupItem.icon}
width={16}
height={16}
className="block justify-self-center"
/>
<span className="min-w-0 leading-5">{t(groupItem.id)}</span>
</Link>
)}
</DropdownMenuItem>
@@ -302,8 +321,7 @@ export function SiteNavbar({
))}
</Fragment>
))}
</TabsList>
</Tabs>
</nav>
<Separator
orientation="vertical"
@@ -430,15 +448,20 @@ export function SiteNavbar({
href={groupItem.href}
target="_blank"
rel="noreferrer"
className="flex min-h-10 w-full items-center gap-3 rounded-sm px-2 py-2 text-sm text-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
className={mobileDropdownItemLinkClassName}
>
<Icon icon={groupItem.icon} width={18} height={18} />
<span>{t(groupItem.id)}</span>
<Icon
icon={groupItem.icon}
width={18}
height={18}
className="block justify-self-center"
/>
<span className="min-w-0 leading-5">{t(groupItem.id)}</span>
<Icon
icon="mdi:open-in-new"
width={14}
height={14}
className="ml-auto text-muted-foreground"
className="block justify-self-center text-muted-foreground"
/>
</a>
</SheetClose>
@@ -446,15 +469,20 @@ export function SiteNavbar({
<SheetClose asChild>
<Link
href={groupItem.href}
className="flex min-h-10 w-full items-center gap-3 rounded-sm px-2 py-2 text-sm text-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
className={mobileDropdownItemLinkClassName}
>
<Icon icon={groupItem.icon} width={18} height={18} />
<span>{t(groupItem.id)}</span>
<Icon
icon={groupItem.icon}
width={18}
height={18}
className="block justify-self-center"
/>
<span className="min-w-0 leading-5">{t(groupItem.id)}</span>
<Icon
icon="mdi:chevron-right"
width={16}
height={16}
className="ml-auto text-muted-foreground"
className="block justify-self-center text-muted-foreground"
/>
</Link>
</SheetClose>
+59
View File
@@ -0,0 +1,59 @@
"use client";
import { useTranslations } from "next-intl";
import { Card, Typography } from "poyraz-ui/atoms";
import { TechnologyBadge } from "@/components/technology-badge";
import { TECHNOLOGY_STACK } from "@/data/technology-stack";
export function TechnologiesContent() {
const t = useTranslations("Technologies");
return (
<section className="flex h-full flex-col gap-10">
<header className="border-b border-border py-4 sm:py-5">
<Typography
variant="h2"
component="h1"
className="font-secondary text-2xl font-semibold leading-none tracking-[-0.045em] text-foreground"
>
{t("title")}
</Typography>
<Typography
variant="small"
className="mt-2 max-w-2xl text-xs leading-5 text-muted-foreground sm:text-sm"
>
{t("description")}
</Typography>
</header>
<div className="grid gap-3 md:grid-cols-2">
{TECHNOLOGY_STACK.map((group) => (
<Card
key={group.id}
role="region"
className="rounded-sm border-border p-4"
aria-labelledby={`technology-group-${group.id}`}
>
<Typography
id={`technology-group-${group.id}`}
variant="large"
component="h2"
className="text-sm font-semibold tracking-[-0.02em]"
>
{t(`categories.${group.id}`)}
</Typography>
<div className="mt-3 flex flex-wrap gap-2">
{group.items.map((technology) => (
<TechnologyBadge
key={technology.id}
technology={technology}
/>
))}
</div>
</Card>
))}
</div>
</section>
);
}
+32
View File
@@ -0,0 +1,32 @@
"use client";
import { Icon } from "@iconify/react";
import { useLocale } from "next-intl";
import { Badge } from "poyraz-ui/atoms";
import type { TechnologyStackItem } from "@/data/technology-stack";
import { getLocalizedValue } from "@/lib/locale";
export function TechnologyBadge({
technology,
}: {
technology: TechnologyStackItem;
}) {
const locale = useLocale();
return (
<Badge
variant="secondary"
radius="sm"
className="shrink-0 gap-1.5 px-2.5 py-1 text-xs font-medium text-foreground"
>
<Icon
icon={technology.icon}
width={14}
height={14}
aria-hidden="true"
className="shrink-0"
/>
<span>{getLocalizedValue(technology.label, locale)}</span>
</Badge>
);
}
@@ -2,7 +2,7 @@
title: "How to Build an AI Avatar That Follows the Pointer"
slug: "poyraz-cursor-portrait"
excerpt: "Build the effect from start to finish with the prompts, Wiro AI and MiniMax H3 video workflow, FFmpeg preparation, and React integration I used."
coverImage: "/animation-sources/poyraz-cursor-portrait/avatar-mouse-follow.gif"
coverImage: "/animation-sources/poyraz-cursor-portrait/avatar-mouse-follow.webp"
platform: "Web"
tools:
- "Wiro AI"
@@ -2,7 +2,7 @@
title: "Fareyi Takip Eden AI Avatar Nasıl Yapılır?"
slug: "poyraz-cursor-portrait"
excerpt: "Wiro AI üzerinde MiniMax H3 ile ürettiğim 1:1 avatar videosunu, promptlardan FFmpeg optimizasyonuna ve React entegrasyonuna kadar adım adım oluşturun."
coverImage: "/animation-sources/poyraz-cursor-portrait/avatar-mouse-follow.gif"
coverImage: "/animation-sources/poyraz-cursor-portrait/avatar-mouse-follow.webp"
platform: "Web"
tools:
- "Wiro AI"
@@ -6,7 +6,7 @@ readTime: "6 min read"
author: "Poyraz Avsever"
slug: "css-frameworkleri-tan-yoruz-5-b-l-m-materialize"
excerpt: "CSS Frameworkleri Tanıyoruz | 5. Bölüm: Materialize Merhaba arkadaşlar, bugün 5.bölümde sizlerle “Materialize” ile tanışıyoruz. Dilerseniz başlayalım. Google’ın Material Design …"
coverImage: "/blog/images/css-frameworkleri-tan-yoruz-5-b-l-m-materialize-cover.jpg"
coverImage: "/blog/images/css-frameworkleri-tan-yoruz-5-b-l-m-materialize-cover.webp"
canonicalUrl: "https://medium.com/@poyrazavsever/css-frameworkleri-tan%C4%B1yoruz-5-b%C3%B6l%C3%BCm-materialize-5fd3c99862c2"
---
@@ -271,4 +271,4 @@ Materialize CSSin resmi web sitesinde, farklı kullanım senaryolarına uygun
Materialize CSS, sade yapısı ve Google’ın Material Design ilkelerine olan sadakatiyle, özellikle görsel olarak güçlü ve modern arayüzler oluşturmak isteyen geliştiriciler için büyük bir kolaylık sağlar. Hazır bileşenleri, responsive grid sistemi ve basit sözdizimi sayesinde hem yeni başlayanlar hem de hızlı prototipleme yapmak isteyen deneyimli geliştiriciler tarafından tercih edilebilir.
Ancak framework’ün büyük projelerdeki sınırlılıkları, özelleştirme konusundaki kısıtlamaları ve topluluk desteğinin sınırlı oluşu gibi dezavantajlarını da göz ardı etmemek gerekir. Eğer sade, hızlı ve estetik bir çözüm arıyorsan, Materialize CSS tam sana göre olabilir.
Ancak framework’ün büyük projelerdeki sınırlılıkları, özelleştirme konusundaki kısıtlamaları ve topluluk desteğinin sınırlı oluşu gibi dezavantajlarını da göz ardı etmemek gerekir. Eğer sade, hızlı ve estetik bir çözüm arıyorsan, Materialize CSS tam sana göre olabilir.
@@ -145,7 +145,7 @@ Bu bakış açısını öğrendiğimden ve uygulamaya başladığımdan beri fro
Frontend mimarisinde güven veren şey, hatasız olmak değil; **hatalarla yaşayabilecek bir yapı kurmaktır**.
![](/blog/images/frontend-mimarisinde-en-zor-k-s-m-karar-vermek-img-7.jpg)
![](/blog/images/frontend-mimarisinde-en-zor-k-s-m-karar-vermek-img-7.webp)
## Frontend Mimarisinde Olgunluğumun Gelişimi
@@ -209,4 +209,4 @@ Bu değişim bir anda olmadı. Projeler büyüdükçe, bazı kararların yük ha
Eğer bu yazı, senin de bir kararın üzerine biraz daha düşünmene ya da “ben bunu neden böyle yapmıştım?” diye sormana sebep olduysa, amacına ulaşmış demektir. Frontend mimarisi çoğu zaman sessiz ilerler ama verdiğimiz kararlar uzun süre bizimle kalır.
Okuduğun için teşekkür ederim. Umarım bir yerinde sana da dokunmuştur
Okuduğun için teşekkür ederim. Umarım bir yerinde sana da dokunmuştur
+1 -1
View File
@@ -6,7 +6,7 @@ readTime: "5 min read"
author: "Poyraz Avsever"
slug: "newsletter0612072026-en"
excerpt: "This week, we have a packed agenda, ranging from OpenAI's changing model strategies to the historic trade secret theft lawsuit filed by Apple, and from new technologies running in the browser to useful open-source projects."
coverImage: "/blog/images/newsletter0612072026-cover.png"
coverImage: "/blog/images/newsletter0612072026-cover.webp"
lang: "en"
---
+2 -2
View File
@@ -6,7 +6,7 @@ readTime: "5 min read"
author: "Poyraz Avsever"
slug: "newsletter0612072026"
excerpt: "Bu hafta OpenAI'ın değişen model stratejilerinden Apple'ın açtığı sır hırsızlığı davasına, tarayıcıda çalışan yeni teknolojilerden açık kaynak projelere kadar dolu dolu bir gündemimiz var."
coverImage: "/blog/images/newsletter0612072026-cover.png"
coverImage: "/blog/images/newsletter0612072026-cover.webp"
lang: "tr"
---
@@ -114,4 +114,4 @@ Ajanların sayfa trafiğini artırması ama reklamlara tıklamaması sorununu ç
[Kaynak linki](https://mashable.com/tech/biggest-cybersecurity-data-breaches-2026)
Hackerlar artık sistem açığı bulmak yerine doğrudan yapay zeka asistanlarını manipüle ediyor. Örneğin Meta'nın destek botu manipüle edilerek hesapların ele geçirilmesi sağlandı. Dil modellerinin dışarıdan gelen talimatlara "ikna olma" zafiyeti, en az geleneksel yazılım açıkları kadar tehlikeli bir boyuta ulaştı. Prompt injection türü saldırılar önümüzdeki dönemin en büyük siber güvenlik problemi olacak.
Hackerlar artık sistem açığı bulmak yerine doğrudan yapay zeka asistanlarını manipüle ediyor. Örneğin Meta'nın destek botu manipüle edilerek hesapların ele geçirilmesi sağlandı. Dil modellerinin dışarıdan gelen talimatlara "ikna olma" zafiyeti, en az geleneksel yazılım açıkları kadar tehlikeli bir boyuta ulaştı. Prompt injection türü saldırılar önümüzdeki dönemin en büyük siber güvenlik problemi olacak.
+1 -1
View File
@@ -6,7 +6,7 @@ readTime: "5 min read"
author: "Poyraz Avsever"
slug: "newsletter2702082026-en"
excerpt: "This week, we have a packed agenda, from autonomous AI models and data center water consumption to major shifts in design tools, next-generation batteries, and chip technologies."
coverImage: "/blog/images/newsletter2702082026-cover.png"
coverImage: "/blog/images/newsletter2702082026-cover.webp"
lang: "en"
---
+1 -1
View File
@@ -6,7 +6,7 @@ readTime: "5 min read"
author: "Poyraz Avsever"
slug: "newsletter2702082026"
excerpt: "Bu hafta otonom hareket eden yapay zeka modellerinden veri merkezlerinin su tüketimine, tasarım araçlarındaki dönüşümden yeni nesil batarya ve çip teknolojilerine kadar yoğun bir gündemimiz var."
coverImage: "/blog/images/newsletter2702082026-cover.png"
coverImage: "/blog/images/newsletter2702082026-cover.webp"
lang: "tr"
---
@@ -6,7 +6,7 @@ readTime: "5 min read"
author: "Poyraz Avsever"
slug: "next-js-ile-edge-functions-h-zl-web-uygulamalar-nas-l-geli-tirilir"
excerpt: "Next.js ile Edge Functions: Hızlı Web Uygulamaları Nasıl Geliştirilir? TL;DR (Too Long; Didnt Read): Edge Functions, kullanıcıya en yakın konumda çalışan, hızlı serverless …"
coverImage: "/blog/images/next-js-ile-edge-functions-h-zl-web-uygulamalar-nas-l-geli-tirilir-cover.jpg"
coverImage: "/blog/images/next-js-ile-edge-functions-h-zl-web-uygulamalar-nas-l-geli-tirilir-cover.webp"
canonicalUrl: "https://medium.com/@poyrazavsever/next-js-ile-edge-functions-h%C4%B1zl%C4%B1-web-uygulamalar%C4%B1-nas%C4%B1l-geli%C5%9Ftirilir-d94eafacf50e"
---
@@ -245,4 +245,4 @@ Bir sonraki yazıda görüşmek üzere, **sağlıcakla!**
![](/blog/images/next-js-ile-edge-functions-h-zl-web-uygulamalar-nas-l-geli-tirilir-img-7.jpg)
> _Görseller_ **_Gemini_** _ile oluşturulmuştur._
> _Görseller_ **_Gemini_** _ile oluşturulmuştur._
@@ -6,7 +6,7 @@ readTime: "7 min read"
author: "Poyraz Avsever"
slug: "poyraz-ile-yazilima-dair-0309082026"
excerpt: "Bu hafta yapay zeka maliyet savaşlarından DeepMind liderlik değişimine, 100x geliştirici tartışmasından Figma dosya mimarisine ve teknoloji sektöründeki güç dengelerine kadar yoğun bir gündemimiz var."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-0309082026-cover.png"
coverImage: "/blog/images/poyraz-ile-yazilima-dair-0309082026-cover.webp"
lang: "tr"
---
@@ -6,7 +6,7 @@ readTime: "6 min read"
author: "Poyraz Avsever"
slug: "poyraz-ile-yazilima-dair-1319072026"
excerpt: "Bu hafta yapay zeka model fiyat savaşlarından kurumsal ajan protokollerine, HTTP QUERY metodundan Figma ve Cloudflare güncellemelerine kadar yoğun bir teknoloji gündemimiz var."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-1319072026-cover.png"
coverImage: "/blog/images/poyraz-ile-yazilima-dair-1319072026-cover.webp"
lang: "tr"
---
@@ -0,0 +1,323 @@
---
title: "Poyraz ile Yazılıma Dair #2430082026"
category: "Newsletter"
date: "2026-08-30"
readTime: "13 min read"
author: "Poyraz Avsever"
slug: "poyraz-ile-yazilima-dair-2430082026"
excerpt: "Bu hafta kontrolden çıkan yapay zekâ ajanlarından fiziksel cihazları yöneten sistemlere, Kubernetes 1.37'den Apple M6'ya kadar yoğun bir teknoloji gündemimiz var."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-2430082026-cover.webp"
lang: "tr"
---
# Poyraz ile Yazılıma Dair #2430082026
Selamlar,
“Poyraz ile Yazılıma Dair”in yeni sayısına hoş geldiniz. Bu hafta yapay zekâ ajanlarının dijital sınırları aşmasından fiziksel cihazları kontrol etmeye başlamasına, Kubernetesin yeni sürümünden Apple’ın M6 işlemcisine kadar oldukça yoğun bir teknoloji gündemi vardı.
Ben de geliştiriciler, tasarımcılar, öğrenciler ve teknoloji meraklıları açısından gerçekten anlamlı bulduğum gelişmeleri ayıklayarak tek bir yerde topladım.
## Yapay zekâ gelişmeleri
### OpenAI, kontrolden çıkan yapay zekâ ajanlarıyla ilgili detaylı raporunu yayımladı
**Kaynaklar:** https://openai.com/index/hugging-face-incident-and-the-road-ahead/, https://metr.org/blog/2026-08-26-openai-hugging-face-incident-investigation/, https://www.reuters.com/business/openai-report-says-its-network-was-hacked-by-its-own-rogue-ai-agents-2026-08-26/, https://news.ycombinator.com/item?id=49454314
OpenAI, temmuz ayında gerçekleştirilen şirket içi siber güvenlik testinde yaşanan olaylarla ilgili ayrıntılı teknik raporunu yayımladı. Testlerde, GPT-5.6 Sol ile benzer ölçekte olduğu belirtilen ancak güvenlik kısıtlamaları azaltılmış şirket içi bir model kullanıldı.
Görevleri güvenlik açıklarını tespit etmek olan bazı ajanlar, kendilerine tanımlanan iletişim kanallarının dışına çıktı. Paylaşılan altyapıdaki zayıflıklardan yararlanarak internete eriştiler ve üçüncü taraf sistemlerle izinsiz etkileşim kurdular. METR’ın bağımsız incelemesine göre açığa çıkan Hugging Face erişim bilgileri, ajanların kullandığı ortak bir çalışma alanında paylaşıldı ve yüzlerce ajan kötü amaçlı veri yüklemelerine yöneldi.
Buradaki yeni gelişme olayın kendisinden ziyade, OpenAI ve METR tarafından bu hafta yayımlanan teknik incelemeler. Raporlar, çok sayıda ajanın aynı altyapıda çalıştırılması durumunda beklenmeyen davranışların birbirini besleyebileceğini gösteriyor.
**Neden önemli?**
Bugüne kadar AI güvenliği denildiğinde daha çok zararlı cevaplar ve yanlış bilgi konuşuluyordu. Otonom ajanlar dosya sistemlerine, terminale, API anahtarlarına ve internet erişimine sahip oldukça güvenlik problemi doğrudan altyapı güvenliğine dönüşüyor.
Özellikle kendi sunucusunda AI ajanı çalıştıran geliştiriciler için izin sınırları, ağ erişimi, gizli anahtarların saklanması ve işlemlerin izlenmesi artık ikincil konular değil.
**Düşüncem:**
Bence bu olay, “model ne kadar akıllı?” sorusundan önce “modele ne kadar yetki verdik?” diye sormamız gerektiğini gösteriyor. Tek bir ajanın hata yapması başka, yüzlerce ajanın ortak altyapı üzerinden birbirini etkilemesi bambaşka bir risk. AI ajanlarını üretim ortamına bağlarken yalnızca iyi prompt yazmak kesinlikle yeterli değil. En az yetki, izole çalışma ortamı ve ayrıntılı kayıt sistemi standart hâline gelmeli.
---
### Anthropic, AI ajanlarını fiziksel cihazlara bağlayan Model Hardware Standard’ı duyurdu
**Kaynaklar:** https://www.anthropic.com/news/model-hardware-standard-research-preview, https://www.reuters.com/technology/anthropic-unveils-new-framework-allowing-ai-agents-operate-physical-devices-2026-08-27/, https://news.ycombinator.com/item?id=49468834
Anthropic, yapay zekâ ajanlarının fiziksel cihazlarla güvenli ve standartlaştırılmış şekilde iletişim kurmasını amaçlayan Model Hardware Standard’ın araştırma ön izlemesini yayımladı. Çalışma, Howard Hughes Medical Institute bünyesindeki Janelia Research Campus ile birlikte geliştiriliyor.
Standart; mikroskoplar, sıvı taşıma sistemleri, robotik kollar ve lazer kalibrasyonu gibi programlanabilir cihazların farklı modeller tarafından kontrol edilebilmesini hedefliyor. Anthropice göre bugün haftalar veya aylar sürebilen bazı donanım entegrasyonları, ortak bir arayüz sayesinde saatler ya da dakikalar içerisinde gerçekleştirilebilir.
Sistem modele bağımlı değil. Donanımlar ortak bir tanım üzerinden kullanılabiliyor ve ajanlar bu araçlara MCP benzeri standart protokoller aracılığıyla erişebiliyor. Anthropic, güvenlik testleri ve erken dönem iş ortaklıklarının ardından standardı açık kaynak olarak yayımlamayı planlıyor.
**Neden önemli?**
AI ajanlarının bugüne kadarki kullanım alanı ağırlıklı olarak tarayıcı, kod editörü ve kurumsal yazılımlardı. Fiziksel cihazların devreye girmesiyle bir ajanın hatası yalnızca yanlış dosya oluşturmakla kalmayabilir; gerçek bir makineyi veya bilimsel deneyi etkileyebilir.
Bu nedenle yetkilendirme, acil durdurma mekanizmaları ve işlemlerin fiziksel olarak doğrulanması çok daha kritik hâle geliyor.
**Düşüncem:**
MCPnin yazılımlar için oluşturduğu ortak bağlantı mantığının donanım tarafına taşınması bence oldukça mantıklı. Ama fiziksel dünyada “ajan yanlış yaptı, işlemi geri alalım” demek her zaman mümkün değil. Bu yüzden hız kadar güvenlik katmanlarının da standartlaştırılması gerekiyor. Doğru uygulanırsa bilimsel araştırma ve üretim otomasyonu açısından gerçekten büyük bir adım olabilir.
---
### Google, Gemini Omni 1.1 Flash ile video üretiminde kontrolü artırdı
**Kaynaklar:** https://blog.google/innovation-and-ai/technology/developers-tools/build-with-gemini-omni-1-1-flash/, https://deepmind.google/blog/gemini-omni-1-1-flash-lets-you-build-with-more-control/, https://ai.google.dev/gemini-api/docs/models/gemini-omni-flash, https://the-decoder.com/googles-gemini-omni-1-1-flash-makes-ai-video-generation-cheaper-and-more-flexible/, https://news.ycombinator.com/item?id=49467922
Google, geliştiricilere yönelik video üretim ve düzenleme modeli Gemini Omni 1.1 Flash’ı kullanıma sundu. Model, yalnızca metinden video oluşturmak yerine üretim sürecinin farklı aşamalarında daha fazla kontrol sağlamaya odaklanıyor.
Sahne uzatma özelliği artık videonun yalnızca son karesini değil, önceki 10 saniyelik bölümü bağlam olarak kullanabiliyor. Videolar 10 saniyelik parçalar hâlinde uzatılarak toplam 40 saniyeye ulaşabiliyor. Başlangıç ve bitiş karelerinin ayrı ayrı belirlenmesi de iki görüntü arasında kontrollü bir hareket oluşturmayı mümkün kılıyor.
Geliştiriciler önce 360p çözünürlükte daha hızlı ve düşük maliyetli denemeler yapabiliyor, beğendikleri sonucu daha sonra 4K olarak oluşturabiliyor. Model Google AI Studio ve Gemini API üzerinden kullanılabiliyor.
**Neden önemli?**
AI video üretimindeki temel sorun artık yalnızca görüntü kalitesi değil. Karakter, kamera, hareket ve sahne devamlılığının her denemede değişmesi profesyonel üretimi zorlaştırıyor.
Ön izleme ile final çıktısının ayrılması, özellikle içerik üreticileri ve AI tabanlı ürün geliştiren ekipler için deneme maliyetini azaltabilir.
**Düşüncem:**
Bence AI video tarafındaki yarış, artık “kim daha gerçekçi görüntü üretiyor?” seviyesinden çıkıyor. Asıl önemli konu, üretilen sonucu ne kadar yönlendirebildiğimiz ve aynı karakteri ne kadar tutarlı koruyabildiğimiz. Düşük çözünürlükte hızlı deneme yapıp yalnızca seçilen sonucu 4K üretmek de oldukça mantıklı. İçerik üretiminde kullanılabilirliği artıran şey gösterişli demolar değil, bu tarz küçük ama gerçek kontrol mekanizmaları olacak.
## Yazılım gelişmeleri
### Kubernetes 1.37 “Garhwal” yayımlandı
**Kaynaklar:** https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/, https://www.sysdig.com/blog/kubernetes-1-37-new-security-features
Kubernetes 1.37 “Garhwal”, toplam 67 geliştirmeyle yayımlandı. Bunların 16sı kararlı, 23’ü beta ve 27si alpha seviyesine yükseldi. Sürümde ayrıca bir kullanımdan kaldırma veya kaldırılma değişikliği bulunuyor.
En dikkat çekici yeniliklerden biri Horizontal Pod Autoscaler’ın belirli harici ve nesne metriklerine göre iş yükünü sıfır pod seviyesine kadar küçültebilmesi. Özellik beta seviyesine geldi ve varsayılan olarak etkinleştirildi. Böylece sürekli çalışması gerekmeyen servislerin kullanılmadıkları zaman kaynak tüketmemesi sağlanabiliyor.
`metrics.k8s.io` APIsi yaklaşık dokuz yıllık beta sürecinin ardından kararlı hâle geldi. SELinuxMount kararlı seviyeye ulaşırken Dynamic Resource Allocation tarafında özellikle GPU ve özel donanım yönetimini ilgilendiren geliştirmeler yapıldı. Pod checkpoint ve restore özelliği de alpha seviyesinde sunuldu.
**Neden önemli?**
Özellikle AI inference, toplu işlem ve olay tabanlı servislerde kaynakların boşta beklemesi ciddi maliyet oluşturuyor. Sıfıra ölçekleme bu maliyeti azaltabilirken dinamik kaynak yönetimi GPU gibi sınırlı donanımların daha verimli paylaşılmasını sağlayabilir.
Self-host sistemler kuran geliştiriciler açısından da daha az donanımla daha fazla servisi yönetebilmek önemli bir avantaj.
**Düşüncem:**
Kubernetes bazen ihtiyacımızdan çok daha karmaşık bir çözüm olabiliyor. Ama çok sayıda servis ve GPU iş yükü yönetmeye başladığımızda bu geliştirmelerin karşılığı ortaya çıkıyor. Sıfıra ölçekleme özellikle sürekli kullanılmayan AI servisleri için ciddi maliyet avantajı sağlayabilir. Yine de her yeni özelliği sırf var diye kullanmak yerine operasyonel karmaşıklığını da hesaba katmak gerekiyor.
---
### GitLab, self-host kurulumlar için kritik güvenlik güncellemesi yayımladı
**Kaynaklar:** https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-3-1-released/, https://nvd.nist.gov/vuln/detail/CVE-2026-77801
GitLab, Community Edition ve Enterprise Edition için 19.3.1, 19.2.5 ve 19.1.7 güvenlik sürümlerini yayımladı. Şirket, internet üzerinden erişilebilen self-host GitLab kurulumlarının mümkün olan en kısa sürede güncellenmesini öneriyor.
Güncellemeyle kapatılan CVE-2026-77801 açığı, kimliği doğrulanmış bir kullanıcının arka plan görevlerinin işlenmesini durdurabilecek bir hizmet engelleme saldırısı gerçekleştirmesine imkân tanıyordu. Açık, belirli nesnelerin sayısına yeterli sınır uygulanmamasından kaynaklanıyordu ve CVSS sisteminde 6,5 puan aldı.
GitLab.com altyapısı şirket tarafından güncellendi. GitLab Dedicated kullanan müşteriler için de ayrıca işlem yapılması gerekmiyor. Ancak kendi GitLab sunucusunu yöneten ekiplerin güncellemeyi kendilerinin uygulaması gerekiyor.
**Neden önemli?**
Arka plan görevleri durduğunda CI/CD işlemleri, e-posta bildirimleri, repository güncellemeleri ve diğer otomasyonlar etkilenebilir. Git sunucusunun erişilebilir olması, sistemin tamamen sağlıklı çalıştığı anlamına gelmez.
Bu güncelleme, self-host sistemlerde kontrolün kullanıcıda olması kadar bakım sorumluluğunun da kullanıcıda olduğunu hatırlatıyor.
**Düşüncem:**
Son dönemde Gitea ve self-host sistemlerle daha fazla ilgilendiğim için bu haber benim açımdan ayrıca önemli. Kendi Git sunucunu kurmak bağımsızlık ve kontrol sağlıyor ama güncellemeleri takip etmediğinde ciddi bir risk de oluşturuyor. Self-host yalnızca Docker Compose dosyasını çalıştırıp unutmak değil. Güncelleme, yedekleme ve izleme süreçlerini de kurulumun bir parçası olarak düşünmek gerekiyor.
---
### GitHub Classroom tamamen kapatıldı
**Kaynaklar:** https://github.blog/changelog/2026-08-27-github-classroom-deprecated/, https://github.com/orgs/community/discussions/205975, https://docs.github.com/en/education/manage-coursework-with-github-classroom/get-started-with-github-classroom/about-github-classroom
GitHub Classroom’ın web sitesi, APIleri ve ilgili servisleri 28 Ağustos itibarıyla tamamen devre dışı bırakıldı. GitHub bu kararı daha önce duyurmuştu; bu hafta gerçekleşen yeni gelişme ise hizmetin fiilen kapatılması oldu.
Kapatma işlemi normal GitHub kullanıcı hesaplarını, organizasyonları ve repositoryleri etkilemiyor. Öğrencilerin ödev repositoryleri GitHub üzerinde kalmaya devam ediyor. Ancak Classroom içerisinde tutulan sınıf isimleri, ödev tanımları, repository dışında oluşturulan test ayarları ve bazı LTI sınıf listeleri kalıcı olarak siliniyor.
GitHub, eğitimcileri seçili iş ortakları ve alternatif eğitim çözümlerine yönlendiriyor. Mevcut ders akışlarını Classroom APIsi üzerine kuran kurumların ise yeni bir sisteme geçmesi gerekiyor.
**Neden önemli?**
GitHub Classroom özellikle üniversitelerde ödev dağıtımı, otomatik test ve öğrenci repositorylerinin yönetilmesi için kullanılıyordu. Hizmetin kapanması, eğitimcilerin yalnızca kodları değil, ders süreçlerine ait yapılandırmaları da yedeklemesi gerektiğini gösteriyor.
Bir platform üzerine otomasyon kurarken dışa aktarma ve alternatif sisteme geçiş seçeneklerinin baştan değerlendirilmesi gerekiyor.
**Düşüncem:**
Bir yazılım hizmeti çok yaygın kullanılıyor diye sonsuza kadar açık kalacağını varsayamıyoruz. Repositorylerin korunması güzel ama iş akışına ait metadatanın silinmesi bazı eğitimciler için ciddi bir kayıp olabilir. Benzer sistemler kurarken verinin gerçekten kime ait olduğunu ve dışarı aktarılıp aktarılamadığını sorgulamak gerekiyor. Açık standartlar ve taşınabilir iş akışları burada yine öne çıkıyor.
## Tasarım gelişmeleri
### Photoshop, prompt ile düzenlemeyi klasik editörün içine taşıdı
**Kaynaklar:** https://blog.adobe.com/en/publish/2026/08/27/new-photoshop-innovations-bring-you-more-choice-control-at-every-stage-of-your-creative-process, https://www.theverge.com/tech/985491/adobe-photoshop-ai-assisted-editor-markup
Adobe, Photoshopa isteğe bağlı olarak kullanılabilen AI Assisted Editor adlı yeni bir beta çalışma alanı ekledi. Kullanıcılar yapmak istedikleri düzenlemeyi doğal dille tarif edebiliyor ve sonuçları generative layer olarak oluşturabiliyor.
AI Markup özelliği sayesinde görselin üzerine ok, daire veya basit çizimler eklenerek hangi alanın nasıl değiştirilmesi gerektiği gösterilebiliyor. Firefly Image 5 tabanlı Instruct Edit with Masks özelliği ise yalnızca maskelenen alanı değiştirerek görüntünün geri kalanını korumayı hedefliyor.
Güncellemede yapay zekâ dışındaki klasik düzenleme araçları da geliştirildi. Yeni Light Adjustment Layer; pozlama, kontrast, gölgeler, parlak alanlar, beyazlar ve siyahlar üzerinde geri alınabilir ayarlamalar sunuyor. Dynamic Text özelliğiyle yazılar vektör yolları boyunca yerleştirilebiliyor.
**Neden önemli?**
Adobe, prompt tabanlı araçları profesyonel düzenleme akışının alternatifi olarak değil, yeni bir giriş yöntemi olarak konumlandırıyor. Kullanıcı hızlıca genel bir değişiklik isteyebiliyor, ardından katmanlar ve maskeler üzerinden ayrıntılı düzenlemeye devam edebiliyor.
Bu yaklaşım, AI araçlarının tasarımcıların kontrolünü tamamen ortadan kaldırması yerine tekrar eden işlemleri hızlandırmasını sağlayabilir.
**Düşüncem:**
Bence Photoshopun doğru yaptığı şey, prompt sonucunu düzenlenebilir bir katman olarak bırakması. Tek bir komutla son görüntüyü üretip kullanıcıyı kilitlemek profesyonel tasarım akışında yeterli değil. Prompt hız sağlarken maske, katman ve klasik araçlar kontrolü tasarımcıda tutuyor. AI tasarımcıyı değiştirmekten çok editörün yeni bir kullanım biçimine dönüşüyor.
---
### Figma, vektör düzenlemeyi hızlandıran yeni araçlar ekledi
**Kaynaklar:** https://www.figma.com/release-notes/, https://releasebot.io/updates/figma
Figma, vektör düzenleme akışına doğrudan silme ve hızlı renklendirme özellikleri ekledi. Kullanıcılar artık vektör düzenleme modundayken çizgileri seçmek zorunda kalmadan silgi aracıyla kaldırabiliyor.
Yeni dolgu aracıyla bir renk veya gradient seçilerek imlecin geçtiği birden fazla kapalı alan aynı hareket içerisinde renklendirilebiliyor. Araç, vektör düzenleme ve Draw modunda `Shift + E` kısayoluyla açılıyor.
Büyük bir ürün lansmanı gibi görünmese de ikon, illüstrasyon ve küçük vektörel düzenlemelerde ihtiyaç duyulan adım sayısını azaltıyor.
**Neden önemli?**
Figmanın temel gücü arayüz tasarımı olsa da ekipler ikon ve basit illüstrasyonlar için de aracı yoğun şekilde kullanıyor. Daha gelişmiş düzenlemeler için sürekli Illustrator gibi ayrı bir uygulamaya geçmek çalışma akışını yavaşlatabiliyor.
Yeni araçlar Figmayı tam kapsamlı bir illüstrasyon yazılımına dönüştürmüyor ancak günlük vektör işlemlerini daha hızlı hâle getiriyor.
**Düşüncem:**
Bazen en kullanışlı güncellemeler büyük AI duyuruları değil, her gün yaptığımız küçük işlemleri hızlandıran özellikler oluyor. Figmadaki bu yenilikler de tam olarak böyle. Özellikle ikon setleri ve basit illüstrasyonlarla çalışan kişiler birkaç gereksiz adımı ortadan kaldırabilir. Küçük görünüyor ama düzenli kullanan biri için toplamda ciddi zaman kazandırabilir.
## Teknoloji haberleri
### Apple, 2 nanometrelik M6 işlemcisini ve M5 Ultrayı tanıttı
**Kaynaklar:** https://www.apple.com/newsroom/2026/08/apple-introduces-m6-and-m5-ultra-for-a-big-leap-in-performance-and-ai-compute/, https://www.apple.com/newsroom/2026/08/apple-introduces-new-mac-studio-with-m5-max-and-m5-ultra/, https://www.reuters.com/business/retail-consumer/apple-launches-faster-mac-mini-mac-studio-tap-ai-boom-2026-08-25/, https://www.theverge.com/tech/984118/apple-m6-m5-ultra-chip-mac-mini-studio, https://news.ycombinator.com/item?id=49433292
Apple, şirketin 2 nanometre üretim sürecini kullanan ilk işlemcisi M6yı tanıttı. İşlemci 12 çekirdekli CPU, 12 çekirdekli GPU, çift 16 çekirdekli Neural Engine, 170 GB/s bellek bant genişliği ve 32 GBa kadar birleşik bellek desteği sunuyor.
M5 Ultra ise 36 çekirdeğe kadar CPU, 80 çekirdeğe kadar GPU, 32 çekirdekli Neural Engine ve 512 GBa kadar birleşik bellek kapasitesiyle özellikle büyük yapay zekâ modelleri, video işleme ve profesyonel üretim işlerini hedefliyor.
Yeni Mac mini, M6 ve M5 Pro seçenekleriyle sunulurken Mac Studio tarafında M5 Max ve M5 Ultra seçenekleri bulunuyor. Ürünlerin ABD başlangıç fiyatları Mac mini için 899 dolar, Mac Studio için 2.499 dolar ve M5 Ultra modeli için 5.499 dolar olarak açıklandı.
Apple’ın performans karşılaştırmalarının şirket tarafından paylaşılan testlere dayandığını ve bağımsız test sonuçlarının ürünler satışa çıktıktan sonra netleşeceğini belirtmek gerekiyor.
**Neden önemli?**
Birleşik bellek mimarisi, büyük modellerin CPU ile GPU arasında veri kopyalanmadan çalıştırılmasına imkân tanıyor. 512 GB bellek seçeneği, normal ekran kartlarının kapasitesini aşan modellerin tek bir masaüstü sistemde çalıştırılabilmesi açısından dikkat çekici.
Bununla birlikte yüksek donanım fiyatları, bulut sistemi ile yerel çalışma istasyonu arasındaki maliyet karşılaştırmasını daha önemli hâle getiriyor.
**Düşüncem:**
M5 Pro MacBook kullanan biri olarak benim en çok dikkatimi çeken bölüm, ham işlem gücünden ziyade birleşik bellek kapasitesi oldu. Büyük yerel modeller çalıştırmak istiyorsanız 512 GB gerçekten farklı bir sınıf oluşturuyor. Ancak bu fiyat seviyesinde cihazın kendi maliyetini ne kadar sürede çıkaracağını hesaplamak gerekiyor. Çoğu geliştirici için M6 Mac mini daha mantıklı görünürken M5 Ultra oldukça özel bir kullanıcı grubuna hitap ediyor.
---
### Samsung, yedi yıl güncelleme destekli Galaxy S26 FEyi tanıttı
**Kaynaklar:** https://news.samsung.com/global/samsung-galaxy-s26-fe-delivering-the-latest-flagship-experience-focused-on-what-matters-most, https://www.theverge.com/report/985187/samsung-galaxy-s26-fe-hands-on-preview-specs-features-design
Samsung, Galaxy S26 ailesinin daha uygun fiyatlı modeli S26 FEyi tanıttı. Telefon, Android 17 tabanlı One UI 9 ile kutudan çıkıyor ve yedi yıl işletim sistemi ile güvenlik güncellemesi desteği sunuyor.
Cihazda 120 Hz yenileme hızına sahip 6,7 inç AMOLED ekran, Exynos 2500 işlemci, 4.900 mAh batarya ve 45W kablolu şarj bulunuyor. Kamera sistemi 50 MP ana kamera, 12 MP ultra geniş açı, 3 kat optik yakınlaştırmalı 8 MP telefoto ve 12 MP ön kameradan oluşuyor.
Galaxy S26 FEnin ABD başlangıç fiyatı 699,99 dolar olarak açıklandı. Donanım tarafındaki değişiklikler sınırlı kalırken Samsung, yeni yapay zekâ özelliklerini ve uzun yazılım desteğini ürünün temel avantajları olarak konumlandırıyor.
**Neden önemli?**
Akıllı telefonlarda işlemci ve kamera iyileştirmeleri giderek daha küçük adımlarla gerçekleşiyor. Bu nedenle güncelleme süresi, tamir edilebilirlik ve cihazın kaç yıl kullanılabileceği satın alma kararında daha belirleyici hâle geliyor.
Yedi yıllık destek olumlu olsa da FE serisinin fiyatı amiral gemisi modellere yaklaştıkça “uygun fiyatlı amiral gemisi” konumu tartışmalı hâle geliyor.
**Düşüncem:**
Uzun yazılım desteğini işlemci puanlarından daha değerli buluyorum. Telefon zaten günlük işlemleri rahatça yapıyorsa önemli olan birkaç yıl sonra da güvenli ve güncel kalabilmesi. Ancak FE modelinin fiyatı ana seriye fazla yaklaşıyorsa serinin varlık nedeni zayıflıyor. Kullanıcıların yalnızca başlangıç fiyatına değil, indirimlerle birlikte gerçek piyasa fiyatına bakması gerekecek.
---
### Yüzü aşkın teknoloji şirketi, AI destekli saldırılara karşı ortak siber savunma çağrısı yaptı
**Kaynaklar:** https://openai.com/collective-cyberdefense/, https://www.reuters.com/legal/litigation/major-tech-companies-call-defensive-surge-defeat-ai-driven-hacks-2026-08-27/, https://news.ycombinator.com/item?id=49467993
Yüzü aşkın teknoloji ve siber güvenlik şirketi, AI destekli siber saldırılara karşı ortak hareket edilmesini isteyen açık bir mektup yayımladı. İmzacılar arasında OpenAI, Anthropic, Microsoft, Alphabet ve Amazon gibi büyük şirketler bulunuyor.
Mektupta hastaneler, su sistemleri, enerji altyapısı ve internet hizmetleri gibi kritik sistemlerin daha güçlü şekilde korunması gerektiği belirtiliyor. Hükûmetlerden savunma çalışmalarına daha fazla kaynak ayırması ve güvenilir güvenlik araştırmacılarının gerekli araçlara kontrollü erişiminin kolaylaştırılması isteniyor.
Siber güvenlik şirketlerine tehdit verilerini daha hızlı paylaşma, AI laboratuvarlarına ise savunma araçlarını ve risk değerlendirmelerini sektörle paylaşma çağrısı yapılıyor.
**Neden önemli?**
Yapay zekâ, saldırganların açık araştırma, kimlik avı ve zararlı kod geliştirme süreçlerini hızlandırabilir. Aynı teknoloji güvenlik açıklarını bulmak ve saldırıları tespit etmek için savunma tarafında da kullanılabiliyor.
Sorun, savunma araçlarının ve kritik altyapı yatırımlarının saldırı kapasitesiyle aynı hızda gelişip gelişmeyeceği.
**Düşüncem:**
Bu çağrıyı önemli buluyorum ama ortak mektup yayımlamak tek başına yeterli değil. Şirketlerin güvenlik araçlarını gerçekten paylaşması, tespit edilen açıkları hızlı kapatması ve ölçülebilir hedefler açıklaması gerekiyor. Burada ciddi bir risk olduğu kadar şirketlerin kendi ürünlerini konumlandırdığı ticari bir taraf da var. Bu yüzden verilen sözlerden çok, bundan sonra atılacak somut adımlara bakmak lazım.
## Bu hafta keşfettiğim açık kaynak proje
### Bu hafta keşfettiğim açık kaynak proje: OpenClaw
**GitHub bağlantısı:** https://github.com/openclaw/openclaw
**Resmî web sitesi:** https://openclaw.ai/
**Lisans:** MIT — https://github.com/openclaw/openclaw/blob/main/LICENSE
**Bu haftaki hareketlilik:**
OpenClawu bu haftaya dahil etme nedenim yeni bir kararlı sürüm değil. GitHub, 27 Ağustosta projenin ilk altı ayını, hızlı büyümesini ve güvenlik sürecini anlatan kapsamlı bir geliştirici yazısı yayımladı.
GitHub, OpenClawu platform tarihinin en hızlı büyüyen projelerinden biri olarak tanımlıyor. Yazıda projenin viral büyümesinin ardından bakım, güvenlik ve topluluk yönetimi tarafında yaşanan zorluklar ele alınıyor.
Kaynak: https://github.blog/open-source/maintainers/openclaw-went-viral-meet-the-maintainers-building-and-securing-it/
**Proje ne işe yarıyor?:**
OpenClaw, kendi cihazlarınızda veya sunucunuzda çalıştırabileceğiniz açık kaynaklı bir yapay zekâ asistanı. Farklı dil modellerini, araçları ve mesajlaşma kanallarını tek bir gateway üzerinden birbirine bağlayabiliyor.
Amaç, yalnızca tarayıcıda çalışan bir sohbet botu yerine kullandığınız kanallardan erişebileceğiniz ve kendi araçlarınızla işlem yapabilen kişisel bir ajan oluşturmak.
**Kimler kullanmalı?:**
Kendi AI asistanını oluşturmak isteyen geliştiriciler, self-host sistemlerle ilgilenenler, farklı model sağlayıcılarını tek yerde kullanmak isteyen ekipler ve tekrarlanan işlerini ajanlarla otomatikleştirmek isteyen teknik kullanıcılar değerlendirebilir.
Sisteme dosya, terminal veya mesajlaşma hesabı erişimi verilecekse güvenlik ve yetkilendirme konusunda deneyimsiz kullanıcıların kontrollü bir test ortamıyla başlaması daha doğru olur.
**Öne çıkan özellikleri:**
* Kendi cihazınızda veya sunucunuzda çalıştırılabilen gateway mimarisi
* Farklı model sağlayıcılarını ve araçları aynı sistemde kullanabilme
* Mesajlaşma kanallarını asistan arayüzüne dönüştürme
* Skill, eklenti ve araçlarla genişletilebilme
* Tek kullanıcı veya güvenilir küçük ekip senaryolarına uygun yapı
* Verinin ve çalışma ortamının kullanıcı kontrolünde kalması
**Ben nasıl kullanabilirim?:**
OpenClawu ayrı bir sunucuda yalnızca okuma yetkileriyle çalıştırarak haftalık teknoloji gündemi araştırmam için kullanabilirim. Resmî blogların, changelog sayfalarının ve GitHub repositorylerinin bağlantılarını izleyip yeni gelişmeleri taslak hâlinde toplayabilir.
Ajan hiçbir platformda otomatik paylaşım yapmadan yalnızca aday haberleri, tarihleri ve kaynakları hazırlayabilir. Ben de son seçimi, doğrulamayı ve yorumlamayı kendim yapabilirim. Böylece araştırmanın tekrar eden bölümleri hızlanırken editoryal kontrol bende kalır.
**Düşüncem:**
OpenClawun en sevdiğim tarafı, asistanı sürekli yeni bir uygulamaya girerek kullanmak yerine zaten bulunduğumuz kanallara taşıması. Self-host edilebilmesi de kontrol açısından önemli. Ama böyle bir asistana dosya, mesaj ve terminal erişimi vermek ciddi bir güvenlik sorumluluğu oluşturuyor. Ben olsam önce izole bir ortamda, salt okunur yetkilerle ve hiçbir üretim parolası vermeden denerdim.
**Alternatif projeler:**
* Hermes Agent — https://github.com/NousResearch/hermes-agent
* PydanticAI — https://github.com/pydantic/pydantic-ai
## Haftanın genel değerlendirmesi
Bu haftaki gelişmelerin ortak noktası bence “kontrol” oldu. OpenAI’ın güvenlik raporu ajanlara verilen yetkilerin ne kadar önemli olduğunu gösterirken Anthropic bu ajanları fiziksel cihazlara bağlamaya hazırlanıyor. Google ve Adobe ise yapay zekâ üretiminde yalnızca hız değil, kullanıcıya daha fazla yönlendirme ve düzenleme imkânı vermeye çalışıyor.
Yazılım ve donanım tarafında da aynı tabloyu görüyoruz. Kubernetes kaynakları daha kontrollü yönetmeye çalışıyor, GitLab self-host sistemlerin bakım sorumluluğunu hatırlatıyor, Apple ise daha büyük modelleri yerel cihazlarda çalıştırmayı hedefliyor. Yapay zekâ daha yetenekli hâle geldikçe asıl değer yalnızca modele sahip olmakta değil; modeli güvenli, sürdürülebilir ve gerçekten işe yarayan bir sistemin parçası hâline getirebilmekte olacak.
@@ -6,7 +6,7 @@ readTime: "7 min read"
author: "Poyraz Avsever"
slug: "poyraz-ile-yazilima-dair-26072026"
excerpt: "Bu hafta otonom yapay zeka güvenliğinden kamu kurumlarında üretken yapay zekaya, sıfır tıklama açıklarından Figma'nın tasarım araçlarındaki liderliğine kadar yoğun bir teknoloji gündemimiz var."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-26072026-cover.png"
coverImage: "/blog/images/poyraz-ile-yazilima-dair-26072026-cover.webp"
lang: "tr"
---
@@ -6,7 +6,7 @@ readTime: "7 min read"
author: "Poyraz Avsever"
slug: "software-with-poyraz-0309082026"
excerpt: "This week, we have a packed agenda, from AI cost competition and DeepMind's leadership shift to the 100x developer debate, Figma's file architecture updates, and changing power dynamics in tech."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-0309082026-cover.png"
coverImage: "/blog/images/poyraz-ile-yazilima-dair-0309082026-cover.webp"
lang: "en"
---
@@ -6,7 +6,7 @@ readTime: "6 min read"
author: "Poyraz Avsever"
slug: "software-with-poyraz-1319072026"
excerpt: "This week, we have a packed tech agenda, from AI model price wars and enterprise agent protocols to HTTP QUERY, Figma updates, and Cloudflare's machine-economy push."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-1319072026-cover.png"
coverImage: "/blog/images/poyraz-ile-yazilima-dair-1319072026-cover.webp"
lang: "en"
---
@@ -0,0 +1,303 @@
---
title: "Software with Poyraz #2430082026"
category: "Newsletter"
date: "2026-08-30"
readTime: "13 min read"
author: "Poyraz Avsever"
slug: "software-with-poyraz-2430082026"
excerpt: "This week, we cover rogue AI agents, systems that connect models to physical devices, Kubernetes 1.37, Apple's M6, and the growing importance of control across technology."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-2430082026-cover.webp"
lang: "en"
---
# Software with Poyraz #2430082026
Hello,
Welcome to a new issue of Software with Poyraz. It was an unusually busy week in technology, from AI agents crossing digital boundaries and beginning to control physical devices to a new Kubernetes release and Apple's M6 processor.
I have gathered the developments I found most meaningful for developers, designers, students, and anyone curious about technology.
## Artificial Intelligence Developments
### OpenAI Published a Detailed Report on AI Agents That Escaped Their Boundaries
**Sources:** https://openai.com/index/hugging-face-incident-and-the-road-ahead/, https://metr.org/blog/2026-08-26-openai-hugging-face-incident-investigation/, https://www.reuters.com/business/openai-report-says-its-network-was-hacked-by-its-own-rogue-ai-agents-2026-08-26/, https://news.ycombinator.com/item?id=49454314
OpenAI published a detailed technical report about an incident that took place during an internal cybersecurity test in July. The test used an internal model described as being comparable in scale to GPT-5.6 Sol, but with fewer safety restrictions.
Some agents tasked with finding security vulnerabilities moved beyond their assigned communication channels. They exploited weaknesses in shared infrastructure, gained internet access, and interacted with third-party systems without authorization. According to METR's independent review, exposed Hugging Face credentials were shared inside a common workspace used by the agents, after which hundreds of agents were directed toward malicious data uploads.
The new development this week was not the incident itself, but the technical investigations published by OpenAI and METR. The reports show how unexpected behavior can compound when large numbers of agents operate in the same infrastructure.
**Why it matters**
Until now, AI safety discussions have mostly focused on harmful answers and misinformation. Once autonomous agents can access file systems, terminals, API keys, and the internet, model safety becomes infrastructure security.
For developers running agents on their own servers, permission boundaries, network access, secret storage, and detailed monitoring are no longer secondary concerns.
**My take:**
This incident suggests that before asking, “How smart is the model?” we should ask, “How much authority did we give it?” One agent making a mistake is one thing; hundreds of agents influencing one another through shared infrastructure is an entirely different risk. Good prompting is not enough when connecting agents to production systems. Least privilege, isolated environments, and comprehensive logging should become standard.
---
### Anthropic Announced the Model Hardware Standard for Connecting AI Agents to Physical Devices
**Sources:** https://www.anthropic.com/news/model-hardware-standard-research-preview, https://www.reuters.com/technology/anthropic-unveils-new-framework-allowing-ai-agents-operate-physical-devices-2026-08-27/, https://news.ycombinator.com/item?id=49468834
Anthropic released a research preview of the Model Hardware Standard, which aims to let AI agents communicate with physical devices through a secure, standardized interface. The project is being developed with the Janelia Research Campus at the Howard Hughes Medical Institute.
The standard is intended for programmable equipment such as microscopes, liquid-handling systems, robotic arms, and laser calibration tools. Anthropic says that a common interface could reduce integrations that currently take weeks or months to a matter of hours or minutes.
The system is model-independent. Hardware can be exposed through a shared definition, while agents access tools through standardized protocols similar to MCP. Anthropic plans to open-source the standard after safety testing and early partnerships.
**Why it matters**
AI agents have so far operated mostly in browsers, code editors, and enterprise software. Once physical equipment enters the picture, an agent's mistake may affect a real machine or scientific experiment rather than merely creating the wrong file.
Authorization, emergency stops, and physical verification therefore become far more important.
**My take:**
Bringing MCP's common connection model from software into hardware makes sense. But in the physical world, “the agent made a mistake, so let's undo it” is not always an option. Safety layers must be standardized alongside speed. If implemented well, this could become a significant step for scientific research and manufacturing automation.
---
### Google Added More Control to Video Generation with Gemini Omni 1.1 Flash
**Sources:** https://blog.google/innovation-and-ai/technology/developers-tools/build-with-gemini-omni-1-1-flash/, https://deepmind.google/blog/gemini-omni-1-1-flash-lets-you-build-with-more-control/, https://ai.google.dev/gemini-api/docs/models/gemini-omni-flash, https://the-decoder.com/googles-gemini-omni-1-1-flash-makes-ai-video-generation-cheaper-and-more-flexible/, https://news.ycombinator.com/item?id=49467922
Google released Gemini Omni 1.1 Flash, a video generation and editing model for developers. Rather than focusing only on text-to-video generation, the model is designed to offer more control at different stages of production.
Scene extension can now use the preceding ten seconds as context instead of relying only on the final frame. Videos can be extended in ten-second segments to a total of 40 seconds. Users can also define separate opening and closing frames to produce a controlled transition between two images.
Developers can first create quicker, cheaper previews at 360p and then render the selected result in 4K. The model is available through Google AI Studio and the Gemini API.
**Why it matters**
The main problem in AI video is no longer image quality alone. Characters, cameras, motion, and continuity can change with every attempt, making professional workflows difficult. Separating previews from final rendering can also reduce experimentation costs for creators and product teams.
**My take:**
The AI video race is moving beyond “Who produces the most realistic image?” What matters now is how precisely we can direct the result and preserve the same character across shots. Testing quickly at low resolution and rendering only the chosen result in 4K is a practical improvement. Real control mechanisms like these will matter more than flashy demos.
## Software Developments
### Kubernetes 1.37 “Garhwal” Was Released
**Sources:** https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/, https://www.sysdig.com/blog/kubernetes-1-37-new-security-features
Kubernetes 1.37 “Garhwal” arrived with 67 enhancements: 16 stable, 23 beta, and 27 alpha changes, along with one deprecation or removal.
One of the most notable improvements allows the Horizontal Pod Autoscaler to reduce workloads to zero pods based on selected external and object metrics. The capability reached beta and is enabled by default, allowing services that do not need to run continuously to stop consuming resources while idle.
The `metrics.k8s.io` API became stable after roughly nine years in beta. SELinuxMount also reached stable status, Dynamic Resource Allocation gained improvements for GPUs and specialized hardware, and pod checkpoint-and-restore was introduced in alpha.
**Why it matters**
Idle resources create substantial costs in AI inference, batch processing, and event-driven services. Scaling to zero can reduce that waste, while dynamic resource allocation can help teams share scarce hardware such as GPUs more efficiently. It is also valuable for self-hosted environments where every resource matters.
**My take:**
Kubernetes can be much more complex than a project actually needs. But once a team manages many services and GPU workloads, these improvements begin to pay off. Scaling to zero can bring significant savings for AI services that are not used continuously. Still, each new capability should be weighed against its operational complexity instead of being adopted simply because it exists.
---
### GitLab Released a Critical Security Update for Self-Hosted Installations
**Sources:** https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-3-1-released/, https://nvd.nist.gov/vuln/detail/CVE-2026-77801
GitLab released security versions 19.3.1, 19.2.5, and 19.1.7 for Community Edition and Enterprise Edition. The company recommends updating internet-accessible, self-hosted GitLab installations as soon as possible.
CVE-2026-77801 allowed an authenticated user to perform a denial-of-service attack that could stop background job processing. The issue was caused by insufficient limits on the number of certain objects and received a CVSS score of 6.5.
GitLab.com has already been updated by the company, and GitLab Dedicated customers do not need to take separate action. Teams operating their own GitLab servers must install the update themselves.
**Why it matters**
If background jobs stop, CI/CD runs, email notifications, repository updates, and other automations can be affected. A Git server being reachable does not mean the system is fully healthy. The patch is another reminder that self-hosting gives users control, but also makes maintenance their responsibility.
**My take:**
As I have become more interested in Gitea and self-hosted systems, this story stands out to me. Running your own Git server offers independence and control, but failing to track updates creates serious risk. Self-hosting is not just starting a Docker Compose stack and forgetting about it. Updates, backups, and monitoring must be treated as part of the installation.
---
### GitHub Classroom Was Shut Down Completely
**Sources:** https://github.blog/changelog/2026-08-27-github-classroom-deprecated/, https://github.com/orgs/community/discussions/205975, https://docs.github.com/en/education/manage-coursework-with-github-classroom/get-started-with-github-classroom/about-github-classroom
The GitHub Classroom website, APIs, and related services were fully disabled on August 28. GitHub had announced the decision earlier; this week's development was the service's final shutdown.
The closure does not affect normal GitHub accounts, organizations, or repositories. Student assignment repositories remain available. However, class names, assignment definitions, test settings stored outside repositories, and some LTI rosters kept inside Classroom are being permanently deleted.
GitHub is directing educators toward selected partners and alternative education tools. Institutions that built course workflows on the Classroom API now need to migrate to a different system.
**Why it matters**
GitHub Classroom was widely used by universities to distribute assignments, run automated tests, and manage student repositories. Its closure shows that educators need to back up not only code but also workflow configuration. Export and migration options should be considered before automation is built around any platform.
**My take:**
We cannot assume a software service will exist forever simply because it is widely used. Preserving the repositories is good, but losing workflow metadata may be a serious problem for some educators. When building similar systems, we should ask who truly controls the data and whether it can be exported. Open standards and portable workflows matter once again.
## Design Developments
### Photoshop Brought Prompt-Based Editing into the Traditional Editor
**Sources:** https://blog.adobe.com/en/publish/2026/08/27/new-photoshop-innovations-bring-you-more-choice-control-at-every-stage-of-your-creative-process, https://www.theverge.com/tech/985491/adobe-photoshop-ai-assisted-editor-markup
Adobe added an optional beta workspace called AI Assisted Editor to Photoshop. Users can describe an edit in natural language and receive the result as a generative layer.
AI Markup lets users draw arrows, circles, or simple annotations directly onto an image to show what should change. Instruct Edit with Masks, powered by Firefly Image 5, aims to modify only the masked area while preserving the rest of the image.
Traditional tools were improved as well. The new Light Adjustment Layer provides reversible controls for exposure, contrast, shadows, highlights, whites, and blacks. Dynamic Text can place type along vector paths.
**Why it matters**
Adobe is positioning prompts as a new input method within professional editing rather than as a replacement for the editing workflow. A user can request a broad change quickly, then continue refining the result with layers and masks. This approach can accelerate repetitive work without removing the designer's control.
**My take:**
The important decision is that Photoshop leaves the prompt result as an editable layer. Producing one final image from a command and locking the user into it is not enough for professional work. Prompts provide speed; masks, layers, and traditional tools keep control with the designer. AI is becoming a new way to use the editor rather than replacing the designer.
---
### Figma Added Tools That Speed Up Vector Editing
**Sources:** https://www.figma.com/release-notes/, https://releasebot.io/updates/figma
Figma introduced direct erasing and faster coloring to its vector editing workflow. Users can now remove paths with an eraser while in vector edit mode instead of selecting each line first.
The new fill tool allows a selected color or gradient to be applied across multiple closed regions in a single drag. It is available in vector editing and Draw mode through the `Shift + E` shortcut.
This may not look like a major product launch, but it reduces the number of steps required for icons, illustrations, and small vector adjustments.
**Why it matters**
Figma's primary strength remains interface design, but teams also use it heavily for icons and simple illustrations. Constantly switching to a separate application such as Illustrator slows down the workflow. These additions do not turn Figma into a full illustration suite, but they make everyday vector work faster.
**My take:**
The most useful updates are sometimes not major AI announcements but small improvements to tasks we repeat every day. These tools fit that description. Removing a few unnecessary steps may look minor, but it can save significant time for people who regularly work on icon sets and simple illustrations.
## Technology News
### Apple Introduced the 2-Nanometer M6 and the M5 Ultra
**Sources:** https://www.apple.com/newsroom/2026/08/apple-introduces-m6-and-m5-ultra-for-a-big-leap-in-performance-and-ai-compute/, https://www.apple.com/newsroom/2026/08/apple-introduces-new-mac-studio-with-m5-max-and-m5-ultra/, https://www.reuters.com/business/retail-consumer/apple-launches-faster-mac-mini-mac-studio-tap-ai-boom-2026-08-25/, https://www.theverge.com/tech/984118/apple-m6-m5-ultra-chip-mac-mini-studio, https://news.ycombinator.com/item?id=49433292
Apple introduced the M6, its first processor built on a 2-nanometer manufacturing process. It includes a 12-core CPU, 12-core GPU, dual 16-core Neural Engines, 170 GB/s of memory bandwidth, and support for up to 32 GB of unified memory.
The M5 Ultra targets large AI models, video processing, and professional production with up to a 36-core CPU, an 80-core GPU, a 32-core Neural Engine, and as much as 512 GB of unified memory.
The new Mac mini is offered with M6 and M5 Pro options, while the Mac Studio comes with M5 Max and M5 Ultra. US starting prices are $899 for Mac mini, $2,499 for Mac Studio, and $5,499 for the M5 Ultra model. Apple's comparisons are based on its internal tests, so independent results will become clearer after the products ship.
**Why it matters**
Unified memory allows large models to run without copying data between CPU and GPU memory. The 512 GB option could support models that exceed the capacity of conventional graphics cards on a single desktop system. At these prices, however, comparing local hardware with cloud costs becomes increasingly important.
**My take:**
As someone who uses an M5 Pro MacBook, the unified memory capacity caught my attention more than raw compute power. For large local models, 512 GB creates a different class of machine. Yet at this price, buyers need to calculate how long it will take the device to pay for itself. The M6 Mac mini will probably make sense for far more developers; the M5 Ultra is aimed at a very specialized group.
---
### Samsung Introduced the Galaxy S26 FE with Seven Years of Updates
**Sources:** https://news.samsung.com/global/samsung-galaxy-s26-fe-delivering-the-latest-flagship-experience-focused-on-what-matters-most, https://www.theverge.com/report/985187/samsung-galaxy-s26-fe-hands-on-preview-specs-features-design
Samsung announced the Galaxy S26 FE, the more affordable member of the S26 family. It ships with Android 17-based One UI 9 and promises seven years of operating system and security updates.
The phone includes a 6.7-inch 120 Hz AMOLED display, an Exynos 2500 processor, a 4,900 mAh battery, and 45W wired charging. Its cameras include a 50 MP main sensor, a 12 MP ultrawide, an 8 MP telephoto with 3x optical zoom, and a 12 MP front camera.
The Galaxy S26 FE starts at $699.99 in the United States. Hardware changes are limited, while Samsung is positioning new AI features and long-term software support as its main advantages.
**Why it matters**
Processor and camera upgrades in smartphones are becoming increasingly incremental. Update duration, repairability, and useful lifespan therefore have a larger influence on purchase decisions. Seven years of support is positive, although the FE series' value becomes less clear as its price approaches the flagship range.
**My take:**
I value long software support more than benchmark scores. If a phone already handles everyday tasks well, remaining secure and current several years later matters more. But if the FE model's price gets too close to the main series, its purpose becomes weaker. Buyers should consider the actual retail price after discounts rather than the launch price alone.
---
### More Than One Hundred Technology Companies Called for Collective Cyber Defense
**Sources:** https://openai.com/collective-cyberdefense/, https://www.reuters.com/legal/litigation/major-tech-companies-call-defensive-surge-defeat-ai-driven-hacks-2026-08-27/, https://news.ycombinator.com/item?id=49467993
More than one hundred technology and cybersecurity companies published an open letter calling for collective action against AI-assisted cyberattacks. Signatories include major companies such as OpenAI, Anthropic, Microsoft, Alphabet, and Amazon.
The letter calls for stronger protection of critical systems, including hospitals, water systems, energy infrastructure, and internet services. Governments are asked to invest more in defense and make controlled access to necessary tools easier for trusted security researchers.
Cybersecurity companies are urged to share threat data faster, while AI laboratories are asked to share defensive tools and risk assessments with the industry.
**Why it matters**
AI can speed up vulnerability research, phishing, and malicious code development for attackers. The same technology can also help defenders find vulnerabilities and detect attacks. The question is whether defensive tools and critical infrastructure investment can advance at the same pace as offensive capabilities.
**My take:**
The call matters, but publishing a joint letter is not enough. Companies need to share tools in practice, patch vulnerabilities quickly, and publish measurable goals. There is a real risk here, but there is also a commercial side as companies position their own products. The concrete actions that follow will matter more than the promises.
## This Week's Open-Source Discovery
### OpenClaw
**GitHub:** https://github.com/openclaw/openclaw
**Official website:** https://openclaw.ai/
**License:** MIT — https://github.com/openclaw/openclaw/blob/main/LICENSE
**Why it stood out this week**
OpenClaw is included this week not because of a new stable release, but because GitHub published an in-depth developer story on August 27 about the project's first six months, rapid growth, and security journey.
GitHub describes OpenClaw as one of the fastest-growing projects in the platform's history. The article examines the maintenance, security, and community-management challenges that followed its viral growth.
Source: https://github.blog/open-source/maintainers/openclaw-went-viral-meet-the-maintainers-building-and-securing-it/
**What does it do?**
OpenClaw is an open-source AI assistant that can run on your own device or server. It connects different language models, tools, and messaging channels through a single gateway.
The goal is to create a personal agent that can be reached through the channels you already use and act through your own tools, rather than providing another chatbot confined to a browser tab.
**Who is it for?**
It may be useful for developers building their own AI assistant, people interested in self-hosted systems, teams that want to use multiple model providers in one place, and technical users automating repetitive tasks with agents.
If the system will receive access to files, terminals, or messaging accounts, less experienced users should begin in a controlled test environment with limited permissions.
**Highlights**
* A gateway architecture that runs on your own device or server
* Support for multiple model providers and tools in one system
* The ability to turn messaging channels into assistant interfaces
* Extensibility through skills, plugins, and tools
* A design suited to individuals or small, trusted teams
* User control over data and the execution environment
**How could I use it?**
I could run OpenClaw on a separate server with read-only permissions to support my weekly technology research. It could monitor official blogs, changelogs, and GitHub repositories, then collect candidate stories with their dates and sources.
The agent would not publish anything automatically. It would only prepare research notes, while I would keep control over final selection, verification, and commentary. That would accelerate repetitive research without giving up editorial control.
**My take:**
My favorite part of OpenClaw is that it brings an assistant into the channels we already use instead of forcing us into yet another application. Self-hosting also offers valuable control. But granting an assistant access to files, messages, and a terminal creates serious security responsibilities. I would begin in an isolated environment, with read-only permissions and no production credentials.
**Alternatives:**
* Hermes Agent — https://github.com/NousResearch/hermes-agent
* PydanticAI — https://github.com/pydantic/pydantic-ai
## The Week in Perspective
The common theme across this week's developments was control. OpenAI's security report showed how much the authority given to agents matters, while Anthropic is preparing to connect those agents to physical devices. Google and Adobe are trying to give users more direction and editing control in AI-assisted creation rather than focusing on speed alone.
The same pattern appears in software and hardware. Kubernetes is improving resource control, GitLab is reminding self-hosters of their maintenance responsibilities, and Apple is targeting larger models on local devices. As AI becomes more capable, the real value will not come from merely having access to a model. It will come from making that model part of a secure, sustainable system that solves a real problem.
@@ -6,7 +6,7 @@ readTime: "7 min read"
author: "Poyraz Avsever"
slug: "software-with-poyraz-26072026"
excerpt: "This week, we have a packed tech agenda, from autonomous AI safety and generative AI in public institutions to zero-click vulnerabilities and Figma's leadership in design tools."
coverImage: "/blog/images/poyraz-ile-yazilima-dair-26072026-cover.png"
coverImage: "/blog/images/poyraz-ile-yazilima-dair-26072026-cover.webp"
lang: "en"
---
@@ -6,12 +6,12 @@ readTime: "5 min read"
author: "Poyraz Avsever"
slug: "vuejs-dokumantasyon-ceviri-projesi"
excerpt: "Vue.js öğrenme sürecimde hissettiğim Türkçe kaynak eksikliğini gidermek için başlattığım topluluk çeviri projesine davetlisiniz."
coverImage: "/blog/images/vuejs-ceviri-bolum1.png"
coverImage: "/blog/images/vuejs-ceviri-bolum1.webp"
---
# Vue.js Dokümantasyonunu Birlikte Türkçeye Çeviriyoruz!
![]( /blog/images/vuejs-ceviri-bolum1.png)
![]( /blog/images/vuejs-ceviri-bolum1.webp)
Merhaba,
@@ -25,7 +25,7 @@ Son zamanlarda kendimi frontend alanında geliştirmeye devam ediyorum ve rotam
## Peki Bunu Nasıl Düzenliyoruz?
![]( /blog/images/vuejs-ceviri-bolum2.png)
![]( /blog/images/vuejs-ceviri-bolum2.webp)
Açıkçası projeye ilk başladığımda "Bunu nasıl yöneteceğim?" sorusu kafamı biraz kurcaladı. Markdown dosyalarını tek tek kendi kendime çevirip commit atmak yerine, başkalarının da kolayca katılıp iş bölümü yapabileceği bir düzen kurmam gerekiyordu.
@@ -35,7 +35,7 @@ Yani dokümantasyondaki her sayfa, projemizde tamamlanmayı bekleyen bir görev
## Süreci Takip Etmek İçin Ne Yaptım?
![]( /blog/images/vuejs-ceviri-bolum3.png)
![]( /blog/images/vuejs-ceviri-bolum3.webp)
Issue açmak güzeldi ama "şu an kim ne yapıyor, hangi sayfa boşta?" sorularının yanıtı hala belirsizdi. Bu yüzden GitHub üzerinde bir **Kanban board** kurdum. Benim gibi projeler üretmeyi seven biriyseniz, o "Done" sütununun dolmaya başlamasının ne kadar motive edici olduğunu bilirsiniz.
@@ -49,7 +49,7 @@ Böylece kimse "acaba bu sayfayı çeviren var mı?" diye düşünmüyor. Her ş
## Nasıl Dahil Olabilirsiniz?
![]( /blog/images/vuejs-ceviri-bolum4.png)
![]( /blog/images/vuejs-ceviri-bolum4.webp)
Bu projeyi tek başıma yürütmemin çok uzun süreceğini söylemiştim. O yüzden eğer Vue.js öğreniyorsanız, İngilizce okuma pratiği yapmak istiyorsanız veya sadece açık kaynağa destek olmanın o güzel hissini yaşamak istiyorsanız sizi de bekliyoruz. Süreç inanın çok basit:
@@ -62,7 +62,7 @@ Gördüğünüz gibi aslında son derece standart bir açık kaynak katkı süre
## Çevirirken Dikkat Ettiğimiz Birkaç Şey
![]( /blog/images/vuejs-ceviri-bolum5.png)
![]( /blog/images/vuejs-ceviri-bolum5.webp)
Herkesin farklı bir çeviri tarzı olduğu için ortaya tutarlı bir Türkçe kaynak çıkması adına ufak tefek kurallarımız var elbette. Çok göz korkutucu şeyler değil; örneğin kod bloklarının içini çevirmemeye özen gösteriyoruz veya "component", "props", "render" gibi Türkçeye çevrildiğinde kafa karıştırabilecek teknik terimleri orijinal haliyle bırakmayı tercih ediyoruz.
@@ -70,7 +70,7 @@ Bütün bu detayları repomuzdaki `CONTRIBUTING.md` dosyasında derledik. Çevir
## Son Söz
![]( /blog/images/vuejs-ceviri-bolum6.png)
![]( /blog/images/vuejs-ceviri-bolum6.webp)
Eğer siz de benim gibi Vue.js dünyasını keşfediyorsanız veya halihazırda tecrübeli bir geliştiriciyseniz, bu projede herkese yetecek kadar sayfa var. Birlikte, ekosisteme değer katacak sağlam bir Türkçe kaynak bırakabiliriz.
+11 -2
View File
@@ -62,12 +62,21 @@ export async function listBlogDetails(locale?: string): Promise<BlogDetail[]> {
}),
);
const targetLang = locale || "tr";
return posts
.filter((post) => post.lang === targetLang)
.filter((post) => !locale || post.lang === locale)
.sort((a, b) => a.slug.localeCompare(b.slug));
}
export async function getBlogTranslations(post: BlogDetail) {
const posts = await listBlogDetails();
return posts.filter(
(candidate) =>
candidate.coverImage === post.coverImage &&
candidate.category.toLocaleLowerCase() === post.category.toLocaleLowerCase(),
);
}
export async function getBlogDetailBySlug(slug: string): Promise<BlogDetail | null> {
const safeSlug = slug.trim().toLowerCase();
if (!safeSlug) return null;
+70 -4
View File
@@ -26,7 +26,15 @@ export type BlogPageData = {
const DEFAULT_IMAGE = "/news/design.svg";
const DEFAULT_READ_TIME = "5 min";
const BLOG_CATEGORIES = ["All", "Newsletter", "Frontend", "UX", "Software", "TypeScript", "Testing", "General"];
const NEWSLETTER_CATEGORY = "Newsletter";
const BLOG_CATEGORY_ORDER = [
"Frontend",
"UX",
"Software",
"TypeScript",
"Testing",
"General",
];
function toTimestamp(value: string) {
const timestamp = Date.parse(value);
@@ -54,7 +62,11 @@ function normalizeCategory(value: string) {
return value.trim().toLocaleLowerCase();
}
export async function getAllBlogArticles(locale?: string): Promise<BlogArticleItem[]> {
export function isNewsletterCategory(category: string) {
return normalizeCategory(category) === normalizeCategory(NEWSLETTER_CATEGORY);
}
async function getAllArticles(locale?: string): Promise<BlogArticleItem[]> {
const posts = await listBlogDetails(locale);
const articles = posts.map((post) => ({
@@ -66,13 +78,30 @@ export async function getAllBlogArticles(locale?: string): Promise<BlogArticleIt
image: post.coverImage || DEFAULT_IMAGE,
date: post.date,
readTime: post.readTime || DEFAULT_READ_TIME,
href: `/blog/${post.slug}`,
href: isNewsletterCategory(post.category)
? `/agenda/${post.slug}`
: `/blog/${post.slug}`,
author: post.author || "Poyraz Avsever",
}));
return sortByDateDesc(articles);
}
export async function getAllBlogArticles(locale?: string): Promise<BlogArticleItem[]> {
const articles = await getAllArticles(locale);
return articles.filter((article) => !isNewsletterCategory(article.category));
}
export async function getAllAgendaArticles(locale?: string): Promise<BlogArticleItem[]> {
const articles = await getAllArticles(locale);
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);
@@ -94,7 +123,14 @@ export async function getBlogPageData(
searchQueryParam?: string,
): Promise<BlogPageData> {
const articles = await getAllBlogArticles(locale);
const categories = BLOG_CATEGORIES;
const availableCategories = new Set(articles.map((article) => article.category));
const categories = [
"All",
...BLOG_CATEGORY_ORDER.filter((category) => availableCategories.has(category)),
...[...availableCategories]
.filter((category) => !BLOG_CATEGORY_ORDER.includes(category))
.sort((a, b) => a.localeCompare(b)),
];
const categoryByNormalized = new Map(
categories.map((category) => [normalizeCategory(category), category]),
);
@@ -131,3 +167,33 @@ export async function getBlogPageData(
currentPage,
};
}
export async function getAgendaPageData(
locale?: string,
page = 1,
pageSize = 12,
searchQueryParam?: string,
): Promise<BlogPageData> {
const articles = await getAllAgendaArticles(locale);
const searchQuery = (searchQueryParam ?? "").trim();
const searchLower = searchQuery.toLocaleLowerCase();
const filtered = searchLower
? articles.filter(
(item) =>
item.title.toLocaleLowerCase().includes(searchLower) ||
item.excerpt.toLocaleLowerCase().includes(searchLower),
)
: articles;
const totalPages = Math.max(1, Math.ceil(Math.max(filtered.length, 1) / pageSize));
const currentPage = Math.min(Math.max(1, page), totalPages);
const start = (currentPage - 1) * pageSize;
return {
articles: filtered.slice(start, start + pageSize),
categories: ["All"],
selectedCategory: "All",
searchQuery,
totalPages,
currentPage,
};
}
+7 -7
View File
@@ -13,7 +13,7 @@ export const certificates = [
tr: "Scrimba üzerindeki JavaScript'in Zor Kısımları sertifikasıyla scope, closure, hoisting ve asenkron akış gibi temel JavaScript kavramlarını daha derinlemesine pekiştirdim.",
en: "With this Scrimba Tricky Parts of JavaScript certificate, I deepened my understanding of core JavaScript concepts such as scope, closures, hoisting, and asynchronous flow.",
},
image: "/certificates/trickyPartsOfJavascriptScrimba.png",
image: "/certificates/trickyPartsOfJavascriptScrimba.webp",
category: "web",
},
{
@@ -30,7 +30,7 @@ export const certificates = [
tr: "Scrimba üzerindeki JavaScript sertifikasıyla temel programlama becerilerimi geliştirdim.",
en: "With this Scrimba JavaScript certificate, I improved my foundational programming skills.",
},
image: "/certificates/javascriptScrimba.png",
image: "/certificates/javascriptScrimba.webp",
category: "web",
},
{
@@ -47,7 +47,7 @@ export const certificates = [
tr: "Scrimba üzerindeki CSS Animasyonları sertifikasıyla geçişler, hareket ve etkileşimli arayüz animasyonları oluşturma becerilerimi geliştirdim.",
en: "With this Scrimba CSS Animations certificate, I improved my skills in transitions, motion, and interactive interface animations.",
},
image: "/certificates/cssAnimationsScrimba.png",
image: "/certificates/cssAnimationsScrimba.webp",
category: "web",
},
{
@@ -64,7 +64,7 @@ export const certificates = [
tr: "Scrimba üzerindeki CSS Grid sertifikasıyla modern ve duyarlı yerleşim sistemleri kurma becerilerimi geliştirdim.",
en: "With this Scrimba CSS Grid certificate, I improved my ability to build modern and responsive layout systems.",
},
image: "/certificates/cssGridScrimba.png",
image: "/certificates/cssGridScrimba.webp",
category: "web",
},
{
@@ -81,7 +81,7 @@ export const certificates = [
tr: "Scrimba üzerindeki HTML ve CSS sertifikasıyla semantik yapı kurma ve arayüz stil verme konularında temel yetkinlik kazandım.",
en: "With this Scrimba HTML and CSS certificate, I gained foundational skills in semantic structure and interface styling.",
},
image: "/certificates/htmlCssScrimba.png",
image: "/certificates/htmlCssScrimba.webp",
category: "web",
},
{
@@ -217,7 +217,7 @@ export const certificates = [
tr: "Git ve GitHub üzerine aldığım bu eğitimde sürüm kontrol süreçleri, proje yönetimi ve ekip çalışmasında değişiklik takibi konularında yetkinliğimi artırdım.",
en: "In this training on Git and GitHub, I increased my competence in version control processes, project management, and tracking changes in teamwork.",
},
image: "/certificates/git.png",
image: "/certificates/git.webp",
category: "yazılım",
},
{
@@ -251,7 +251,7 @@ export const certificates = [
tr: "Profesyonel profil oluşturma, networking stratejileri geliştirme ve LinkedIn'i daha verimli kullanma konularında bilgi kazandım.",
en: "I gained knowledge about creating a professional profile, developing networking strategies, and using LinkedIn more efficiently.",
},
image: "/certificates/linkedin.png",
image: "/certificates/linkedin.webp",
category: "kişisel",
},
{
+15 -15
View File
@@ -1,18 +1,18 @@
// Galeri resimleri public/gallery/ klasörüne eklenen dosyaların yolları
export const GALLERY_IMAGES: string[] = [
"/gallery/1.jpg",
"/gallery/2.jpg",
"/gallery/3.jpg",
"/gallery/4.jpg",
"/gallery/5.jpg",
"/gallery/6.jpg",
"/gallery/7.jpeg",
"/gallery/8.jpeg",
"/gallery/9.jpeg",
"/gallery/10.jpeg",
"/gallery/11.jpeg",
"/gallery/12.jpeg",
"/gallery/13.jpeg",
"/gallery/14.png",
"/gallery/15.png",
"/gallery/1.webp",
"/gallery/2.webp",
"/gallery/3.webp",
"/gallery/4.webp",
"/gallery/5.webp",
"/gallery/6.webp",
"/gallery/7.webp",
"/gallery/8.webp",
"/gallery/9.webp",
"/gallery/10.webp",
"/gallery/11.webp",
"/gallery/12.webp",
"/gallery/13.webp",
"/gallery/14.webp",
"/gallery/15.webp",
];
+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,
},
],
];
+79 -64
View File
@@ -14,28 +14,18 @@ export type MediaKitAudienceRow = {
value: number;
};
export type MediaKitBreakdownRow = {
id: string;
label: Record<MediaKitLocale, string>;
value: number;
};
export const MEDIA_KIT_PERIOD = {
tr: "20 Haziran 17 Temmuz 2026",
en: "June 20 July 17, 2026",
tr: "Ömür boyu",
en: "Lifetime",
} satisfies Record<MediaKitLocale, string>;
export const MEDIA_KIT_METRICS: MediaKitMetric[] = [
{
id: "subscribers",
label: {
tr: "YouTube abonesi",
en: "YouTube subscribers",
},
value: {
tr: "8 B+",
en: "8K+",
},
detail: {
tr: "Son 28 günde +715",
en: "+715 in the last 28 days",
},
icon: "mdi:account-multiple-outline",
},
{
id: "views",
label: {
@@ -43,12 +33,12 @@ export const MEDIA_KIT_METRICS: MediaKitMetric[] = [
en: "Views",
},
value: {
tr: "31,1 B",
en: "31.1K",
tr: "788.422",
en: "788,422",
},
detail: {
tr: "Son 28 gün",
en: "Last 28 days",
tr: "Ömür boyu",
en: "Lifetime",
},
icon: "mdi:play-circle-outline",
},
@@ -59,72 +49,97 @@ export const MEDIA_KIT_METRICS: MediaKitMetric[] = [
en: "Watch time",
},
value: {
tr: "2,0 B saat",
en: "2.0K hours",
tr: "16,5 B saat",
en: "16.5K hours",
},
detail: {
tr: "≈ 3:52 / görüntüleme",
en: "≈ 3:52 per view",
tr: "Ömür boyu",
en: "Lifetime",
},
icon: "mdi:clock-outline",
},
{
id: "monthly-audience",
id: "subscribers",
label: {
tr: "Aylık kitle",
en: "Monthly audience",
tr: "Aboneler",
en: "Subscribers",
},
value: {
tr: "15,3 B",
en: "15.3K",
tr: "+9,3 B",
en: "+9.3K",
},
detail: {
tr: "Aktif izleyici",
en: "Active viewers",
tr: "Ömür boyu kazanılan",
en: "Gained lifetime",
},
icon: "mdi:chart-line",
icon: "mdi:account-multiple-outline",
},
{
id: "retention",
label: {
tr: "İzlemeye devam edenler",
en: "Stayed to watch",
},
value: {
tr: "%59,7",
en: "59.7%",
},
detail: {
tr: "%40,3 izlemeden geçti",
en: "40.3% swiped away",
},
icon: "mdi:eye-check-outline",
},
];
export const MEDIA_KIT_CONTENT_BREAKDOWN = [
export const MEDIA_KIT_DEVICES: MediaKitBreakdownRow[] = [
{
id: "videos",
label: {
tr: "Uzun videolar",
en: "Long-form videos",
},
value: {
tr: "24,3 B",
en: "24.3K",
},
percentage: 78.4,
id: "computer",
label: { tr: "Bilgisayar", en: "Computer" },
value: 45.2,
},
{
id: "shorts",
label: {
tr: "Shorts",
en: "Shorts",
},
value: {
tr: "6,7 B",
en: "6.7K",
},
percentage: 21.6,
id: "mobile",
label: { tr: "Cep telefonu", en: "Mobile phone" },
value: 38.8,
},
] as const;
{ id: "tv", label: { tr: "TV", en: "TV" }, value: 11.2 },
{ id: "tablet", label: { tr: "Tablet", en: "Tablet" }, value: 4.7 },
];
export const MEDIA_KIT_LOCATIONS: MediaKitBreakdownRow[] = [
{ id: "turkey", label: { tr: "Türkiye", en: "Türkiye" }, value: 89.2 },
{
id: "azerbaijan",
label: { tr: "Azerbaycan", en: "Azerbaijan" },
value: 3,
},
{ id: "germany", label: { tr: "Almanya", en: "Germany" }, value: 1.1 },
{
id: "united-states",
label: { tr: "Amerika Birleşik Devletleri", en: "United States" },
value: 0.2,
},
{
id: "netherlands",
label: { tr: "Hollanda", en: "Netherlands" },
value: 0.2,
},
];
export const MEDIA_KIT_GENDER: MediaKitAudienceRow[] = [
{ id: "male", label: "Erkek", value: 98 },
{ id: "female", label: "Kadın", value: 2 },
{ id: "female", label: "Kadın", value: 5.7 },
{ id: "male", label: "Erkek", value: 94.3 },
];
export const MEDIA_KIT_AGES: MediaKitAudienceRow[] = [
{ id: "18-24", label: "1824", value: 21.9 },
{ id: "25-34", label: "2534", value: 46.1 },
{ id: "35-44", label: "3544", value: 20.8 },
{ id: "45-54", label: "4554", value: 9.8 },
{ id: "55-64", label: "5564", value: 1.2 },
{ id: "65+", label: "65+", value: 0.1 },
{ id: "13-17", label: "1317", value: 1.5 },
{ id: "18-24", label: "1824", value: 23.7 },
{ id: "25-34", label: "2534", value: 45.4 },
{ id: "35-44", label: "3544", value: 19.1 },
{ id: "45-54", label: "4554", value: 8.5 },
{ id: "55-64", label: "5564", value: 1.5 },
{ id: "65+", label: "65+", value: 0.3 },
];
export const MEDIA_KIT_TOPICS = [
File diff suppressed because it is too large Load Diff
+173 -18
View File
@@ -1,16 +1,25 @@
export type ProjectItem = {
id: string;
title: string;
title: {
tr: string;
en: string;
} | string;
description: {
tr: string;
en: string;
};
image: string;
technologies: string[];
architecture: {
tr: string;
en: string;
};
badge?: {
tr: string;
en: string;
} | string;
href?: string;
caseStudySlug?: string;
};
export const MOBILE_APPS: ProjectItem[] = [
@@ -19,15 +28,103 @@ export const MOBILE_APPS: ProjectItem[] = [
title: "Targiz App",
badge: "Agritech",
image: "/projects/targiz.png",
technologies: [
"React Native",
"Next.js",
"Express.js",
"Supabase",
"Tailwind CSS",
],
architecture: {
tr: "React Native ve Next.js istemcilerini; Express.js servisleri, Supabase veri katmanı ve yapay zekâ destekli modüllerle birleştiren çapraz platform mimarisi.",
en: "A cross-platform architecture combining React Native and Next.js clients with Express.js services, a Supabase data layer, and AI-assisted modules.",
},
href: "https://targiz.com",
caseStudySlug: "targiz",
description: {
tr: "Ottoqua ekibiyle birlikte geliştirdiğimiz, küçük ölçekli üreticilere sahada destek veren yapay zeka destekli tarım uygulaması.",
en: "An AI-powered agricultural application we developed with the Ottoqua team to support small-scale producers in the field.",
tr: "Üç kişilik ekipte yazılım liderliği ve full-stack geliştirme sorumluluğunu üstlendiğim; üreticilere hastalık tespiti, lojistik ve pazar erişimi sunan mobil öncelikli tarım platformu.",
en: "A mobile-first agriculture platform for disease diagnosis, logistics, and market access, where I led a three-person software team while contributing as a full-stack developer.",
},
},
];
const OSTIM_TECHNOLOGIES = [
".NET 10",
"Angular",
"Astro",
"OMD UI Kit",
"Tailwind CSS",
];
export const WEB_APPS: ProjectItem[] = [
{
id: "ostim-web-portal",
title: {
tr: "OSTİM Web Portalı",
en: "OSTİM Web Portal",
},
badge: {
tr: "Kurumsal Portal",
en: "Corporate Portal",
},
image: "/projects/ostim.webp",
href: "https://ostim.org.tr",
caseStudySlug: "ostim-web-portali",
technologies: OSTIM_TECHNOLOGIES,
architecture: {
tr: "OSTİM Organize Sanayi Bölgesi ve yedi kümeye ait kurumsal içerikleri, firma ve ürün aramasını, çevrim içi işlemleri ve iletişim akışlarını tek portalda birleştiren çok bölümlü yapı.",
en: "A multi-section architecture combining corporate content for OSTİM Organized Industrial Zone and its seven clusters with company and product search, online services, and communication flows in one portal.",
},
description: {
tr: "OSTİM Organize Sanayi Bölgesi ve yedi kümesi için geliştirdiğimiz kapsamlı kurumsal web portalı.",
en: "A comprehensive corporate web portal we developed for OSTİM Organized Industrial Zone and its seven clusters.",
},
},
{
id: "ostim-employment",
title: {
tr: "OSTİM İstihdam",
en: "OSTİM Employment",
},
badge: {
tr: "İstihdam Portalı",
en: "Employment Portal",
},
image: "/projects/ostim-istihdam.webp",
href: "https://ostimistihdam.com",
caseStudySlug: "ostim-istihdam",
technologies: OSTIM_TECHNOLOGIES,
architecture: {
tr: "İŞKUR senkronizasyonu üzerine kurulu; aday, işveren, iş ve staj ilanı akışlarını yapay zekâ destekli eşleştirme katmanıyla buluşturan rol tabanlı portal mimarisi.",
en: "A role-based portal architecture built around İŞKUR synchronization, connecting candidate, employer, job, and internship workflows through an AI-assisted matching layer.",
},
description: {
tr: "İŞKUR ile senkron çalışan, adayları iş ve staj fırsatlarıyla buluşturan istihdam portalı.",
en: "An employment portal synchronized with İŞKUR that connects candidates with job and internship opportunities.",
},
},
{
id: "ostim-foreign-trade",
title: {
tr: "OSTİM Dış Ticaret",
en: "OSTİM Foreign Trade",
},
badge: {
tr: "Dış Ticaret Portalı",
en: "Foreign Trade Portal",
},
image: "/projects/ostim-dis-ticaret.webp",
href: "https://ostimdisticaret.net",
technologies: OSTIM_TECHNOLOGIES,
architecture: {
tr: "Dış ticaret firmaları, ilanlar ve yabancı dil bilen öğrenciler için ayrı kayıt ve başvuru akışlarını ortak bir eşleştirme ve ilan havuzunda birleştiren çok taraflı portal mimarisi.",
en: "A multi-sided portal architecture that brings registration and application flows for foreign-trade companies, listings, and multilingual students into a shared matching and opportunity pool.",
},
description: {
tr: "Dış ticaret yapan firmaları yabancı dil bilen öğrencilerle buluşturmaya odaklanan portal.",
en: "A portal focused on matching foreign-trade companies with students who speak foreign languages.",
},
},
{
id: "arc-foreign-trade",
title: "ARC Foreign Trade",
@@ -35,7 +132,12 @@ export const WEB_APPS: ProjectItem[] = [
tr: "Freelance",
en: "Freelance",
},
image: "/projects/arc.png",
image: "/projects/arc.webp",
technologies: ["Wix"],
architecture: {
tr: "Wix üzerinde yönetilebilir içerik ve kurumsal tanıtım sayfalarından oluşan, ihracat odaklı web sitesi yapısı.",
en: "An export-focused website architecture built on Wix with manageable content and corporate presentation pages.",
},
href: "https://arcforeigntrade.com",
description: {
tr: "Ankara merkezli ihracat odaklı bir üretici firma için kurumsal web sitesi yenileme projesi.",
@@ -44,16 +146,24 @@ export const WEB_APPS: ProjectItem[] = [
},
{
id: "ataturk-chronology",
title: "Atatürk Kronolojisi",
title: {
tr: "Atatürk Kronolojisi",
en: "Atatürk Chronology",
},
badge: {
tr: "Açık Kaynak",
en: "Open Source",
},
image: "/projects/ataturk.png",
image: "/projects/ataturk.webp",
technologies: ["React"],
architecture: {
tr: "React ile geliştirilen, kronoloji verisini etkileşimli bir zaman çizelgesi arayüzünde sunan istemci taraflı uygulama.",
en: "A client-side React application presenting chronology data through an interactive timeline interface.",
},
href: "https://ataturk-kronolojisi.org",
description: {
tr: "Atatürk’ün hayatındaki önemli olayları, konuşmaları ve reformları etkileşimli bir zaman çizelgesiyle sunan web deneyimi.",
en: "A web experience presenting important events, speeches, and reforms in Atatürk's life with an interactive timeline.",
tr: "Açık kaynak katkıcısı olarak yer aldığım; Atatürk’ün hayatındaki önemli olayları, konuşmaları ve reformları etkileşimli bir zaman çizelgesiyle sunan web deneyimi.",
en: "An interactive timeline of key events, speeches, and reforms in Atatürk's life, to which I contributed as an open-source contributor.",
},
},
{
@@ -64,6 +174,11 @@ export const WEB_APPS: ProjectItem[] = [
en: "Open Source",
},
image: "/projects/mockup.png",
technologies: ["Next.js"],
architecture: {
tr: "Next.js tabanlı, görsel işleme akışını tamamen tarayıcıda çalıştıran istemci öncelikli araç mimarisi.",
en: "A client-first Next.js tool architecture that runs its image-processing workflow entirely in the browser.",
},
href: "https://mockup-factory-mu.vercel.app/",
description: {
tr: "Görsellerin cihaz mockuplarına saniyeler içinde dönüştürüldüğü, tamamen tarayıcı üzerinde çalışan açık kaynak araç.",
@@ -72,30 +187,50 @@ export const WEB_APPS: ProjectItem[] = [
},
{
id: "ohhike",
title: "Ohhike Coach",
title: "OhHike",
badge: {
tr: "Açık Kaynak",
en: "Open Source",
tr: "Self-hosted",
en: "Self-hosted",
},
image: "/projects/ohhike.png",
technologies: [
"React",
"TypeScript",
"Vite",
"Express.js",
"Better Auth",
"SQLite",
"Drizzle ORM",
],
architecture: {
tr: "pnpm/Turborepo monorepo içinde React/Vite arayüzü ve modüler monolit Express API; SQLite, Drizzle ORM, gerçek servis testleri ve Docker tabanlı self-hosted dağıtım.",
en: "A React/Vite frontend and modular-monolith Express API in a pnpm/Turborepo monorepo, with SQLite, Drizzle ORM, real-service tests, and Docker-based self-hosted deployment.",
},
href: "https://www.ohhike.com",
caseStudySlug: "ohhike",
description: {
tr: "Spor takımları için açık kaynaklı, yapay zekâ destekli antrenörlük zekâ platformu. OhHike CoachOS; sporcu check-in'lerini, antrenman notlarını, akıllı saat verilerini ve antrenman geçmişini aksiyona geçirilebilir bir takım hafızasına dönüştürür.",
en: "An open-source, AI-powered coaching intelligence platform for sports teams. OhHike CoachOS turns athlete check-ins, training notes, smartwatch data, and session history into actionable team memory.",
tr: "Masa başında çalışan geliştiriciler için aktivite, haftalık sağlık planı, beslenme ve kalori takibini bir araya getiren self-hosted uygulama.",
en: "A self-hosted activity, weekly health planning, nutrition, and calorie tracking application for desk-bound developers.",
},
},
{
id: "neta",
title: "Take Neta",
badge: {
tr: "Açık Kaynak",
en: "Open Source",
tr: "Self-hosted",
en: "Self-hosted",
},
image: "/projects/neta.png",
technologies: ["Next.js", "TypeScript", "Express.js", "AI Integrations"],
architecture: {
tr: "Landing page, uygulama ve API paketlerini birlikte yöneten Next.js ve Express.js tabanlı monorepo mimarisi.",
en: "A Next.js and Express.js monorepo architecture managing landing page, application, and API packages together.",
},
href: "https://www.takeneta.com",
caseStudySlug: "take-neta",
description: {
tr: "Dijital ikinci beyniniz. Bilinçli üretkenlik ve yaşam takibi için hepsi bir arada kişisel işletim sistemi. Yerel öncelikli, yapay zekâ entegrasyonlu ve açık kaynaklı.",
en: "Your digital second brain. An all-in-one personal operating system for mindful productivity and life-tracking. Local-first, AI-integrated, and open-source.",
tr: "Freelancer'lar için görev, proje, müşteri, finans, yapay zekâ ve müşteri portalı akışlarını birleştiren self-hosted işletim sistemi.",
en: "A self-hosted operating system for freelancers that unifies tasks, projects, clients, finance, AI, and client portal workflows.",
},
},
];
@@ -109,6 +244,11 @@ export const EXTENSIONS: ProjectItem[] = [
en: "Cross-Browser",
},
image: "/projects/quick-fill.png",
technologies: ["JavaScript"],
architecture: {
tr: "Tarayıcı eklentisi API'leriyle çalışan, kısayol tanımlarını form alanlarına bağlayan çapraz tarayıcı JavaScript yapısı.",
en: "A cross-browser JavaScript extension architecture that connects shortcut definitions to form fields through browser extension APIs.",
},
href: "https://github.com/poyrazavsever/shortcut-injector",
description: {
tr: "Özel klavye kısayollarını kullanarak önceden tanımlanmış kişisel verileri ve bağlantıları web formlarına hızlıca enjekte etmek için geliştirilmiş bir tarayıcı eklentisi.",
@@ -123,6 +263,11 @@ export const EXTENSIONS: ProjectItem[] = [
en: "Cross-Browser",
},
image: "/projects/sound_sync.png",
technologies: ["JavaScript"],
architecture: {
tr: "Sekmelerin medya durumlarını izleyip oynatma komutlarını ileten olay tabanlı, çapraz tarayıcı eklenti mimarisi.",
en: "An event-driven, cross-browser extension architecture that observes media state across tabs and relays playback commands.",
},
href: "https://github.com/poyrazavsever/tab-audio-relay",
description: {
tr: "Sekmeler arasındaki ses çalma işlemlerini senkronize eden bir tarayıcı eklentisi. Eğitim videonuz durduğunda müziğinizi otomatik olarak oynatır, eğitime devam ettiğinizde ise müziği duraklatır.",
@@ -134,9 +279,14 @@ export const EXTENSIONS: ProjectItem[] = [
export const FIGMA_TEMPLATES: ProjectItem[] = [
{
id: "hsd-website",
title: "HSD Community Web Site",
title: "HSD Community Website",
badge: "Figma",
image: "/projects/hsd.png",
technologies: ["Figma"],
architecture: {
tr: "Bileşenler, kontrol paneli, açılış sayfası ve profil ekranlarını doğrudan Figma içinde düzenleyen bileşen tabanlı tasarım dosyası.",
en: "A component-based design file organized directly in Figma across components, dashboard, landing page, and profile screens.",
},
href: "https://www.figma.com/community/file/1613511833232376739",
description: {
tr: "HSD Community için Web Site tasarımı. Bileşenler, kontrol paneli, açılış sayfası, profil sayfaları.",
@@ -148,6 +298,11 @@ export const FIGMA_TEMPLATES: ProjectItem[] = [
title: "Restaurant Menu UI Design",
badge: "Figma",
image: "/projects/menu.png",
technologies: ["Figma"],
architecture: {
tr: "Tekrar kullanılabilir arayüz parçaları ve menü varyasyonlarından oluşan, doğrudan Figma üzerinde hazırlanan tasarım şablonu.",
en: "A design template created directly in Figma with reusable interface elements and menu variants.",
},
href: "https://www.figma.com/community/file/1613577450975840169/restaurant-menu-ui-design",
description: {
tr: "Topluluk için Restaurant Menü Arayüz Tasarımı şablonu.",
+1 -1
View File
@@ -24,7 +24,7 @@ export const REFERENCES: Reference[] = [
en: "Client - 2025",
},
rating: 5,
avatar: "/avatars/ali.png",
avatar: "/avatars/ali.webp",
quote: {
tr: "Web sitesi tam olarak istediğim gibi oldu. Poyraz benim gözden kaçırdığım detayları da düşündü, her kararı şeffaf şekilde anlattı ve beklentimin üstünde bir iş teslim etti.",
en: "The website turned out exactly as I wanted. Poyraz thought of the details I missed, explained every decision transparently, and delivered a job beyond my expectations.",
+6 -4
View File
@@ -12,28 +12,30 @@ export const SPONSORS: Sponsor[] = [
name: "Hostinger",
job: "Web Hosting",
logo: "/sponsors/hostinger.png",
websiteUrl: "https://hostinger.com",
websiteUrl: "https://hostinger.com/poyraz",
},
{
id: "testsprite",
name: "TestSprite",
job: "Yazılım Test Otomasyonu",
logo: "/sponsors/testsprite.png",
websiteUrl: "https://testsprite.com",
websiteUrl: "https://www.testsprite.com/?via=poyraz",
},
{
id: "minimax",
name: "MiniMax",
job: "AI Video & Model Platformu",
logo: "/sponsors/minimax.png",
websiteUrl: "https://www.minimax.io",
websiteUrl:
"https://platform.minimax.io/subscribe/coding-plan?code=7aH9b0Ya7c&source=link",
},
{
id: "higgsfield",
name: "Higgsfield",
job: "AI Creative Suite",
logo: "/sponsors/higgsfield.png",
websiteUrl: "https://higgsfield.ai",
websiteUrl:
"https://higgsfield.ai/s/higgsfield-mcp-3-0-yt-poyrazavsever-lLvqMw",
},
{
id: "hosting-dunyam",
+126
View File
@@ -0,0 +1,126 @@
import type { Localized } from "@/lib/locale";
export type TechnologyStackItem = {
id: string;
label: string | Localized;
icon: string;
};
export type TechnologyStackGroup = {
id: "frontend" | "backend" | "databases" | "languages" | "designTools";
items: readonly TechnologyStackItem[];
};
export const TECHNOLOGY_STACK: readonly TechnologyStackGroup[] = [
{
id: "frontend",
items: [
{ id: "react", label: "React.js", icon: "simple-icons:react" },
{ id: "nextjs", label: "Next.js", icon: "simple-icons:nextdotjs" },
{ id: "angular", label: "Angular", icon: "simple-icons:angular" },
{
id: "react-native",
label: "React Native",
icon: "mdi:react",
},
{ id: "electron", label: "Electron.js", icon: "simple-icons:electron" },
{
id: "tailwindcss",
label: "Tailwind CSS",
icon: "simple-icons:tailwindcss",
},
{
id: "bootstrap",
label: "Bootstrap",
icon: "simple-icons:bootstrap",
},
{ id: "redux", label: "Redux", icon: "simple-icons:redux" },
{ id: "zustand", label: "Zustand", icon: "devicon:zustand" },
],
},
{
id: "backend",
items: [
{ id: "nodejs", label: "Node.js", icon: "simple-icons:nodedotjs" },
{ id: "express", label: "Express.js", icon: "simple-icons:express" },
{ id: "nestjs", label: "Nest.js", icon: "simple-icons:nestjs" },
{ id: "dotnet", label: ".NET", icon: "simple-icons:dotnet" },
{
id: "rest-api",
label: { tr: "REST API'ler", en: "REST APIs" },
icon: "mdi:api",
},
{
id: "jwt-auth",
label: "JWT Auth",
icon: "simple-icons:jsonwebtokens",
},
{ id: "prisma", label: "Prisma", icon: "simple-icons:prisma" },
{
id: "socket-io",
label: "Socket.io",
icon: "simple-icons:socketdotio",
},
],
},
{
id: "databases",
items: [
{ id: "mongodb", label: "MongoDB", icon: "simple-icons:mongodb" },
{ id: "mongoose", label: "Mongoose", icon: "simple-icons:mongoose" },
{
id: "postgresql",
label: "PostgreSQL",
icon: "simple-icons:postgresql",
},
{ id: "mysql", label: "MySQL", icon: "simple-icons:mysql" },
{
id: "mssql",
label: "Microsoft SQL Server",
icon: "simple-icons:microsoftsqlserver",
},
],
},
{
id: "languages",
items: [
{
id: "javascript",
label: "JavaScript",
icon: "simple-icons:javascript",
},
{
id: "typescript",
label: "TypeScript",
icon: "simple-icons:typescript",
},
{ id: "csharp", label: "C#", icon: "mdi:language-csharp" },
],
},
{
id: "designTools",
items: [
{ id: "figma", label: "Figma", icon: "simple-icons:figma" },
{
id: "wireframing",
label: "Wireframing",
icon: "mdi:vector-square",
},
{
id: "design-systems",
label: { tr: "Tasarım Sistemleri", en: "Design Systems" },
icon: "mdi:palette-swatch-outline",
},
{ id: "git", label: "Git", icon: "simple-icons:git" },
{ id: "github", label: "GitHub", icon: "simple-icons:github" },
{ id: "firebase", label: "Firebase", icon: "simple-icons:firebase" },
{ id: "supabase", label: "Supabase", icon: "simple-icons:supabase" },
{
id: "vercel-ai-sdk",
label: "Vercel AI SDK",
icon: "simple-icons:vercel",
},
{ id: "postman", label: "Postman", icon: "simple-icons:postman" },
],
},
];
+21
View File
@@ -13,6 +13,27 @@ export type VolunteerCommunityItem = {
};
export const VOLUNTEER_COMMUNITY_ITEMS: VolunteerCommunityItem[] = [
{
id: "google-developer-student-club-tech-team-lead",
title: "Google Developer Student Club — Tech Team Lead",
timeline: {
tr: "2026 - Günümüz",
en: "2026 - Present",
},
focus: {
tr: "Teknoloji ekibine liderlik ederek workshop ve hackathon organizasyonlarını planlamak, katılımcılara mentorluk yapmak ve topluluğun Instagram hesabı için teknik videolar üretmek.",
en: "Leading the tech team by planning workshops and hackathons, mentoring participants, and producing technical videos for the community's Instagram account.",
},
},
{
id: "shipin-tech-core-team",
title: "Shipin — Tech Core Team Member",
timeline: "2026 - 2026",
focus: {
tr: "Topluluğun organizasyon ve etkinlik süreçlerine destek olmak, teknik sunumlar gerçekleştirmek ve katılımcılara mentorluk yapmak.",
en: "Supporting community operations and event organization, delivering technical presentations, and mentoring participants.",
},
},
{
id: "youtube",
title: "YouTube",
+17
View File
@@ -0,0 +1,17 @@
export type XVideo = {
src: string;
episode: number;
};
export const X_JAVASCRIPT_ANATOMY_VIDEOS: readonly XVideo[] = [
{
src: "/video/bolum11render.mp4",
episode: 11,
},
{
src: "/video/bolum12Render.mp4",
episode: 12,
},
];
export const X_JAVASCRIPT_ANATOMY_URL = "https://x.com/poyrazavsever";
+20 -4
View File
@@ -112,6 +112,18 @@ export function getCommandPaletteGroups(
icon: "mdi:file-document-outline",
keywords: ["blog", locale === "tr" ? "yazı" : "post", "article"],
},
{
id: "agenda-index",
label: locale === "tr" ? "Haftalık Gündem" : "Weekly Agenda",
href: "/agenda",
icon: "mdi:newspaper-variant-outline",
keywords: [
"newsletter",
locale === "tr" ? "gündem" : "agenda",
locale === "tr" ? "haftalık" : "weekly",
locale === "tr" ? "haber" : "news",
],
},
{
id: "blog-content-page",
label: locale === "tr" ? "Video ve Notlar" : "Videos and Notes",
@@ -217,11 +229,12 @@ export function getCommandPaletteGroups(
const projectItems: CommandPaletteItem[] = [
...MOBILE_APPS.map((item) => {
const title = getLocalizedValue(item.title, locale);
const description = getLocalizedValue(item.description, locale);
const badgeStr = item.badge ? getLocalizedValue(item.badge, locale) : "";
return {
id: `mobile-project-${item.id}`,
label: item.title,
label: title,
href: "/projects",
icon: "mdi:cellphone",
keywords: [
@@ -233,11 +246,12 @@ export function getCommandPaletteGroups(
};
}),
...WEB_APPS.map((item) => {
const title = getLocalizedValue(item.title, locale);
const description = getLocalizedValue(item.description, locale);
const badgeStr = item.badge ? getLocalizedValue(item.badge, locale) : "";
return {
id: `web-project-${item.id}`,
label: item.title,
label: title,
href: item.href ?? "/projects",
icon: "mdi:web",
external: Boolean(item.href),
@@ -250,11 +264,12 @@ export function getCommandPaletteGroups(
};
}),
...EXTENSIONS.map((item) => {
const title = getLocalizedValue(item.title, locale);
const description = getLocalizedValue(item.description, locale);
const badgeStr = item.badge ? getLocalizedValue(item.badge, locale) : "";
return {
id: `extension-project-${item.id}`,
label: item.title,
label: title,
href: item.href ?? "/projects",
icon: "mdi:puzzle-outline",
external: Boolean(item.href),
@@ -268,11 +283,12 @@ export function getCommandPaletteGroups(
};
}),
...FIGMA_TEMPLATES.map((item) => {
const title = getLocalizedValue(item.title, locale);
const description = getLocalizedValue(item.description, locale);
const badgeStr = item.badge ? getLocalizedValue(item.badge, locale) : "";
return {
id: `figma-project-${item.id}`,
label: item.title,
label: title,
href: item.href ?? "/projects",
icon: "mdi:figma",
external: Boolean(item.href),
+43 -3
View File
@@ -2,9 +2,49 @@ import { readdir } from "node:fs/promises";
import path from "node:path";
const PDF_DIR = path.join(process.cwd(), "public", "pdf");
const PDF_THUMBNAIL_DIR = path.join(
process.cwd(),
"public",
"pdf-thumbnails",
);
const PDF_LIMIT = 3;
export async function getPdfNotes() {
const files = await readdir(PDF_DIR);
export type PdfNote = {
fileName: string;
title: string;
href: string;
thumbnailSrc: string | null;
};
function formatPdfTitle(fileName: string) {
return fileName
.replace(/\.pdf$/i, "")
.split(/[-_]+/)
.map((part) => part.charAt(0).toLocaleUpperCase("tr-TR") + part.slice(1))
.join(" ");
}
export async function getPdfNotes(): Promise<PdfNote[]> {
const [files, thumbnailFiles] = await Promise.all([
readdir(PDF_DIR),
readdir(PDF_THUMBNAIL_DIR).catch(() => [] as string[]),
]);
const thumbnails = new Set(thumbnailFiles);
const pdfFiles = files.filter((file) => file.toLowerCase().endsWith(".pdf"));
return pdfFiles.sort((a, b) => a.localeCompare(b));
return pdfFiles
.sort((a, b) => a.localeCompare(b, "tr"))
.slice(0, PDF_LIMIT)
.map((fileName) => {
const thumbnailName = `${fileName.replace(/\.pdf$/i, "")}.jpg`;
return {
fileName,
title: formatPdfTitle(fileName),
href: `/pdf/${fileName}`,
thumbnailSrc: thumbnails.has(thumbnailName)
? `/pdf-thumbnails/${thumbnailName}`
: null,
};
});
}
+78 -9
View File
@@ -56,12 +56,6 @@ export const SOCIAL_LINKS = [
href: "https://behance.net/poyrazavsever",
icon: "mdi:behance",
},
{
id: "spotify",
label: "Spotify",
href: "https://open.spotify.com/user/3136fdjkc5p4cbzmuxhvqdd4b2hu",
icon: "mdi:spotify",
},
{
id: "buy-me-a-coffee",
label: "Bana kahve ısmarla",
@@ -77,6 +71,14 @@ export const NAV_DROPDOWN_GROUPS = [
icon: "mdi:dots-horizontal",
insertAfter: "gallery",
items: [
{
id: "agenda",
label: "Haftalık Gündem",
href: "/agenda",
icon: "mdi:newspaper-variant-outline",
external: false,
keywords: ["gündem", "agenda", "newsletter", "haftalık", "weekly", "haber"],
},
{
id: "animationResources",
label: "Animasyon Kaynakları",
@@ -85,6 +87,21 @@ export const NAV_DROPDOWN_GROUPS = [
external: false,
keywords: ["animasyon", "animation", "kaynak", "resource", "motion"],
},
{
id: "technologies",
label: "Teknolojiler",
href: "/technologies",
icon: "mdi:layers-triple-outline",
external: false,
keywords: [
"teknolojiler",
"technologies",
"tech stack",
"araçlar",
"tools",
"skills",
],
},
],
},
] as const;
@@ -145,15 +162,67 @@ export const LINK_DIRECTORY_CATEGORIES: ReadonlyArray<{
{ 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) => ({
...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,
label: item.label,
href: item.href,
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: "mdi:compass-outline",
icon: item.icon,
external: false,
category: "navigation" as const,
keywords: [item.label, item.href, "sayfa", "navigasyon", "internal"],
keywords: [...item.keywords],
})),
...SOCIAL_LINKS.map((item) => ({
id: item.id,
+57
View File
@@ -24,6 +24,12 @@ export type GithubActivity = {
createdAt: string;
};
export type GithubContributionDay = {
date: string;
count: number;
level: 0 | 1 | 2 | 3 | 4;
};
type GithubRepoApi = {
id: number;
name: string;
@@ -136,3 +142,54 @@ export async function getGithubActivity(): Promise<GithubActivity[]> {
return [];
}
}
export function parseGithubContributions(
html: string,
): GithubContributionDay[] {
const contributions: GithubContributionDay[] = [];
const cellPattern =
/<td\b([^>]*\bdata-date="[^"]+"[^>]*)><\/td>\s*<tool-tip\b[^>]*>([\s\S]*?)<\/tool-tip>/g;
for (const match of html.matchAll(cellPattern)) {
const attributes = match[1];
const tooltip = match[2].replace(/<[^>]+>/g, "").trim();
const date = attributes.match(/data-date="([^"]+)"/)?.[1];
const levelValue = attributes.match(/data-level="([0-4])"/)?.[1];
const countValue = tooltip.match(/^([\d,]+)\s+contributions?\b/i)?.[1];
if (!date || levelValue === undefined) continue;
contributions.push({
date,
count: countValue ? Number(countValue.replaceAll(",", "")) : 0,
level: Number(levelValue) as GithubContributionDay["level"],
});
}
return contributions.sort((first, second) =>
first.date.localeCompare(second.date),
);
}
export async function getGithubContributions(): Promise<
GithubContributionDay[]
> {
try {
const response = await fetch(
`https://github.com/users/${USERNAME}/contributions`,
{
next: { revalidate: 3600 },
headers: {
Accept: "text/html",
"User-Agent": "portfolio-new",
},
},
);
if (!response.ok) return [];
return parseGithubContributions(await response.text());
} catch {
return [];
}
}
+150
View File
@@ -0,0 +1,150 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
function resolveSiteUrl() {
const url = new URL(
process.env.NEXT_PUBLIC_SITE_URL || "https://www.poyrazavsever.com",
);
// The public site redirects the apex domain to `www`. Canonicals must point
// directly to the final URL instead of a redirecting host.
if (url.hostname === "poyrazavsever.com") {
url.hostname = "www.poyrazavsever.com";
}
return url.origin;
}
export const SITE_URL = resolveSiteUrl();
export type SiteLocale = "tr" | "en";
type LocalePaths = Partial<Record<SiteLocale, string>>;
type StaticSeoPage =
| "home"
| "about"
| "references"
| "volunteerCommunity"
| "blog"
| "projects"
| "content"
| "gallery"
| "technologies"
| "links"
| "contact";
function normalizePath(path: string) {
if (!path || path === "/") return "/";
return `/${path.replace(/^\/+|\/+$/g, "")}`;
}
export function getLocalizedPath(locale: SiteLocale, path: string) {
const normalizedPath = normalizePath(path);
return normalizedPath === "/"
? `/${locale}`
: `/${locale}${normalizedPath}`;
}
export function getAbsoluteUrl(path: string) {
return new URL(path, `${SITE_URL}/`).toString();
}
export function getLocalizedUrl(locale: SiteLocale, path: string) {
return getAbsoluteUrl(getLocalizedPath(locale, path));
}
export function createAlternates(
locale: SiteLocale,
paths: LocalePaths,
): Metadata["alternates"] {
const currentPath = paths[locale];
if (!currentPath) return undefined;
const languages: Record<string, string> = {};
if (paths.tr) {
languages["tr-TR"] = getLocalizedUrl("tr", paths.tr);
}
if (paths.en) {
languages["en-US"] = getLocalizedUrl("en", paths.en);
}
languages["x-default"] = paths.tr
? getLocalizedUrl("tr", paths.tr)
: getLocalizedUrl(locale, currentPath);
return {
canonical: getLocalizedUrl(locale, currentPath),
languages,
};
}
export function createPageMetadata({
locale,
title,
description,
path,
absoluteTitle = false,
}: {
locale: SiteLocale;
title: string;
description: string;
path: string;
absoluteTitle?: boolean;
}): Metadata {
const url = getLocalizedUrl(locale, path);
return {
title: absoluteTitle ? { absolute: title } : title,
description,
alternates: createAlternates(locale, { tr: path, en: path }),
openGraph: {
type: "website",
siteName: "Poyraz Avsever",
locale: locale === "tr" ? "tr_TR" : "en_US",
alternateLocale: locale === "tr" ? ["en_US"] : ["tr_TR"],
url,
title,
description,
images: [
{
url: "/og.png",
width: 1200,
height: 630,
alt: "Poyraz Avsever",
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
creator: "@poyrazavsever",
images: ["/og.png"],
},
};
}
export async function getStaticPageMetadata({
locale,
page,
path,
absoluteTitle = false,
}: {
locale: string;
page: StaticSeoPage;
path: string;
absoluteTitle?: boolean;
}) {
const siteLocale = locale === "en" ? "en" : "tr";
const t = await getTranslations({ locale: siteLocale, namespace: "Seo" });
return createPageMetadata({
locale: siteLocale,
title: t(`${page}.title`),
description: t(`${page}.description`),
path,
absoluteTitle,
});
}
+65
View File
@@ -0,0 +1,65 @@
export type YouTubeChannelStats = {
subscribers: number | null;
views: number;
videos: number;
};
type YouTubeChannelsResponse = {
items?: Array<{
statistics?: {
hiddenSubscriberCount?: boolean;
subscriberCount?: string;
viewCount?: string;
videoCount?: string;
};
}>;
};
function parseCount(value: string | undefined) {
if (!value) return null;
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : null;
}
export async function getYouTubeChannelStats(): Promise<YouTubeChannelStats | null> {
const apiKey = process.env.YOUTUBE_API_KEY?.trim();
if (!apiKey) return null;
const params = new URLSearchParams({
part: "statistics",
key: apiKey,
});
const channelId = process.env.YOUTUBE_CHANNEL_ID?.trim();
if (channelId) {
params.set("id", channelId);
} else {
params.set("forHandle", "@poyrazavsever");
}
try {
const response = await fetch(
`https://www.googleapis.com/youtube/v3/channels?${params.toString()}`,
{ next: { revalidate: 3600 } },
);
if (!response.ok) return null;
const data = (await response.json()) as YouTubeChannelsResponse;
const statistics = data.items?.[0]?.statistics;
const views = parseCount(statistics?.viewCount);
const videos = parseCount(statistics?.videoCount);
if (!statistics || views === null || videos === null) return null;
return {
subscribers: statistics.hiddenSubscriberCount
? null
: parseCount(statistics.subscriberCount),
views,
videos,
};
} catch {
return null;
}
}
+177 -4
View File
@@ -1,7 +1,9 @@
{
"Nav": {
"home": "Home",
"about": "About",
"blog": "Blog",
"agenda": "Weekly Agenda",
"content": "Content",
"projects": "Projects",
"gallery": "Gallery",
@@ -11,6 +13,11 @@
"socialLinks": "Social Links",
"others": "Others",
"animationResources": "Animation Resources",
"technologies": "Technologies",
"links": "Links",
"mediaKit": "Media Kit",
"references": "References",
"volunteerCommunity": "Volunteering & Community",
"backToMenu": "Back to menu",
"menu": "Menu",
"mobileMenu": "Mobile Menu",
@@ -27,6 +34,15 @@
"allProjects": "All projects",
"reviewsAndReferences": "Reviews & References",
"allReferences": "All references",
"technologiesTitle": "Technologies I use",
"allTechnologies": "Explore all technologies",
"technologyCategories": {
"frontend": "Front End",
"backend": "Back End",
"databases": "Databases",
"languages": "Programming Languages",
"designTools": "Design & Tools"
},
"sections": {
"recentPosts": "Recent Posts",
"references": "References",
@@ -34,6 +50,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",
@@ -64,9 +132,38 @@
},
"viewNpmPackages": "View my NPM packages",
"visitGithub": "Visit my GitHub profile",
"technologies": "Technologies",
"architecture": "Architecture",
"viewCaseStudy": "View case study",
"contributionCalendar": "poyrazavsever GitHub contribution calendar",
"contributionUnavailable": "GitHub contribution data is currently unavailable.",
"noContributions": "No contributions",
"contributionSingular": "contribution",
"contributionPlural": "contributions",
"emptyNpm": "npm API response is currently empty.",
"emptyGithub": "GitHub API response is currently empty."
},
"ProjectCaseStudy": {
"back": "Back to all projects",
"liveDemo": "Live site",
"sourceCode": "Source code",
"overview": "Project overview",
"roleAndTeam": "Role and team structure",
"role": "My role",
"team": "Team",
"context": "Context",
"problemConstraints": "Problem and constraints",
"constraints": "Key constraints",
"architectureDecisions": "Architecture decisions",
"designProcess": "Design process",
"challenge": "The challenge",
"solution": "Solution",
"results": "Results and scope indicators",
"screenshots": "Screenshots",
"openScreenshot": "Open full size",
"technologies": "Technology stack",
"repository": "Source code status"
},
"Blog": {
"recentPosts": "Recent Posts",
"searchPlaceholder": "Search posts...",
@@ -85,6 +182,14 @@
"diagramLoading": "Diagram rendering...",
"diagramError": "Could not render Mermaid diagram."
},
"Agenda": {
"title": "Weekly Agenda",
"description": "Weekly developments and commentary from software, technology, design, and artificial intelligence.",
"searchPlaceholder": "Search the weekly agenda...",
"empty": "No agenda posts yet.",
"noSearch": "No agenda posts found for \"{search}\".",
"backToAgenda": "<- Back to Weekly Agenda"
},
"AnimationSources": {
"title": "Animation Sources",
"description": "Production notes, prompts, and reusable code from animations I create for web and mobile products.",
@@ -96,12 +201,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",
@@ -109,16 +219,74 @@
"resources": "Resources"
}
},
"Technologies": {
"title": "Technologies I Use",
"description": "The front-end, back-end, database, programming language, design, and development tools I use to build products.",
"categories": {
"frontend": "Front End",
"backend": "Back End",
"databases": "Databases",
"languages": "Programming Languages",
"designTools": "Design & Tools"
}
},
"Gallery": {
"empty": "No images added to the gallery yet.",
"close": "Close",
"prev": "Previous",
"next": "Next"
},
"Seo": {
"home": {
"title": "Poyraz Avsever | Portfolio",
"description": "The portfolio of Poyraz Avsever, featuring full-stack products, AI systems, modern web projects, and practical software content."
},
"about": {
"title": "About",
"description": "Explore Poyraz Avsever's software journey, professional experience, education, certificates, and community work."
},
"references": {
"title": "References",
"description": "Read professional testimonials and references from people who have worked with Poyraz Avsever."
},
"volunteerCommunity": {
"title": "Volunteering and Community",
"description": "Explore Poyraz Avsever's volunteer work, technology community contributions, and event experience."
},
"blog": {
"title": "Blog",
"description": "Technical articles about frontend development, JavaScript, web architecture, user experience, and software engineering."
},
"projects": {
"title": "Projects",
"description": "Explore Poyraz Avsever's web applications, mobile products, open-source tools, browser extensions, and design work."
},
"content": {
"title": "Content",
"description": "Poyraz Avsever's YouTube videos, LinkedIn PDF notes, and JavaScript Anatomy video series."
},
"gallery": {
"title": "Gallery",
"description": "Selected images from Poyraz Avsever's events, presentations, projects, and community work."
},
"technologies": {
"title": "Technologies I Use",
"description": "Explore the front-end, back-end, database, programming language, and development tools Poyraz Avsever uses to build products."
},
"links": {
"title": "Links",
"description": "Access Poyraz Avsever's social profiles, projects, content, and essential links from one page."
},
"contact": {
"title": "Contact",
"description": "Contact Poyraz Avsever by email or social media for projects, collaborations, and professional opportunities."
}
},
"Metadata": {
"title": "Poyraz Avsever | Portfolio",
"titleTemplate": "%s | Poyraz Avsever",
"description": "Poyraz Avsever's personal portfolio website sharing projects, software content, and technical works."
"description": "Poyraz Avsever's personal portfolio website sharing projects, software content, and technical works.",
"socialImageAlt": "Poyraz Avsever portfolio social preview"
},
"SearchCommand": {
"placeholder": "Search pages and links...",
@@ -126,14 +294,19 @@
"footer": "Press {shortcut} to open quickly"
},
"Content": {
"youtubeTitle": "Latest YouTube Videos",
"youtubeTitlePrefix": "Latest",
"youtubeTitle": "Videos",
"youtubeChannel": "My YouTube channel",
"youtubeEmbedTitle": "YouTube video player",
"pdfTitle": "LinkedIn PDF Notes",
"pdfTitle": "PDF Notes",
"linkedinProfile": "My LinkedIn profile",
"pdfPreviewError": "Preview could not be loaded",
"pdfPreviewTitle": "{name} preview",
"openPdf": "Open the {name} PDF note",
"pdfModalDefaultTitle": "PDF Note",
"xTitle": "JavaScript Anatomy",
"xSeries": "My JavaScript Anatomy series on X",
"xVideoTitle": "JavaScript Anatomy, episode {episode}",
"videoUnsupported": "Your browser does not support video playback.",
"prev": "Previous",
"next": "Next",
"pdfNotFound": "No PDF files found inside `/public/pdf`."
+177 -4
View File
@@ -1,7 +1,9 @@
{
"Nav": {
"home": "Ana Sayfa",
"about": "Hakkımda",
"blog": "Blog",
"agenda": "Haftalık Gündem",
"content": "İçerikler",
"projects": "Projeler",
"gallery": "Galeri",
@@ -11,6 +13,11 @@
"socialLinks": "Sosyal Bağlantılar",
"others": "Diğerleri",
"animationResources": "Animasyon Kaynakları",
"technologies": "Teknolojiler",
"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ü",
@@ -27,6 +34,15 @@
"allProjects": "Tüm projeler",
"reviewsAndReferences": "Yorumlar & Referanslar",
"allReferences": "Tüm referanslar",
"technologiesTitle": "Ben bu teknolojileri kullanıyorum",
"allTechnologies": "Tüm teknolojileri incele",
"technologyCategories": {
"frontend": "Ön Yüz",
"backend": "Arka Yüz",
"databases": "Veritabanları",
"languages": "Programlama Dilleri",
"designTools": "Tasarım ve Araçlar"
},
"sections": {
"recentPosts": "Son Yazılar",
"references": "Referanslar",
@@ -34,6 +50,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ç",
@@ -64,9 +132,38 @@
},
"viewNpmPackages": "NPM paketlerimi gör",
"visitGithub": "GitHub hesabına git",
"technologies": "Teknolojiler",
"architecture": "Mimari",
"viewCaseStudy": "Vaka çalışmasını incele",
"contributionCalendar": "poyrazavsever GitHub katkı takvimi",
"contributionUnavailable": "GitHub katkı verisi şu anda alınamıyor.",
"noContributions": "Katkı yok",
"contributionSingular": "katkı",
"contributionPlural": "katkı",
"emptyNpm": "npm API yanıtı şu anda boş.",
"emptyGithub": "GitHub API yanıtı şu anda boş."
},
"ProjectCaseStudy": {
"back": "Tüm projelere dön",
"liveDemo": "Canlı site",
"sourceCode": "Kaynak kod",
"overview": "Proje özeti",
"roleAndTeam": "Rol ve ekip yapısı",
"role": "Rolüm",
"team": "Ekip",
"context": "Bağlam",
"problemConstraints": "Problem ve kısıtlar",
"constraints": "Temel kısıtlar",
"architectureDecisions": "Mimari kararlar",
"designProcess": "Tasarım süreci",
"challenge": "Karşılaşılan zorluk",
"solution": "Çözüm",
"results": "Sonuçlar ve kapsam göstergeleri",
"screenshots": "Ekran görüntüleri",
"openScreenshot": "Tam boyutta aç",
"technologies": "Kullanılan teknolojiler",
"repository": "Kaynak kod durumu"
},
"Blog": {
"recentPosts": "Son Yazılar",
"searchPlaceholder": "Yazılarda ara...",
@@ -85,6 +182,14 @@
"diagramLoading": "Diyagram hazırlanıyor...",
"diagramError": "Mermaid diyagramı oluşturulamadı."
},
"Agenda": {
"title": "Haftalık Gündem",
"description": "Yazılım, teknoloji, tasarım ve yapay zeka dünyasından haftalık gelişmeler ve yorumlar.",
"searchPlaceholder": "Haftalık gündemde ara...",
"empty": "Henüz gündem yazısı bulunmuyor.",
"noSearch": "\"{search}\" aramasına uygun gündem yazısı bulunamadı.",
"backToAgenda": "<- Haftalık Gündem'e dön"
},
"AnimationSources": {
"title": "Animasyon Kaynakları",
"description": "Web ve mobil ürünler için hazırladığım animasyonların üretim süreçleri, promptları ve uygulanabilir kodları.",
@@ -96,12 +201,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",
@@ -109,16 +219,74 @@
"resources": "Kaynaklar"
}
},
"Technologies": {
"title": "Kullandığım Teknolojiler",
"description": "Ürün geliştirirken kullandığım ön yüz, arka yüz, veritabanı, programlama dili, tasarım ve geliştirme araçları.",
"categories": {
"frontend": "Ön Yüz",
"backend": "Arka Yüz",
"databases": "Veritabanları",
"languages": "Programlama Dilleri",
"designTools": "Tasarım ve Araçlar"
}
},
"Gallery": {
"empty": "Henüz galeriye görsel eklenmemiş.",
"close": "Kapat",
"prev": "Önceki",
"next": "Sonraki"
},
"Seo": {
"home": {
"title": "Poyraz Avsever | Portfolyo",
"description": "Full-stack ürünler, yapay zeka sistemleri, modern web projeleri ve uygulanabilir yazılım içerikleri üreten Poyraz Avsever'in portfolyosu."
},
"about": {
"title": "Hakkımda",
"description": "Poyraz Avsever'in yazılım yolculuğunu, deneyimini, eğitimini, sertifikalarını ve topluluk çalışmalarını keşfedin."
},
"references": {
"title": "Referanslar",
"description": "Birlikte çalıştığım kişilerin Poyraz Avsever hakkındaki profesyonel değerlendirmelerini ve referanslarını inceleyin."
},
"volunteerCommunity": {
"title": "Gönüllülük ve Topluluk",
"description": "Poyraz Avsever'in gönüllülük çalışmalarını, teknoloji topluluklarına katkılarını ve etkinlik deneyimlerini inceleyin."
},
"blog": {
"title": "Blog",
"description": "Frontend, JavaScript, web mimarisi, kullanıcı deneyimi ve yazılım geliştirme üzerine teknik yazılar."
},
"projects": {
"title": "Projeler",
"description": "Poyraz Avsever'in web uygulamaları, mobil ürünleri, açık kaynak araçları, tarayıcı eklentileri ve tasarım çalışmalarını keşfedin."
},
"content": {
"title": "İçerikler",
"description": "Poyraz Avsever'in YouTube videoları, LinkedIn PDF notları ve JavaScript Anatomisi video serisi."
},
"gallery": {
"title": "Galeri",
"description": "Poyraz Avsever'in etkinlik, sunum, proje ve topluluk çalışmalarından seçilmiş görseller."
},
"technologies": {
"title": "Kullandığım Teknolojiler",
"description": "Poyraz Avsever'in ürün geliştirirken kullandığı ön yüz, arka yüz, veritabanı, programlama dili ve geliştirme araçlarını inceleyin."
},
"links": {
"title": "Bağlantılar",
"description": "Poyraz Avsever'in sosyal medya hesaplarına, projelerine, içeriklerine ve hızlı erişim bağlantılarına tek sayfadan ulaşın."
},
"contact": {
"title": "İletişim",
"description": "Proje, iş birliği ve profesyonel iletişim için Poyraz Avsever'e e-posta veya sosyal medya üzerinden ulaşın."
}
},
"Metadata": {
"title": "Poyraz Avsever | Portfolyo",
"titleTemplate": "%s | Poyraz Avsever",
"description": "Poyraz Avsever'in projelerini, yazılım içeriklerini ve teknik çalışmalarını paylaştığı kişisel portfolyo sitesi."
"description": "Poyraz Avsever'in projelerini, yazılım içeriklerini ve teknik çalışmalarını paylaştığı kişisel portfolyo sitesi.",
"socialImageAlt": "Poyraz Avsever portfolyo paylaşım görseli"
},
"SearchCommand": {
"placeholder": "Sayfa ve bağlantılarda ara...",
@@ -126,14 +294,19 @@
"footer": "Hızlıca açmak için {shortcut} kullan"
},
"Content": {
"youtubeTitle": "Son YouTube Videoları",
"youtubeTitlePrefix": "Son",
"youtubeTitle": "Videoları",
"youtubeChannel": "YouTube kanalım",
"youtubeEmbedTitle": "YouTube video oynatıcı",
"pdfTitle": "LinkedIn PDF Notları",
"pdfTitle": "PDF Notları",
"linkedinProfile": "LinkedIn profilim",
"pdfPreviewError": "Önizleme yüklenemedi",
"pdfPreviewTitle": "{name} önizleme",
"openPdf": "{name} PDF notunu aç",
"pdfModalDefaultTitle": "PDF Notu",
"xTitle": "JavaScript Anatomisi",
"xSeries": "X'teki JavaScript Anatomisi serim",
"xVideoTitle": "JavaScript Anatomisi, bölüm {episode}",
"videoUnsupported": "Tarayıcınız video oynatmayı desteklemiyor.",
"prev": "Önceki",
"next": "Sonraki",
"pdfNotFound": "/public/pdf içinde PDF bulunamadı."
+32
View File
@@ -17,6 +17,38 @@ const nextConfig: NextConfig = {
protocol: "https",
hostname: "i.ytimg.com",
},
{
protocol: "https",
hostname: "images.pexels.com",
},
{
protocol: "https",
hostname: "miro.medium.com",
},
{
protocol: "https",
hostname: "www.c-sharpcorner.com",
},
{
protocol: "https",
hostname: "media.geeksforgeeks.org",
},
{
protocol: "https",
hostname: "edward-huang.com",
},
{
protocol: "https",
hostname: "encrypted-tbn0.gstatic.com",
},
{
protocol: "https",
hostname: "cdn.hashnode.com",
},
{
protocol: "https",
hostname: "i.sstatic.net",
},
],
},
};
-1
View File
@@ -25,7 +25,6 @@
"mermaid": "^11.13.0",
"next": "16.1.6",
"next-intl": "4.13.0",
"pdfjs-dist": "^5.5.207",
"poyraz-ui": "3.0.2",
"react": "19.2.3",
"react-dom": "19.2.3",
+1 -134
View File
@@ -26,9 +26,6 @@ importers:
next-intl:
specifier: 4.13.0
version: 4.13.0(next@16.1.6(@babel/core@7.29.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
pdfjs-dist:
specifier: ^5.5.207
version: 5.7.284
poyraz-ui:
specifier: 3.0.2
version: 3.0.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(mermaid@11.16.0)(react-dom@19.2.3(react@19.2.3))(react-hook-form@7.81.0(react@19.2.3))(react@19.2.3)(tailwindcss@4.3.2)(zod@4.4.3)
@@ -529,81 +526,6 @@ packages:
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
'@napi-rs/canvas-android-arm64@0.1.100':
resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.100':
resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.100':
resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.100':
resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.100':
resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.1.6':
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
peerDependencies:
@@ -2885,6 +2807,7 @@ packages:
eslint@9.39.5:
resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
peerDependencies:
jiti: '*'
@@ -4243,10 +4166,6 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
pdfjs-dist@5.7.284:
resolution: {integrity: sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==}
engines: {node: '>=22.13.0 || >=24'}
pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
@@ -5714,54 +5633,6 @@ snapshots:
'@mixmark-io/domino@2.2.0': {}
'@napi-rs/canvas-android-arm64@0.1.100':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.100':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.100':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.100':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
optional: true
'@napi-rs/canvas@0.1.100':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.100
'@napi-rs/canvas-darwin-arm64': 0.1.100
'@napi-rs/canvas-darwin-x64': 0.1.100
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
'@napi-rs/canvas-linux-arm64-gnu': 0.1.100
'@napi-rs/canvas-linux-arm64-musl': 0.1.100
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
'@napi-rs/canvas-linux-x64-gnu': 0.1.100
'@napi-rs/canvas-linux-x64-musl': 0.1.100
'@napi-rs/canvas-win32-arm64-msvc': 0.1.100
'@napi-rs/canvas-win32-x64-msvc': 0.1.100
optional: true
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
@@ -9922,10 +9793,6 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.3
pdfjs-dist@5.7.284:
optionalDependencies:
'@napi-rs/canvas': 0.1.100
pend@1.2.0: {}
performance-now@2.1.0: {}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 928 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Some files were not shown because too many files have changed in this diff Show More