Refactor site hero and localized branding UI

This commit is contained in:
MOH
2026-03-08 05:13:10 +01:00
parent 9edb162148
commit 2bda611a3b
35 changed files with 682 additions and 234 deletions
+71 -107
View File
@@ -1,9 +1,13 @@
import type { Metadata } from "next";
import {
ArrowRight,
BadgeCheck,
Blocks,
BriefcaseBusiness,
Boxes,
Mail,
ChartNoAxesCombined,
Globe,
Package2,
Search,
} from "lucide-react";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
@@ -17,11 +21,6 @@ import { AppCard } from "@/components/ui/app-card";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
import {
getLocalizedValue,
getPublishedPortfolioProjects,
} from "@/lib/portfolio";
import { pickText, productItems } from "@/lib/site-data";
type HomePageProps = {
params: {
@@ -29,6 +28,16 @@ type HomePageProps = {
};
};
const serviceIcons = [
BriefcaseBusiness,
Globe,
Package2,
Blocks,
Search,
ChartNoAxesCombined,
BadgeCheck,
];
export const dynamic = "force-dynamic";
export async function generateMetadata({
@@ -51,9 +60,16 @@ export async function generateMetadata({
export default async function HomePage({ params: { locale } }: HomePageProps) {
const localeKey = resolveLocale(locale);
const featuredProjects = (await getPublishedPortfolioProjects()).slice(0, 3);
const featuredProducts = productItems.slice(0, 3);
const t = await getTranslations({ locale: localeKey, namespace: "homepage" });
const serviceItems = [
t("serviceBrand"),
t("serviceWebsite"),
t("serviceStore"),
t("serviceWordPress"),
t("serviceSeo"),
t("serviceTracking"),
t("serviceSystem"),
];
return (
<>
@@ -66,113 +82,61 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
contactLabel={t("toContact")}
/>
<Container className="flex flex-col gap-section pb-12 lg:pb-16">
<Container className="relative z-10 -mt-6 flex flex-col gap-section pb-12 lg:-mt-8 lg:pb-16">
<MotionFade delay={0.05}>
<AppCard>
<CardContent className="p-6 lg:p-8">
<div className="mb-6 flex items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold text-foreground">
{t("portfolioTitle")}
</h2>
<p className="mt-2 text-sm text-muted-foreground">
{t("portfolioText")}
</p>
</div>
<BriefcaseBusiness className="h-5 w-5 text-muted-foreground" />
</div>
<div className="grid gap-4 md:grid-cols-3">
{featuredProjects.map((item) => (
<AppCard key={item.slug} interactive>
<CardContent className="p-5">
<Link
href={getLocalizedPath(localeKey, `/portfolio/${item.slug}`)}
className="group block"
<AppCard
level="inverse"
className="overflow-hidden border border-white/6 bg-[linear-gradient(145deg,hsl(var(--foreground)),hsl(221_42%_10%))] text-primary-foreground"
>
<p className="text-sm text-muted-foreground/80">
{getLocalizedValue(item.category.name, localeKey)} - {item.projectYear}
</p>
<h3 className="mt-2 text-lg font-semibold text-foreground">
{getLocalizedValue(item.title, localeKey)}
</h3>
<p className="mt-2 text-sm text-muted-foreground">
{getLocalizedValue(item.summary, localeKey)}
</p>
<span className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-foreground/80 group-hover:text-foreground">
{t("toPortfolio")}
<ArrowRight className="h-4 w-4" />
</span>
</Link>
</CardContent>
</AppCard>
))}
</div>
</CardContent>
</AppCard>
</MotionFade>
<MotionFade delay={0.1}>
<AppCard>
<CardContent className="p-6 lg:p-8">
<div className="mb-6 flex items-center justify-between gap-4">
<CardContent className="p-6 lg:p-10">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)] lg:items-start">
<div>
<h2 className="text-2xl font-semibold text-foreground">
{t("productsTitle")}
<p className="text-sm font-medium tracking-[0.08em] text-primary-foreground/62">
{t("servicesEyebrow")}
</p>
<h2 className="mt-4 max-w-[12ch] text-balance text-4xl font-semibold leading-[0.92] tracking-[-0.06em] text-primary-foreground sm:text-5xl lg:text-6xl">
{t("servicesTitle")}
</h2>
<p className="mt-2 text-sm text-muted-foreground">
{t("productsText")}
<p className="mt-5 max-w-[34rem] text-sm leading-7 text-primary-foreground/76 sm:text-base">
{t("servicesText")}
</p>
</div>
<Boxes className="h-5 w-5 text-muted-foreground" />
</div>
<div className="grid gap-4 md:grid-cols-3">
{featuredProducts.map((item) => (
<AppCard key={item.slug} interactive>
<CardContent className="p-5">
<Link
href={getLocalizedPath(localeKey, `/products/${item.slug}`)}
className="group block"
>
<p className="text-sm text-muted-foreground/80">
{pickText(item.segment, localeKey)}
</p>
<h3 className="mt-2 text-lg font-semibold text-foreground">
{pickText(item.name, localeKey)}
</h3>
<p className="mt-2 text-sm text-muted-foreground">
{pickText(item.summary, localeKey)}
</p>
<p className="mt-3 text-sm font-medium text-foreground/80">
{pickText(item.price, localeKey)}
</p>
</Link>
</CardContent>
</AppCard>
))}
</div>
</CardContent>
</AppCard>
</MotionFade>
<MotionFade delay={0.15}>
<AppCard level="inverse">
<CardContent className="p-6 lg:p-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="inline-flex items-center gap-2 text-sm font-medium text-primary-foreground/80">
<Mail className="h-4 w-4" />
{t("ctaTitle")}
</p>
<p className="mt-2 max-w-2xl text-sm text-primary-foreground/88 sm:text-base">
{t("ctaText")}
</p>
</div>
<Button asChild variant="secondary">
<div className="mt-7 flex flex-wrap gap-3">
<Button asChild size="lg" variant="secondary" className="min-w-[11rem]">
<Link href={getLocalizedPath(localeKey, "/contact")}>
{t("toContact")}
{t("servicesPrimaryCta")}
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button
asChild
size="lg"
variant="outline"
className="min-w-[11rem] border-white/16 bg-white/6 text-primary-foreground hover:bg-white/10 hover:text-primary-foreground"
>
<Link href={getLocalizedPath(localeKey, "/portfolio")}>
{t("servicesSecondaryCta")}
</Link>
</Button>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{serviceItems.map((item, index) => {
const Icon = serviceIcons[index];
return (
<div
key={item}
className="rounded-nested border border-white/10 bg-white/6 p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-sm transition-colors hover:bg-white/8"
>
<Icon className="h-5 w-5 text-primary-foreground/72" />
<p className="mt-4 text-sm font-medium leading-6 text-primary-foreground/88 sm:text-base">
{item}
</p>
</div>
);
})}
</div>
</div>
</CardContent>
</AppCard>
+4 -1
View File
@@ -34,10 +34,13 @@ export default async function LocaleLayout({
setRequestLocale(locale);
const messages = await getMessages();
const localeClassName = locale === "ar" ? "font-arabic" : "font-latin";
return (
<NextIntlClientProvider messages={messages}>
<div lang={locale} dir={getDirection(locale)}>{children}</div>
<div lang={locale} dir={getDirection(locale)} className={localeClassName}>
{children}
</div>
</NextIntlClientProvider>
);
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+215
View File
@@ -40,6 +40,11 @@
--brand-primary: 9 73% 50%;
--brand-secondary: 12 64% 58%;
--hero-base: 260 22% 95%;
--hero-ink: 226 43% 11%;
--hero-left: 226 100% 85%;
--hero-right: 334 33% 91%;
--hero-bottom: 205 61% 88%;
--ambient-primary-alpha: 0.06;
--ambient-secondary-alpha: 0.04;
--ambient-grid-alpha: 0;
@@ -114,6 +119,11 @@
--brand-primary: 9 78% 56%;
--brand-secondary: 12 74% 64%;
--hero-base: 222 24% 10%;
--hero-ink: 210 20% 96%;
--hero-left: 223 45% 24%;
--hero-right: 293 18% 22%;
--hero-bottom: 212 35% 20%;
--ambient-primary-alpha: 0.08;
--ambient-secondary-alpha: 0.05;
--ambient-grid-alpha: 0;
@@ -139,12 +149,25 @@
html {
color-scheme: light;
background-color: hsl(var(--background));
font-family: var(--font-museo), sans-serif;
}
html.dark {
color-scheme: dark;
}
html[lang="ar"] {
font-family: var(--font-dubai), sans-serif;
}
.font-latin {
font-family: var(--font-museo), sans-serif;
}
.font-arabic {
font-family: var(--font-dubai), sans-serif;
}
@layer base {
* {
@apply border-border;
@@ -154,11 +177,19 @@ html.dark {
@apply min-h-screen text-foreground antialiased;
background-color: hsl(var(--background));
background-image: none;
font-family: inherit;
}
a {
@apply transition-colors;
}
button,
input,
select,
textarea {
font: inherit;
}
}
::selection {
@@ -214,4 +245,188 @@ html.dark {
transparent 32%
);
}
.hero-surface {
background: linear-gradient(180deg, hsl(var(--hero-base)), hsl(var(--background)));
}
.hero-home {
color: hsl(var(--hero-ink));
}
.hero-page {
background: linear-gradient(180deg, hsl(var(--hero-base) / 0.96), hsl(var(--background)));
}
.hero-sheet {
isolation: isolate;
}
.hero-sheet-base {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 14% 18%, hsl(var(--hero-left) / 0.42), transparent 26%),
radial-gradient(circle at 84% 14%, hsl(var(--hero-right) / 0.28), transparent 24%),
radial-gradient(circle at 52% 84%, hsl(var(--hero-bottom) / 0.24), transparent 32%),
linear-gradient(180deg, hsl(var(--hero-base)), hsl(var(--background)) 88%);
}
.hero-blob {
position: absolute;
border-radius: 9999px;
filter: blur(110px);
will-change: transform, opacity;
}
.hero-blob-left {
left: -8%;
top: 8%;
height: 24rem;
width: 24rem;
background: radial-gradient(circle, hsl(var(--hero-left) / 0.48), transparent 68%);
}
.hero-blob-right {
right: -10%;
top: 2%;
height: 22rem;
width: 22rem;
background: radial-gradient(circle, hsl(var(--hero-right) / 0.36), transparent 70%);
}
.hero-blob-bottom {
left: 18%;
bottom: -10%;
height: 24rem;
width: 38rem;
background: radial-gradient(circle, hsl(var(--hero-bottom) / 0.32), transparent 70%);
}
.hero-sheet-glow {
position: absolute;
inset: -10% -8%;
background:
radial-gradient(circle at 18% 24%, hsl(var(--hero-left) / 0.24), transparent 18%),
radial-gradient(circle at 86% 18%, hsl(var(--hero-right) / 0.16), transparent 18%),
radial-gradient(circle at 50% 78%, hsl(var(--hero-bottom) / 0.14), transparent 24%);
filter: blur(36px);
will-change: transform, opacity;
}
.hero-sheet-wave {
position: absolute;
inset: -16% -10%;
background:
conic-gradient(
from 180deg at 50% 50%,
transparent 0deg,
hsl(var(--hero-left) / 0.08) 58deg,
transparent 120deg,
transparent 228deg,
hsl(var(--hero-right) / 0.1) 300deg,
transparent 360deg
);
filter: blur(90px);
mix-blend-mode: screen;
will-change: transform, opacity;
}
.hero-sheet-noise {
position: absolute;
inset: 0;
background-image:
linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23noise)' opacity='0.1'/%3E%3C/svg%3E");
background-size: auto, 160px 160px;
mix-blend-mode: soft-light;
opacity: 0.22;
will-change: background-position, opacity, transform;
}
.hero-sheet-veil {
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.025) 44%, rgba(255, 255, 255, 0));
backdrop-filter: blur(20px) saturate(112%);
-webkit-backdrop-filter: blur(20px) saturate(112%);
}
.hero-sheet-fade {
position: absolute;
inset: auto 0 0 0;
height: 7rem;
background: linear-gradient(180deg, transparent, hsl(var(--background)));
}
.hero-header-bridge {
position: absolute;
left: 50%;
top: 3.5rem;
height: 11rem;
width: min(34rem, 72vw);
transform: translateX(-50%);
background:
radial-gradient(circle at 50% 0%, hsl(var(--hero-left) / 0.18), transparent 56%),
linear-gradient(180deg, hsl(var(--foreground) / 0.06), transparent 72%);
filter: blur(24px);
opacity: 0.9;
pointer-events: none;
}
.hero-content-bridge {
position: absolute;
left: 50%;
bottom: 2.5rem;
height: 9rem;
width: min(44rem, 82vw);
transform: translateX(-50%);
background:
radial-gradient(circle at 50% 50%, hsl(var(--hero-bottom) / 0.2), transparent 58%),
linear-gradient(180deg, transparent, hsl(var(--background) / 0.86));
filter: blur(20px);
opacity: 0.92;
pointer-events: none;
}
}
@media (min-width: 640px) {
.hero-blob-left {
height: 30rem;
width: 30rem;
}
.hero-blob-right {
height: 28rem;
width: 28rem;
}
.hero-blob-bottom {
height: 28rem;
width: 44rem;
}
}
@media (min-width: 1024px) {
.hero-blob-left {
left: -4%;
top: 10%;
height: 36rem;
width: 36rem;
}
.hero-blob-right {
right: -4%;
top: 0;
height: 34rem;
width: 34rem;
}
.hero-blob-bottom {
left: 14%;
bottom: -16%;
height: 32rem;
width: 52rem;
}
}
+66 -1
View File
@@ -1,11 +1,76 @@
import type { Metadata } from "next";
import { unstable_noStore as noStore } from "next/cache";
import localFont from "next/font/local";
import { getLocale } from "next-intl/server";
import { ThemeProvider } from "@/components/theme-provider";
import { buildAppMetadata } from "@/lib/metadata";
import { getDirection } from "@/lib/locale";
import "./globals.css";
const museo = localFont({
src: [
{
path: "./fonts/MuseoSansRounded100.woff2",
weight: "100",
style: "normal",
},
{
path: "./fonts/MuseoSansRounded300.woff2",
weight: "300",
style: "normal",
},
{
path: "./fonts/MuseoSansRounded500.woff2",
weight: "500",
style: "normal",
},
{
path: "./fonts/MuseoSansRounded700.woff2",
weight: "700",
style: "normal",
},
{
path: "./fonts/MuseoSansRounded900.woff2",
weight: "900",
style: "normal",
},
{
path: "./fonts/MuseoSansRounded1000.woff2",
weight: "1000",
style: "normal",
},
],
variable: "--font-museo",
display: "swap",
});
const dubai = localFont({
src: [
{
path: "./fonts/Dubai-Light.woff2",
weight: "300",
style: "normal",
},
{
path: "./fonts/Dubai-Regular.woff2",
weight: "400",
style: "normal",
},
{
path: "./fonts/Dubai-Medium.woff2",
weight: "500",
style: "normal",
},
{
path: "./fonts/Dubai-Bold.woff2",
weight: "700",
style: "normal",
},
],
variable: "--font-dubai",
display: "swap",
});
export const dynamic = "force-dynamic";
export const revalidate = 0;
@@ -26,7 +91,7 @@ export default async function RootLayout({
return (
<html lang={locale} dir={getDirection(locale)} suppressHydrationWarning>
<body>
<body className={`${museo.variable} ${dubai.variable}`}>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
+1 -1
View File
@@ -21,5 +21,5 @@ export const metadata: Metadata = {
};
export default function RootLayout({ children }: RootLayoutProps) {
return children;
return <div className="font-latin">{children}</div>;
}
+100
View File
@@ -0,0 +1,100 @@
"use client";
import { motion } from "framer-motion";
type HeroMotionBackdropProps = {
compact?: boolean;
};
export function HeroMotionBackdrop({ compact = false }: HeroMotionBackdropProps) {
return (
<div className="hero-sheet pointer-events-none absolute inset-0 overflow-hidden">
<div className="hero-sheet-base" />
<motion.div
animate={{
x: compact ? [0, 18, -10, 0] : [0, 44, -24, 0],
y: compact ? [0, -16, 10, 0] : [0, -34, 18, 0],
scale: compact ? [1, 1.03, 0.99, 1] : [1, 1.08, 0.96, 1],
}}
transition={{
duration: compact ? 18 : 26,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="hero-blob hero-blob-left"
/>
<motion.div
animate={{
x: compact ? [0, -14, 8, 0] : [0, -36, 18, 0],
y: compact ? [0, 10, -8, 0] : [0, 22, -14, 0],
scale: compact ? [1, 0.98, 1.04, 1] : [1, 0.94, 1.08, 1],
}}
transition={{
duration: compact ? 20 : 30,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="hero-blob hero-blob-right"
/>
<motion.div
animate={{
x: compact ? [0, 12, -8, 0] : [0, 26, -14, 0],
y: compact ? [0, -6, 8, 0] : [0, -18, 16, 0],
scale: compact ? [1, 1.02, 0.98, 1] : [1, 1.06, 0.97, 1],
opacity: compact ? [0.36, 0.5, 0.4, 0.36] : [0.34, 0.54, 0.4, 0.34],
}}
transition={{
duration: compact ? 16 : 24,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="hero-blob hero-blob-bottom"
/>
<motion.div
animate={{
opacity: compact ? [0.18, 0.26, 0.2, 0.18] : [0.2, 0.3, 0.22, 0.2],
scale: compact ? [1, 1.01, 1, 1] : [1, 1.02, 1, 1],
}}
transition={{
duration: compact ? 16 : 22,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="hero-sheet-glow"
/>
<motion.div
animate={{
x: compact ? [0, 10, -8, 0] : [0, 18, -14, 0],
y: compact ? [0, -8, 6, 0] : [0, -18, 10, 0],
rotate: compact ? [0, 2, -1, 0] : [0, 4, -2, 0],
opacity: compact ? [0.12, 0.2, 0.14, 0.12] : [0.14, 0.24, 0.16, 0.14],
}}
transition={{
duration: compact ? 20 : 28,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="hero-sheet-wave"
/>
<motion.div
animate={{
backgroundPosition: compact
? ["0 0, 0 0", "40px 24px, 0 0", "0 0, 0 0"]
: ["0 0, 0 0", "64px 40px, 0 0", "0 0, 0 0"],
}}
transition={{
duration: compact ? 20 : 28,
repeat: Number.POSITIVE_INFINITY,
ease: "linear",
}}
className="hero-sheet-noise"
/>
<div className="hero-sheet-veil" />
<div className="hero-sheet-fade" />
</div>
);
}
+39 -14
View File
@@ -1,8 +1,7 @@
import { ArrowRight } from "lucide-react";
import Link from "next/link";
import { HeroAtmosphere } from "@/components/layout/hero-atmosphere";
import { MotionFade } from "@/components/motion-fade";
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
import { Button } from "@/components/ui/button";
import { getLocalizedPath } from "@/lib/locale";
@@ -23,34 +22,60 @@ export function HomeHero({
portfolioLabel,
contactLabel,
}: HomeHeroProps) {
const titleLines = title.split("\n").filter(Boolean);
const isArabic = locale === "ar";
return (
<section className="relative isolate flex min-h-[85vh] items-center overflow-hidden">
<HeroAtmosphere />
<section className="hero-surface hero-home relative isolate flex min-h-[88vh] items-center justify-center overflow-hidden">
<HeroMotionBackdrop />
<div className="hero-header-bridge" />
<div className="hero-content-bridge" />
<div className="relative z-10 mx-auto flex w-full max-w-[72rem] flex-col items-center justify-center px-4 pb-12 pt-24 text-center sm:px-6 lg:px-8 lg:pb-16 lg:pt-28">
<MotionFade className="w-full">
<div className="mx-auto max-w-[54rem]">
<p className="text-sm font-medium tracking-[0.08em] text-foreground/62">
<div className="mx-auto flex max-w-[46rem] flex-col items-center">
<p className="text-base font-medium tracking-[-0.03em] text-[hsl(var(--hero-ink)/0.76)] drop-shadow-[0_10px_24px_rgba(15,23,42,0.16)] sm:text-lg">
{kicker}
</p>
<h1 className="mt-6 whitespace-pre-line text-balance text-[3rem] font-semibold leading-[0.92] tracking-[-0.06em] text-foreground sm:text-[4.5rem] lg:text-[6.75rem]">
{title}
<h1
className={
isArabic
? "mt-5 flex w-full max-w-[40rem] flex-col text-[3rem] font-bold leading-[1.12] tracking-normal text-[hsl(var(--hero-ink))] drop-shadow-[0_18px_40px_rgba(15,23,42,0.16)] sm:text-[4.25rem] lg:max-w-[48rem] lg:text-[5.35rem]"
: "mt-4 flex w-full max-w-[36rem] flex-col gap-y-1 text-[3.25rem] font-semibold leading-[0.9] tracking-[-0.085em] text-[hsl(var(--hero-ink))] drop-shadow-[0_18px_40px_rgba(15,23,42,0.22)] sm:max-w-[38rem] sm:text-[4.9rem] lg:max-w-[39rem] lg:gap-y-2 lg:text-[6.15rem]"
}
>
{titleLines.map((line, index) => (
<span
key={`${locale}-${index}-${line}`}
className={
isArabic
? index === 1
? "self-center whitespace-nowrap"
: "self-center whitespace-nowrap bg-[linear-gradient(135deg,hsl(var(--hero-ink)),hsl(var(--hero-right)/0.78))] bg-clip-text text-transparent"
: index === 1
? "self-end"
: index === titleLines.length - 1
? "self-start bg-[linear-gradient(135deg,hsl(var(--hero-ink)),hsl(var(--hero-right)/0.9))] bg-clip-text text-transparent"
: "self-start bg-[linear-gradient(135deg,hsl(var(--hero-ink)),hsl(var(--hero-left)/0.9))] bg-clip-text text-transparent"
}
>
{line}
</span>
))}
</h1>
<p className="mx-auto mt-6 max-w-[30rem] text-sm leading-6 text-foreground/66 sm:text-base">
<p className="mx-auto mt-8 max-w-[30rem] whitespace-pre-line text-sm leading-7 text-[hsl(var(--hero-ink)/0.72)] drop-shadow-[0_8px_18px_rgba(15,23,42,0.12)] sm:text-base">
{description}
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<Button asChild size="lg" className="px-6">
<div className="mt-9 flex flex-wrap items-center justify-center gap-3">
<Button asChild size="lg" className="min-w-[10.5rem] px-6">
<Link href={getLocalizedPath(locale, "/portfolio")}>
{portfolioLabel}
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button asChild variant="outline" size="lg" className="px-6">
<Button asChild variant="outline" size="lg" className="min-w-[10.5rem] border-white/40 bg-white/35 px-6 backdrop-blur-sm hover:bg-white/55 dark:border-white/14 dark:bg-white/6 dark:hover:bg-white/10">
<Link href={getLocalizedPath(locale, "/contact")}>{contactLabel}</Link>
</Button>
</div>
</div>
</MotionFade>
</div>
</section>
);
+35 -13
View File
@@ -1,6 +1,6 @@
"use client";
import { Languages } from "lucide-react";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { Button } from "@/components/ui/button";
@@ -24,9 +24,16 @@ export function LocaleToggle({ locale, className, showLabel = false }: LocaleTog
const locales: AppLocale[] = ["de", "en", "ar"];
const currentPath = stripLocalePrefix(pathname);
const currentLocale = (["de", "en", "ar"].includes(locale) ? locale : "de") as AppLocale;
const availableLocales = locales.filter((targetLocale) => targetLocale !== currentLocale);
const contentFontClassName = currentLocale === "ar" ? "font-arabic" : "font-latin";
const localeFlags: Record<AppLocale, { src: string; alt: string }> = {
de: { src: "/flags/de.svg", alt: "German" },
en: { src: "/flags/us.svg", alt: "English" },
ar: { src: "/flags/sy.svg", alt: "Arabic" },
};
return (
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
type="button"
@@ -34,26 +41,41 @@ export function LocaleToggle({ locale, className, showLabel = false }: LocaleTog
size={showLabel ? "sm" : "icon"}
aria-label={`Locale: ${currentLocale.toUpperCase()}`}
className={cn(
"rounded-pill border border-transparent text-muted-foreground hover:border-border/70 hover:bg-background/80 hover:text-foreground",
"group cursor-pointer rounded-pill border border-transparent text-muted-foreground hover:border-border/70 hover:bg-background/80 hover:text-foreground",
showLabel ? "gap-2 px-3" : undefined,
className,
)}
>
<Languages className="h-4 w-4" />
{showLabel ? <span className="text-xs font-semibold uppercase tracking-[0.22em]">{currentLocale}</span> : null}
<Image
src={localeFlags[currentLocale].src}
alt={localeFlags[currentLocale].alt}
width={18}
height={18}
className="h-[18px] w-[18px] rounded-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[8rem]">
{locales.map((targetLocale) => (
<DropdownMenuItem key={targetLocale} asChild>
<DropdownMenuContent align="end" className={cn("w-auto min-w-0", contentFontClassName)}>
{availableLocales.map((targetLocale) => (
<DropdownMenuItem
key={targetLocale}
asChild
className={cn(
"cursor-pointer",
targetLocale === "ar" ? "font-arabic" : "font-latin",
)}
>
<a
href={getLocalizedPath(targetLocale, currentPath)}
className="flex items-center justify-between gap-4"
className="group flex items-center justify-center"
>
<span>{targetLocale.toUpperCase()}</span>
{targetLocale === currentLocale ? (
<span className="text-xs text-muted-foreground">Active</span>
) : null}
<Image
src={localeFlags[targetLocale].src}
alt={localeFlags[targetLocale].alt}
width={18}
height={18}
className="h-[18px] w-[18px] rounded-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
/>
</a>
</DropdownMenuItem>
))}
+8 -9
View File
@@ -1,5 +1,4 @@
import { HeroAtmosphere } from "@/components/layout/hero-atmosphere";
import { MotionFade } from "@/components/motion-fade";
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
type PageHeroProps = {
title: string;
@@ -8,22 +7,22 @@ type PageHeroProps = {
export function PageHero({ title, description }: PageHeroProps) {
return (
<section className="relative isolate flex min-h-[42vh] items-end overflow-hidden">
<HeroAtmosphere compact />
<section className="hero-surface hero-page relative isolate flex min-h-[42vh] items-end overflow-hidden">
<HeroMotionBackdrop compact />
<div className="relative z-10 mx-auto w-full max-w-[72rem] px-4 pb-10 pt-24 sm:px-6 lg:px-8 lg:pb-12 lg:pt-28">
<MotionFade className="max-w-[44rem]">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-foreground/48">
<div className="mx-auto max-w-[42rem] text-center">
<p className="text-sm font-medium tracking-[-0.02em] text-foreground/58">
{title}
</p>
<h1 className="mt-4 text-balance text-4xl font-semibold leading-[0.95] tracking-[-0.05em] text-foreground sm:text-5xl lg:text-6xl">
<h1 className="mt-4 text-balance text-4xl font-semibold leading-[0.9] tracking-[-0.07em] text-foreground sm:text-5xl lg:text-[4.5rem]">
{title}
</h1>
{description ? (
<p className="mt-5 max-w-[36rem] text-sm leading-6 text-foreground/66 sm:text-base">
<p className="mx-auto mt-5 max-w-[32rem] text-sm leading-6 text-foreground/64 sm:text-base">
{description}
</p>
) : null}
</MotionFade>
</div>
</div>
</section>
);
+25 -18
View File
@@ -61,9 +61,11 @@ function NavLinks({
key={item.key}
href={getLocalizedPath(locale, itemPath)}
className={cn(
"relative z-10 rounded-[var(--radius-pill)] text-sm font-medium transition-colors",
mobile ? "px-4 py-3" : "px-3.5 py-2 xl:px-4",
isActive ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground",
"relative z-10 rounded-[var(--radius-pill)] transition-colors",
mobile ? "px-4 py-3 text-sm font-medium" : "px-3.5 py-2 text-sm font-medium xl:px-4",
isActive
? "text-primary-foreground"
: "text-[hsl(var(--hero-ink)/0.62)] hover:text-[hsl(var(--hero-ink))] dark:text-muted-foreground dark:hover:text-foreground",
)}
onClick={onNavigate}
>
@@ -92,21 +94,23 @@ export function SiteHeader({
const pathname = usePathname();
const locale = useLocale();
const t = useTranslations("navigation");
const isArabic = locale === "ar";
return (
<header className="fixed inset-x-0 top-3 z-40 bg-transparent">
<Container className="max-w-[88rem]">
<div className="relative grid min-h-[4.5rem] grid-cols-[auto_1fr_auto] items-center gap-3 lg:min-h-[5rem]">
<div className="flex items-center">
<div className="col-start-1 flex items-center justify-self-start">
<Link
href={getLocalizedPath(locale)}
className="inline-flex items-center py-1 sm:py-1.5 lg:py-2"
className="inline-flex items-center rounded-[var(--radius-pill)] px-1 py-1 sm:py-1.5 lg:py-2"
>
<SiteLogo
lightLogoUrl={lightLogoUrl}
darkLogoUrl={darkLogoUrl}
alt="mohfarawati"
priority
className="h-7 opacity-88 sm:h-8 lg:h-9"
/>
</Link>
</div>
@@ -116,7 +120,10 @@ export function SiteHeader({
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.28, ease: "easeOut" }}
className="pointer-events-auto inline-flex min-w-[27rem] max-w-[42rem] items-center justify-center gap-1 rounded-[var(--radius-pill)] border border-white/18 bg-white/10 p-1.5 shadow-[0_18px_44px_-28px_rgba(15,23,42,0.24)] backdrop-blur-[24px] dark:border-white/10 dark:bg-white/5 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]"
className={cn(
"pointer-events-auto inline-flex items-center gap-1 rounded-[var(--radius-pill)] border border-black/6 bg-white/24 p-1.5 shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] dark:border-white/10 dark:bg-white/4 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]",
isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]",
)}
>
<NavLinks />
</motion.nav>
@@ -127,15 +134,15 @@ export function SiteHeader({
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.28, ease: "easeOut", delay: 0.03 }}
className="inline-flex items-center gap-1 rounded-[var(--radius-pill)] border border-white/18 bg-white/10 p-1 shadow-[0_18px_44px_-28px_rgba(15,23,42,0.24)] backdrop-blur-[24px] dark:border-white/10 dark:bg-white/5 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]"
className="inline-flex items-center gap-1 rounded-[var(--radius-pill)] border border-black/6 bg-white/24 p-1 shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] dark:border-white/10 dark:bg-white/4 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]"
>
<LocaleToggle
locale={locale}
showLabel
className="h-9 rounded-[var(--radius-pill)] px-3 text-foreground hover:bg-white/34 dark:hover:bg-white/10"
className="h-9 rounded-[var(--radius-pill)] px-3 text-[hsl(var(--hero-ink)/0.76)] hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:bg-white/10"
/>
{isAdmin ? (
<Button asChild variant="ghost" size="icon" className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent text-foreground hover:border-white/12 hover:bg-white/34 dark:hover:border-white/10 dark:hover:bg-white/10">
<Button asChild variant="ghost" size="icon" className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent text-[hsl(var(--hero-ink)/0.76)] hover:border-black/5 hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:border-white/10 dark:hover:bg-white/10">
<Link href="/root" aria-label={t("root")}>
<LayoutDashboard className="h-4 w-4" />
</Link>
@@ -144,7 +151,7 @@ export function SiteHeader({
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent text-foreground hover:border-white/12 hover:bg-white/34 dark:hover:border-white/10 dark:hover:bg-white/10"
className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent text-[hsl(var(--hero-ink)/0.76)] hover:border-black/5 hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:border-white/10 dark:hover:bg-white/10"
/>
</motion.div>
</div>
@@ -154,7 +161,7 @@ export function SiteHeader({
onClick={() => setIsOpen((open) => !open)}
variant="ghost"
size="icon"
className="ml-auto h-11 w-11 rounded-[var(--radius-pill)] border border-white/18 bg-white/12 text-foreground shadow-[0_18px_44px_-28px_rgba(15,23,42,0.24)] backdrop-blur-[24px] hover:bg-white/18 dark:border-white/10 dark:bg-white/5 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)] dark:hover:bg-white/10 lg:hidden"
className="col-start-3 ml-auto h-11 w-11 justify-self-end rounded-[var(--radius-pill)] border border-black/6 bg-white/24 text-[hsl(var(--hero-ink))] shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] hover:bg-white/34 dark:border-white/10 dark:bg-white/4 dark:text-foreground dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)] dark:hover:bg-white/10 lg:hidden"
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
>
<motion.span
@@ -183,7 +190,7 @@ export function SiteHeader({
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="overflow-hidden rounded-surface border border-white/24 bg-[linear-gradient(180deg,rgba(255,255,255,0.24),rgba(255,255,255,0.1))] shadow-[inset_0_1px_0_rgba(255,255,255,0.46),inset_0_-1px_0_rgba(255,255,255,0.08),0_18px_44px_-28px_rgba(15,23,42,0.32)] backdrop-blur-[28px] dark:border-white/12 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.09),rgba(255,255,255,0.03))] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.12),inset_0_-1px_0_rgba(255,255,255,0.03),0_20px_48px_-28px_rgba(0,0,0,0.72)]"
className="overflow-hidden rounded-surface border border-black/6 bg-[linear-gradient(180deg,rgba(255,255,255,0.46),rgba(255,255,255,0.24))] shadow-[inset_0_1px_0_rgba(255,255,255,0.54),0_16px_34px_-24px_rgba(15,23,42,0.24)] backdrop-blur-[20px] dark:border-white/12 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.09),rgba(255,255,255,0.03))] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.12),inset_0_-1px_0_rgba(255,255,255,0.03),0_20px_48px_-28px_rgba(0,0,0,0.72)]"
>
<motion.nav
initial="closed"
@@ -223,8 +230,8 @@ export function SiteHeader({
className={cn(
"flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-foreground hover:bg-accent hover:text-accent-foreground",
? "bg-white/72 text-[hsl(var(--hero-ink))] shadow-[0_10px_20px_-18px_rgba(15,23,42,0.24)] dark:bg-white/10 dark:text-foreground"
: "text-[hsl(var(--hero-ink)/0.78)] hover:bg-white/40 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:bg-white/6",
)}
onClick={() => setIsOpen(false)}
>
@@ -243,17 +250,17 @@ export function SiteHeader({
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.18, ease: "easeOut", delay: 0.04 }}
className="border-t border-white/18 bg-[linear-gradient(180deg,rgba(255,255,255,0.14),rgba(255,255,255,0.06))] p-2 dark:border-white/8 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]"
className="border-t border-black/6 bg-[linear-gradient(180deg,rgba(255,255,255,0.2),rgba(255,255,255,0.08))] p-2 dark:border-white/8 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]"
>
<div className="flex items-center justify-between gap-2">
<LocaleToggle
locale={locale}
showLabel
className="h-10 rounded-nested px-3 text-foreground hover:bg-white/34 dark:hover:bg-white/10"
className="h-10 rounded-nested px-3 text-[hsl(var(--hero-ink)/0.8)] hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:bg-white/10"
/>
<div className="flex items-center gap-2">
{isAdmin ? (
<Button asChild variant="ghost" size="icon" className="h-10 w-10 rounded-nested border border-white/18 bg-white/18 text-foreground hover:bg-white/34 dark:border-white/10 dark:bg-white/6 dark:hover:bg-white/10">
<Button asChild variant="ghost" size="icon" className="h-10 w-10 rounded-nested border border-black/6 bg-white/32 text-[hsl(var(--hero-ink)/0.8)] hover:bg-white/42 hover:text-[hsl(var(--hero-ink))] dark:border-white/10 dark:bg-white/6 dark:text-foreground dark:hover:bg-white/10">
<Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}>
<LayoutDashboard className="h-4 w-4" />
</Link>
@@ -262,7 +269,7 @@ export function SiteHeader({
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
className="h-10 w-10 rounded-nested border border-white/18 bg-white/18 text-foreground hover:bg-white/34 dark:border-white/10 dark:bg-white/6 dark:hover:bg-white/10"
className="h-10 w-10 rounded-nested border border-black/6 bg-white/32 text-[hsl(var(--hero-ink)/0.8)] hover:bg-white/42 hover:text-[hsl(var(--hero-ink))] dark:border-white/10 dark:bg-white/6 dark:text-foreground dark:hover:bg-white/10"
/>
</div>
</div>
+15 -3
View File
@@ -1,14 +1,18 @@
"use client";
import { motion } from "framer-motion";
import Image from "next/image";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
type SiteLogoProps = {
lightLogoUrl?: string | null;
darkLogoUrl?: string | null;
alt: string;
priority?: boolean;
className?: string;
};
export function SiteLogo({
@@ -16,6 +20,7 @@ export function SiteLogo({
darkLogoUrl,
alt,
priority = false,
className,
}: SiteLogoProps) {
const { theme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
@@ -30,16 +35,23 @@ export function SiteLogo({
const src = isDark ? darkLogoUrl || lightLogoUrl || fallbackSrc : lightLogoUrl || darkLogoUrl || fallbackSrc;
return (
<span className="block">
<motion.span
className="block"
whileHover={{ scale: 1.04, y: -1 }}
transition={{ duration: 0.28, ease: "easeOut" }}
>
<Image
src={src}
alt={alt}
width={360}
height={92}
sizes="(min-width: 1280px) 360px, (min-width: 1024px) 320px, (min-width: 640px) 280px, 240px"
className="h-9 w-auto object-contain object-left sm:h-10 lg:h-11 xl:h-12"
className={cn(
"h-9 w-auto object-contain object-left drop-shadow-[0_10px_24px_rgba(15,23,42,0.12)] transition-[filter,opacity,transform] duration-300 ease-out sm:h-10 lg:h-11 xl:h-12",
className,
)}
priority={priority}
/>
</span>
</motion.span>
);
}
+8 -4
View File
@@ -36,9 +36,9 @@ export function ThemeToggle({
variant={variant}
size={label ? "sm" : "icon"}
aria-label={ariaLabel}
className={cn(label ? "justify-start" : undefined, className)}
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
>
<Moon className={cn("h-4 w-4", iconClassName)} />
<Moon className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
{label ? <span>{label}</span> : null}
</Button>
);
@@ -54,9 +54,13 @@ export function ThemeToggle({
size={label ? "sm" : "icon"}
onClick={() => setTheme(isDark ? "light" : "dark")}
aria-label={ariaLabel}
className={cn(label ? "justify-start" : undefined, className)}
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
>
{isDark ? <Sun className={cn("h-4 w-4", iconClassName)} /> : <Moon className={cn("h-4 w-4", iconClassName)} />}
{isDark ? (
<Sun className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
) : (
<Moon className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
)}
{label ? <span>{label}</span> : null}
</Button>
);
+1 -1
View File
@@ -60,7 +60,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-surface border border-border/80 bg-popover p-1 text-popover-foreground shadow-panel",
"z-50 min-w-[12rem] overflow-hidden rounded-surface border border-border/80 bg-popover p-1 text-popover-foreground shadow-panel data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
+21 -17
View File
@@ -1,10 +1,10 @@
{
"navigation": {
"home": "الرئيسية",
"portfolio": "الأعمال",
"products": "المنتجات",
"about": ن أنا",
"contact": "تواصل",
"home": "هومبيج",
"portfolio": "معرض أعمالي",
"products": "منتجاتي",
"about": علومات عني",
"contact": "تواصل معي",
"root": "Root",
"openMenu": "فتح القائمة",
"closeMenu": "إغلاق القائمة",
@@ -22,20 +22,24 @@
"note": "شكراً لصبرك. النسخة الجديدة ستصل قريباً بهوية أنضج وحضور أدق."
},
"homepage": {
"heroKicker": "مرحباً، أنا مو",
"heroTitle": "frontend\n developer\n& designer",
"heroText": "أصمم وأبني تجارب رقمية نظيفة مع اهتمام دقيق بالتفاصيل والحركة وسهولة الاستخدام.",
"portfolioTitle": "أعمال مميزة",
"portfolioText": "منطقة تجريبية لعرض مشاريع مختارة.",
"productsTitle": "منتجات مميزة",
"productsText": "منطقة تجريبية لأهم العروض والمنتجات.",
"ctaTitle": "جاهز للخطوة التالية؟",
"ctaText": "فيك تراسلني لنحدد النطاق الأنسب لمشروعك.",
"heroKicker": "مرحبااا، أنا محمد",
"heroTitle": "مطور واجهات ويب\nومصمم جرافيك",
"heroText": "بشتغل على حلول رقمية لمشاكل حقيقية\nبتطلع ممتازة وبتشتغل بكفاءة.",
"servicesEyebrow": "كيف بقدر ساعدك",
"servicesTitle": "أقدر أساعدك تبني حضور رقمي مرتب وفعّال.",
"servicesText": "إذا بدك تطلق براند، موقع، متجر، إضافة ووردبريس، أو بدك ترتب السيو والتراكنغ وتبني سستم خاص لشغلك، بقدر ساعدك من الفكرة للتنفيذ بشكل واضح وعملي.",
"servicesPrimaryCta": "احكيلي عن مشروعك",
"servicesSecondaryCta": "شوف شغلي",
"serviceBrand": "بقدر ساعدك تبني براند واضح ومرتب.",
"serviceWebsite": "بقدر ساعدك تبني موقع إلكتروني يليق بشغلك.",
"serviceStore": "بقدر ساعدك تبني متجر يبيع بشكل أوضح وأسهل.",
"serviceWordPress": "بقدر ساعدك تبني إضافة WordPress خاصة فيك.",
"serviceSeo": "بقدر ساعدك تحسن ظهور موقعك بمحركات البحث.",
"serviceTracking": "بقدر ساعدك تضبط التراكنغ وتفهم حركة الزوار.",
"serviceSystem": "بقدر ساعدك تبني سستم خاص لشركتك أو لموظفينك.",
"toPortfolio": "عرض الأعمال",
"toProducts": "عرض المنتجات",
"toContact": "تواصل معي",
"heroCardTitle": "إطلاق سريع",
"heroCardText": "هيكل ومحتوى ومكونات تساعد على نمو سريع وواضح."
"toContact": "تواصل معي"
},
"portfolioPage": {
"title": "الأعمال",
+16 -12
View File
@@ -1,6 +1,6 @@
{
"navigation": {
"home": "Start",
"home": "Startseite",
"portfolio": "Portfolio",
"products": "Produkte",
"about": "Über mich",
@@ -23,19 +23,23 @@
},
"homepage": {
"heroKicker": "Hi, ich bin Moh",
"heroTitle": "frontend\n developer\n& designer",
"heroText": "Ich entwerfe und entwickle klare digitale Erlebnisse mit Fokus auf Details, Motion und starke Interfaces.",
"portfolioTitle": "Featured Projects",
"portfolioText": "Platzhalter fuer ausgewaehlte Kundenprojekte.",
"productsTitle": "Featured Products",
"productsText": "Platzhalter fuer die wichtigsten Produktangebote.",
"ctaTitle": "Bereit fuer den naechsten Schritt?",
"ctaText": "Wir planen zusammen den passenden Scope fuer dein Projekt.",
"heroTitle": "frontend\ndeveloper\n& designer",
"heroText": "Ich entwickle digitale Loesungen fuer echte Probleme,\ndie gut aussehen und sauber funktionieren.",
"servicesEyebrow": "Wobei ich helfen kann",
"servicesTitle": "Ich kann dir helfen, den richtigen digitalen Aufbau fuer dein Business zu schaffen.",
"servicesText": "Von Brand Aufbau und Websites bis zu Shops, WordPress Plugins, SEO, Tracking und internen Systemen helfe ich dir dabei, die passende digitale Loesung klar und sauber umzusetzen.",
"servicesPrimaryCta": "Erzaehl mir von deinem Projekt",
"servicesSecondaryCta": "Meine Arbeiten ansehen",
"serviceBrand": "Ich kann dir helfen, eine klare und starke Marke aufzubauen.",
"serviceWebsite": "Ich kann dir helfen, eine Website fuer dein Business zu bauen.",
"serviceStore": "Ich kann dir helfen, einen Shop mit besserer Klarheit aufzubauen.",
"serviceWordPress": "Ich kann dir helfen, ein eigenes WordPress Plugin zu entwickeln.",
"serviceSeo": "Ich kann dir helfen, deine Sichtbarkeit in Suchmaschinen zu verbessern.",
"serviceTracking": "Ich kann dir helfen, Tracking sauber aufzusetzen und auszuwerten.",
"serviceSystem": "Ich kann dir helfen, ein eigenes System fuer deine Firma oder dein Team zu bauen.",
"toPortfolio": "Portfolio ansehen",
"toProducts": "Produkte ansehen",
"toContact": "Kontakt aufnehmen",
"heroCardTitle": "Schneller Rollout",
"heroCardText": "Struktur, Content und Komponenten fuer schnelles Wachstum."
"toContact": "Kontakt aufnehmen"
},
"portfolioPage": {
"title": "Portfolio",
+17 -13
View File
@@ -1,9 +1,9 @@
{
"navigation": {
"home": "Home",
"portfolio": "Portfolio",
"portfolio": "My Portfolio",
"products": "Products",
"about": "About",
"about": "About Me",
"contact": "Contact",
"root": "Root",
"openMenu": "Open menu",
@@ -23,19 +23,23 @@
},
"homepage": {
"heroKicker": "Hi, I am Moh",
"heroTitle": "frontend\ndelevoper\n& designer",
"heroText": "I design and build clean digital experiences with a sharp eye for detail, motion, and usable interfaces.",
"portfolioTitle": "Featured Projects",
"portfolioText": "Placeholder area for highlighted client projects.",
"productsTitle": "Featured Products",
"productsText": "Placeholder area for top product offerings.",
"ctaTitle": "Ready for your next step?",
"ctaText": "We can shape the right project scope together.",
"heroTitle": "frontend\ndeveloper\n& designer",
"heroText": "I craft digital solutions for human problems\nthat look good and work well.",
"servicesEyebrow": "How I Can Help",
"servicesTitle": "I can help you build the right digital setup for your business.",
"servicesText": "From brand foundations and websites to stores, WordPress plugins, SEO, tracking, and internal systems, I can help shape, build, and refine the setup your business actually needs.",
"servicesPrimaryCta": "Tell me about your project",
"servicesSecondaryCta": "See my work",
"serviceBrand": "I can help you build a clear and reliable brand.",
"serviceWebsite": "I can help you build a website that fits your business.",
"serviceStore": "I can help you launch an online store that sells more clearly.",
"serviceWordPress": "I can help you build a custom WordPress plugin.",
"serviceSeo": "I can help you improve your visibility in search engines.",
"serviceTracking": "I can help you set up tracking and understand user behavior.",
"serviceSystem": "I can help you build a custom system for your company or team.",
"toPortfolio": "View portfolio",
"toProducts": "View products",
"toContact": "Contact me",
"heroCardTitle": "Fast rollout",
"heroCardText": "Structure, content, and components for rapid growth."
"toContact": "Contact me"
},
"portfolioPage": {
"title": "Portfolio",
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-de" viewBox="0 0 512 512">
<path fill="#fc0" d="M0 341.3h512V512H0z"/>
<path fill="#000001" d="M0 0h512v170.7H0z"/>
<path fill="red" d="M0 170.7h512v170.6H0z"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="flag-icons-sy" viewBox="0 0 512 512">
<path d="M0 0h512v512H0Z"/>
<path fill="#fff" d="M0 0h512v341.3H0Z"/>
<path fill="#007a3d" d="M0 0h512v170.7H0Z"/>
<path fill="#ce1126" d="M26.3 317.9 67.2 192 108 317.9 1 240h132.4m270.5 77.8L445 192l40.8 125.9-107-77.8H511m-295.9 77.8L256 192l40.9 125.9-107-77.8h132.3"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 512 512">
<path fill="#bd3d44" d="M0 0h512v512H0"/>
<path stroke="#fff" stroke-width="40" d="M0 58h512M0 137h512M0 216h512M0 295h512M0 374h512M0 453h512"/>
<path fill="#192f5d" d="M0 0h390v275H0z"/>
<marker id="us-a" markerHeight="30" markerWidth="30">
<path fill="#fff" d="m15 0 9.3 28.6L0 11h30L5.7 28.6"/>
</marker>
<path fill="none" marker-mid="url(#us-a)" d="m0 0 18 11h65 65 65 65 66L51 39h65 65 65 65L18 66h65 65 65 65 66L51 94h65 65 65 65L18 121h65 65 65 65 66L51 149h65 65 65 65L18 177h65 65 65 65 66L51 205h65 65 65 65L18 232h65 65 65 65 66z"/>
</svg>

After

Width:  |  Height:  |  Size: 650 B