feat: added new project detail page
@@ -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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import type { MetadataRoute } from "next";
|
|||||||
import { listAnimationSources } from "@/data/animation-sources";
|
import { listAnimationSources } from "@/data/animation-sources";
|
||||||
import { isNewsletterCategory } from "@/data/blog";
|
import { isNewsletterCategory } from "@/data/blog";
|
||||||
import { listBlogDetails } from "@/data/blog-detail";
|
import { listBlogDetails } from "@/data/blog-detail";
|
||||||
|
import { listProjectCaseStudies } from "@/data/project-case-studies";
|
||||||
import {
|
import {
|
||||||
getAbsoluteUrl,
|
getAbsoluteUrl,
|
||||||
getLocalizedUrl,
|
getLocalizedUrl,
|
||||||
@@ -127,5 +128,26 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,57 @@ export function ProjectsJsonLd({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export type ArticleJsonLdProps = {
|
export type ArticleJsonLdProps = {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
import { Badge, Typography } from "poyraz-ui/atoms";
|
import { Badge, Typography } from "poyraz-ui/atoms";
|
||||||
import {
|
import {
|
||||||
ImageCard,
|
ImageCard,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "poyraz-ui/molecules";
|
} from "poyraz-ui/molecules";
|
||||||
|
import { Link } from "@/i18n/routing";
|
||||||
|
|
||||||
type ProjectCardWithPopoverProps = {
|
type ProjectCardWithPopoverProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -15,6 +17,8 @@ type ProjectCardWithPopoverProps = {
|
|||||||
image: string;
|
image: string;
|
||||||
badge?: string;
|
badge?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
caseStudyHref?: string;
|
||||||
|
caseStudyLabel?: string;
|
||||||
technologies: string[];
|
technologies: string[];
|
||||||
architecture: string;
|
architecture: string;
|
||||||
technologiesLabel: string;
|
technologiesLabel: string;
|
||||||
@@ -29,6 +33,8 @@ export function ProjectCardWithPopover({
|
|||||||
image,
|
image,
|
||||||
badge,
|
badge,
|
||||||
href,
|
href,
|
||||||
|
caseStudyHref,
|
||||||
|
caseStudyLabel,
|
||||||
technologies,
|
technologies,
|
||||||
architecture,
|
architecture,
|
||||||
technologiesLabel,
|
technologiesLabel,
|
||||||
@@ -76,7 +82,18 @@ export function ProjectCardWithPopover({
|
|||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
{href ? (
|
{caseStudyHref ? (
|
||||||
|
<Link
|
||||||
|
href={caseStudyHref}
|
||||||
|
className={`${triggerClasses} no-underline`}
|
||||||
|
onPointerEnter={showPopover}
|
||||||
|
onPointerLeave={scheduleClose}
|
||||||
|
onFocus={showPopover}
|
||||||
|
onBlur={scheduleClose}
|
||||||
|
>
|
||||||
|
{card}
|
||||||
|
</Link>
|
||||||
|
) : href ? (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
className={`${triggerClasses} no-underline`}
|
className={`${triggerClasses} no-underline`}
|
||||||
@@ -145,6 +162,17 @@ export function ProjectCardWithPopover({
|
|||||||
{architecture}
|
{architecture}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ type LocalizedProjectItem = {
|
|||||||
architecture: string;
|
architecture: string;
|
||||||
badge?: string;
|
badge?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
caseStudySlug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ProjectSection({
|
function ProjectSection({
|
||||||
@@ -75,11 +76,13 @@ function ProjectSection({
|
|||||||
items,
|
items,
|
||||||
technologiesLabel,
|
technologiesLabel,
|
||||||
architectureLabel,
|
architectureLabel,
|
||||||
|
caseStudyLabel,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
items: LocalizedProjectItem[];
|
items: LocalizedProjectItem[];
|
||||||
technologiesLabel: string;
|
technologiesLabel: string;
|
||||||
architectureLabel: string;
|
architectureLabel: string;
|
||||||
|
caseStudyLabel: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
@@ -95,6 +98,12 @@ function ProjectSection({
|
|||||||
description={item.description}
|
description={item.description}
|
||||||
badge={item.badge}
|
badge={item.badge}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
caseStudyHref={
|
||||||
|
item.caseStudySlug
|
||||||
|
? `/projects/${item.caseStudySlug}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
caseStudyLabel={caseStudyLabel}
|
||||||
technologies={item.technologies}
|
technologies={item.technologies}
|
||||||
architecture={item.architecture}
|
architecture={item.architecture}
|
||||||
technologiesLabel={technologiesLabel}
|
technologiesLabel={technologiesLabel}
|
||||||
@@ -148,24 +157,28 @@ export async function ProjectsContent() {
|
|||||||
items={localizeItems(WEB_APPS)}
|
items={localizeItems(WEB_APPS)}
|
||||||
technologiesLabel={t("technologies")}
|
technologiesLabel={t("technologies")}
|
||||||
architectureLabel={t("architecture")}
|
architectureLabel={t("architecture")}
|
||||||
|
caseStudyLabel={t("viewCaseStudy")}
|
||||||
/>
|
/>
|
||||||
<ProjectSection
|
<ProjectSection
|
||||||
title={t("sections.mobileApps")}
|
title={t("sections.mobileApps")}
|
||||||
items={localizeItems(MOBILE_APPS)}
|
items={localizeItems(MOBILE_APPS)}
|
||||||
technologiesLabel={t("technologies")}
|
technologiesLabel={t("technologies")}
|
||||||
architectureLabel={t("architecture")}
|
architectureLabel={t("architecture")}
|
||||||
|
caseStudyLabel={t("viewCaseStudy")}
|
||||||
/>
|
/>
|
||||||
<ProjectSection
|
<ProjectSection
|
||||||
title={t("sections.extensions")}
|
title={t("sections.extensions")}
|
||||||
items={localizeItems(EXTENSIONS)}
|
items={localizeItems(EXTENSIONS)}
|
||||||
technologiesLabel={t("technologies")}
|
technologiesLabel={t("technologies")}
|
||||||
architectureLabel={t("architecture")}
|
architectureLabel={t("architecture")}
|
||||||
|
caseStudyLabel={t("viewCaseStudy")}
|
||||||
/>
|
/>
|
||||||
<ProjectSection
|
<ProjectSection
|
||||||
title={t("sections.figmaTemplates")}
|
title={t("sections.figmaTemplates")}
|
||||||
items={localizeItems(FIGMA_TEMPLATES)}
|
items={localizeItems(FIGMA_TEMPLATES)}
|
||||||
technologiesLabel={t("technologies")}
|
technologiesLabel={t("technologies")}
|
||||||
architectureLabel={t("architecture")}
|
architectureLabel={t("architecture")}
|
||||||
|
caseStudyLabel={t("viewCaseStudy")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type ProjectItem = {
|
|||||||
en: string;
|
en: string;
|
||||||
} | string;
|
} | string;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
caseStudySlug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MOBILE_APPS: ProjectItem[] = [
|
export const MOBILE_APPS: ProjectItem[] = [
|
||||||
@@ -27,15 +28,22 @@ export const MOBILE_APPS: ProjectItem[] = [
|
|||||||
title: "Targiz App",
|
title: "Targiz App",
|
||||||
badge: "Agritech",
|
badge: "Agritech",
|
||||||
image: "/projects/targiz.png",
|
image: "/projects/targiz.png",
|
||||||
technologies: ["Next.js", "Supabase"],
|
technologies: [
|
||||||
|
"React Native",
|
||||||
|
"Next.js",
|
||||||
|
"Express.js",
|
||||||
|
"Supabase",
|
||||||
|
"Tailwind CSS",
|
||||||
|
],
|
||||||
architecture: {
|
architecture: {
|
||||||
tr: "Atomic Design yaklaşımıyla oluşturulmuş, Supabase tabanlı modüler uygulama mimarisi.",
|
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 modular, Supabase-backed application architecture built with the Atomic Design approach.",
|
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",
|
href: "https://targiz.com",
|
||||||
|
caseStudySlug: "targiz",
|
||||||
description: {
|
description: {
|
||||||
tr: "Ottoqua ekibiyle birlikte geliştirdiğimiz, küçük ölçekli üreticilere sahada destek veren yapay zeka destekli tarım uygulaması.",
|
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: "An AI-powered agricultural application we developed with the Ottoqua team to support small-scale producers in the field.",
|
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.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -61,6 +69,7 @@ export const WEB_APPS: ProjectItem[] = [
|
|||||||
},
|
},
|
||||||
image: "/projects/ostim.webp",
|
image: "/projects/ostim.webp",
|
||||||
href: "https://ostim.org.tr",
|
href: "https://ostim.org.tr",
|
||||||
|
caseStudySlug: "ostim-web-portali",
|
||||||
technologies: OSTIM_TECHNOLOGIES,
|
technologies: OSTIM_TECHNOLOGIES,
|
||||||
architecture: {
|
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ı.",
|
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ı.",
|
||||||
@@ -83,6 +92,7 @@ export const WEB_APPS: ProjectItem[] = [
|
|||||||
},
|
},
|
||||||
image: "/projects/ostim-istihdam.webp",
|
image: "/projects/ostim-istihdam.webp",
|
||||||
href: "https://ostimistihdam.com",
|
href: "https://ostimistihdam.com",
|
||||||
|
caseStudySlug: "ostim-istihdam",
|
||||||
technologies: OSTIM_TECHNOLOGIES,
|
technologies: OSTIM_TECHNOLOGIES,
|
||||||
architecture: {
|
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.",
|
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.",
|
||||||
@@ -152,8 +162,8 @@ export const WEB_APPS: ProjectItem[] = [
|
|||||||
},
|
},
|
||||||
href: "https://ataturk-kronolojisi.org",
|
href: "https://ataturk-kronolojisi.org",
|
||||||
description: {
|
description: {
|
||||||
tr: "Atatürk’ün hayatındaki önemli olayları, konuşmaları ve reformları etkileşimli bir zaman çizelgesiyle sunan web deneyimi.",
|
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: "A web experience presenting important events, speeches, and reforms in Atatürk's life with an interactive timeline.",
|
en: "An interactive timeline of key events, speeches, and reforms in Atatürk's life, to which I contributed as an open-source contributor.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -177,40 +187,50 @@ export const WEB_APPS: ProjectItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ohhike",
|
id: "ohhike",
|
||||||
title: "Ohhike Coach",
|
title: "OhHike",
|
||||||
badge: {
|
badge: {
|
||||||
tr: "Açık Kaynak",
|
tr: "Self-hosted",
|
||||||
en: "Open Source",
|
en: "Self-hosted",
|
||||||
},
|
},
|
||||||
image: "/projects/ohhike.png",
|
image: "/projects/ohhike.png",
|
||||||
technologies: ["React", "Express.js", "Better Auth", "better-sqlite3"],
|
technologies: [
|
||||||
|
"React",
|
||||||
|
"TypeScript",
|
||||||
|
"Vite",
|
||||||
|
"Express.js",
|
||||||
|
"Better Auth",
|
||||||
|
"SQLite",
|
||||||
|
"Drizzle ORM",
|
||||||
|
],
|
||||||
architecture: {
|
architecture: {
|
||||||
tr: "Landing page, uygulama ve API katmanlarını aynı çalışma alanında yöneten; React arayüzü ve Express API'sinden oluşan monorepo.",
|
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 monorepo managing the landing page, application, and API in one workspace, with a React interface and Express API.",
|
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",
|
href: "https://www.ohhike.com",
|
||||||
|
caseStudySlug: "ohhike",
|
||||||
description: {
|
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.",
|
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: "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.",
|
en: "A self-hosted activity, weekly health planning, nutrition, and calorie tracking application for desk-bound developers.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "neta",
|
id: "neta",
|
||||||
title: "Take Neta",
|
title: "Take Neta",
|
||||||
badge: {
|
badge: {
|
||||||
tr: "Açık Kaynak",
|
tr: "Self-hosted",
|
||||||
en: "Open Source",
|
en: "Self-hosted",
|
||||||
},
|
},
|
||||||
image: "/projects/neta.png",
|
image: "/projects/neta.png",
|
||||||
technologies: ["Next.js", "Express.js"],
|
technologies: ["Next.js", "TypeScript", "Express.js", "AI Integrations"],
|
||||||
architecture: {
|
architecture: {
|
||||||
tr: "Landing page, uygulama ve API paketlerini birlikte yöneten Next.js ve Express.js tabanlı monorepo mimarisi.",
|
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.",
|
en: "A Next.js and Express.js monorepo architecture managing landing page, application, and API packages together.",
|
||||||
},
|
},
|
||||||
href: "https://www.takeneta.com",
|
href: "https://www.takeneta.com",
|
||||||
|
caseStudySlug: "take-neta",
|
||||||
description: {
|
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ı.",
|
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: "Your digital second brain. An all-in-one personal operating system for mindful productivity and life-tracking. Local-first, AI-integrated, and open-source.",
|
en: "A self-hosted operating system for freelancers that unifies tasks, projects, clients, finance, AI, and client portal workflows.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -67,9 +67,31 @@
|
|||||||
"visitGithub": "Visit my GitHub profile",
|
"visitGithub": "Visit my GitHub profile",
|
||||||
"technologies": "Technologies",
|
"technologies": "Technologies",
|
||||||
"architecture": "Architecture",
|
"architecture": "Architecture",
|
||||||
|
"viewCaseStudy": "View case study",
|
||||||
"emptyNpm": "npm API response is currently empty.",
|
"emptyNpm": "npm API response is currently empty.",
|
||||||
"emptyGithub": "GitHub 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": {
|
"Blog": {
|
||||||
"recentPosts": "Recent Posts",
|
"recentPosts": "Recent Posts",
|
||||||
"searchPlaceholder": "Search posts...",
|
"searchPlaceholder": "Search posts...",
|
||||||
|
|||||||
@@ -67,9 +67,31 @@
|
|||||||
"visitGithub": "GitHub hesabına git",
|
"visitGithub": "GitHub hesabına git",
|
||||||
"technologies": "Teknolojiler",
|
"technologies": "Teknolojiler",
|
||||||
"architecture": "Mimari",
|
"architecture": "Mimari",
|
||||||
|
"viewCaseStudy": "Vaka çalışmasını incele",
|
||||||
"emptyNpm": "npm API yanıtı şu anda boş.",
|
"emptyNpm": "npm API yanıtı şu anda boş.",
|
||||||
"emptyGithub": "GitHub 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": {
|
"Blog": {
|
||||||
"recentPosts": "Son Yazılar",
|
"recentPosts": "Son Yazılar",
|
||||||
"searchPlaceholder": "Yazılarda ara...",
|
"searchPlaceholder": "Yazılarda ara...",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 774 KiB |
|
Before Width: | Height: | Size: 512 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 671 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 244 KiB |