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
+78 -114
View File
@@ -1,9 +1,13 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { import {
ArrowRight, ArrowRight,
BadgeCheck,
Blocks,
BriefcaseBusiness, BriefcaseBusiness,
Boxes, ChartNoAxesCombined,
Mail, Globe,
Package2,
Search,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -17,11 +21,6 @@ import { AppCard } from "@/components/ui/app-card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card";
import { getLocalizedPath, resolveLocale } from "@/lib/locale"; import { getLocalizedPath, resolveLocale } from "@/lib/locale";
import {
getLocalizedValue,
getPublishedPortfolioProjects,
} from "@/lib/portfolio";
import { pickText, productItems } from "@/lib/site-data";
type HomePageProps = { type HomePageProps = {
params: { params: {
@@ -29,6 +28,16 @@ type HomePageProps = {
}; };
}; };
const serviceIcons = [
BriefcaseBusiness,
Globe,
Package2,
Blocks,
Search,
ChartNoAxesCombined,
BadgeCheck,
];
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function generateMetadata({ export async function generateMetadata({
@@ -51,9 +60,16 @@ export async function generateMetadata({
export default async function HomePage({ params: { locale } }: HomePageProps) { export default async function HomePage({ params: { locale } }: HomePageProps) {
const localeKey = resolveLocale(locale); 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 t = await getTranslations({ locale: localeKey, namespace: "homepage" });
const serviceItems = [
t("serviceBrand"),
t("serviceWebsite"),
t("serviceStore"),
t("serviceWordPress"),
t("serviceSeo"),
t("serviceTracking"),
t("serviceSystem"),
];
return ( return (
<> <>
@@ -66,116 +82,64 @@ export default async function HomePage({ params: { locale } }: HomePageProps) {
contactLabel={t("toContact")} 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}> <MotionFade delay={0.05}>
<AppCard> <AppCard
<CardContent className="p-6 lg:p-8"> level="inverse"
<div className="mb-6 flex items-center justify-between gap-4"> className="overflow-hidden border border-white/6 bg-[linear-gradient(145deg,hsl(var(--foreground)),hsl(221_42%_10%))] text-primary-foreground"
<div> >
<h2 className="text-2xl font-semibold text-foreground"> <CardContent className="p-6 lg:p-10">
{t("portfolioTitle")} <div className="grid gap-8 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)] lg:items-start">
</h2> <div>
<p className="mt-2 text-sm text-muted-foreground"> <p className="text-sm font-medium tracking-[0.08em] text-primary-foreground/62">
{t("portfolioText")} {t("servicesEyebrow")}
</p> </p>
</div> <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">
<BriefcaseBusiness className="h-5 w-5 text-muted-foreground" /> {t("servicesTitle")}
</div> </h2>
<div className="grid gap-4 md:grid-cols-3"> <p className="mt-5 max-w-[34rem] text-sm leading-7 text-primary-foreground/76 sm:text-base">
{featuredProjects.map((item) => ( {t("servicesText")}
<AppCard key={item.slug} interactive> </p>
<CardContent className="p-5"> <div className="mt-7 flex flex-wrap gap-3">
<Link <Button asChild size="lg" variant="secondary" className="min-w-[11rem]">
href={getLocalizedPath(localeKey, `/portfolio/${item.slug}`)} <Link href={getLocalizedPath(localeKey, "/contact")}>
className="group block" {t("servicesPrimaryCta")}
>
<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" /> <ArrowRight className="h-4 w-4" />
</span> </Link>
</Link> </Button>
</CardContent> <Button
</AppCard> asChild
))} size="lg"
</div> variant="outline"
</CardContent> className="min-w-[11rem] border-white/16 bg-white/6 text-primary-foreground hover:bg-white/10 hover:text-primary-foreground"
</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">
<div>
<h2 className="text-2xl font-semibold text-foreground">
{t("productsTitle")}
</h2>
<p className="mt-2 text-sm text-muted-foreground">
{t("productsText")}
</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"> <Link href={getLocalizedPath(localeKey, "/portfolio")}>
{pickText(item.segment, localeKey)} {t("servicesSecondaryCta")}
</p> </Link>
<h3 className="mt-2 text-lg font-semibold text-foreground"> </Button>
{pickText(item.name, localeKey)} </div>
</h3> </div>
<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}> <div className="grid gap-3 sm:grid-cols-2">
<AppCard level="inverse"> {serviceItems.map((item, index) => {
<CardContent className="p-6 lg:p-8"> const Icon = serviceIcons[index];
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div> return (
<p className="inline-flex items-center gap-2 text-sm font-medium text-primary-foreground/80"> <div
<Mail className="h-4 w-4" /> key={item}
{t("ctaTitle")} 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"
</p> >
<p className="mt-2 max-w-2xl text-sm text-primary-foreground/88 sm:text-base"> <Icon className="h-5 w-5 text-primary-foreground/72" />
{t("ctaText")} <p className="mt-4 text-sm font-medium leading-6 text-primary-foreground/88 sm:text-base">
</p> {item}
</p>
</div>
);
})}
</div>
</div> </div>
<Button asChild variant="secondary"> </CardContent>
<Link href={getLocalizedPath(localeKey, "/contact")}> </AppCard>
{t("toContact")}
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</AppCard>
</MotionFade> </MotionFade>
</Container> </Container>
</> </>
+4 -1
View File
@@ -34,10 +34,13 @@ export default async function LocaleLayout({
setRequestLocale(locale); setRequestLocale(locale);
const messages = await getMessages(); const messages = await getMessages();
const localeClassName = locale === "ar" ? "font-arabic" : "font-latin";
return ( return (
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
<div lang={locale} dir={getDirection(locale)}>{children}</div> <div lang={locale} dir={getDirection(locale)} className={localeClassName}>
{children}
</div>
</NextIntlClientProvider> </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-primary: 9 73% 50%;
--brand-secondary: 12 64% 58%; --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-primary-alpha: 0.06;
--ambient-secondary-alpha: 0.04; --ambient-secondary-alpha: 0.04;
--ambient-grid-alpha: 0; --ambient-grid-alpha: 0;
@@ -114,6 +119,11 @@
--brand-primary: 9 78% 56%; --brand-primary: 9 78% 56%;
--brand-secondary: 12 74% 64%; --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-primary-alpha: 0.08;
--ambient-secondary-alpha: 0.05; --ambient-secondary-alpha: 0.05;
--ambient-grid-alpha: 0; --ambient-grid-alpha: 0;
@@ -139,12 +149,25 @@
html { html {
color-scheme: light; color-scheme: light;
background-color: hsl(var(--background)); background-color: hsl(var(--background));
font-family: var(--font-museo), sans-serif;
} }
html.dark { html.dark {
color-scheme: 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 { @layer base {
* { * {
@apply border-border; @apply border-border;
@@ -154,11 +177,19 @@ html.dark {
@apply min-h-screen text-foreground antialiased; @apply min-h-screen text-foreground antialiased;
background-color: hsl(var(--background)); background-color: hsl(var(--background));
background-image: none; background-image: none;
font-family: inherit;
} }
a { a {
@apply transition-colors; @apply transition-colors;
} }
button,
input,
select,
textarea {
font: inherit;
}
} }
::selection { ::selection {
@@ -214,4 +245,188 @@ html.dark {
transparent 32% 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 type { Metadata } from "next";
import { unstable_noStore as noStore } from "next/cache"; import { unstable_noStore as noStore } from "next/cache";
import localFont from "next/font/local";
import { getLocale } from "next-intl/server"; import { getLocale } from "next-intl/server";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { buildAppMetadata } from "@/lib/metadata"; import { buildAppMetadata } from "@/lib/metadata";
import { getDirection } from "@/lib/locale"; import { getDirection } from "@/lib/locale";
import "./globals.css"; 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 dynamic = "force-dynamic";
export const revalidate = 0; export const revalidate = 0;
@@ -26,7 +91,7 @@ export default async function RootLayout({
return ( return (
<html lang={locale} dir={getDirection(locale)} suppressHydrationWarning> <html lang={locale} dir={getDirection(locale)} suppressHydrationWarning>
<body> <body className={`${museo.variable} ${dubai.variable}`}>
<ThemeProvider>{children}</ThemeProvider> <ThemeProvider>{children}</ThemeProvider>
</body> </body>
</html> </html>
+1 -1
View File
@@ -21,5 +21,5 @@ export const metadata: Metadata = {
}; };
export default function RootLayout({ children }: RootLayoutProps) { 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>
);
}
+52 -27
View File
@@ -1,8 +1,7 @@
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { HeroAtmosphere } from "@/components/layout/hero-atmosphere"; import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
import { MotionFade } from "@/components/motion-fade";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { getLocalizedPath } from "@/lib/locale"; import { getLocalizedPath } from "@/lib/locale";
@@ -23,34 +22,60 @@ export function HomeHero({
portfolioLabel, portfolioLabel,
contactLabel, contactLabel,
}: HomeHeroProps) { }: HomeHeroProps) {
const titleLines = title.split("\n").filter(Boolean);
const isArabic = locale === "ar";
return ( return (
<section className="relative isolate flex min-h-[85vh] items-center overflow-hidden"> <section className="hero-surface hero-home relative isolate flex min-h-[88vh] items-center justify-center overflow-hidden">
<HeroAtmosphere /> <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"> <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 flex max-w-[46rem] flex-col items-center">
<div className="mx-auto max-w-[54rem]"> <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">
<p className="text-sm font-medium tracking-[0.08em] text-foreground/62"> {kicker}
{kicker} </p>
</p> <h1
<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]"> className={
{title} isArabic
</h1> ? "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]"
<p className="mx-auto mt-6 max-w-[30rem] text-sm leading-6 text-foreground/66 sm:text-base"> : "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]"
{description} }
</p> >
<div className="mt-8 flex flex-wrap items-center justify-center gap-3"> {titleLines.map((line, index) => (
<Button asChild size="lg" className="px-6"> <span
<Link href={getLocalizedPath(locale, "/portfolio")}> key={`${locale}-${index}-${line}`}
{portfolioLabel} className={
<ArrowRight className="h-4 w-4" /> isArabic
</Link> ? index === 1
</Button> ? "self-center whitespace-nowrap"
<Button asChild variant="outline" size="lg" className="px-6"> : "self-center whitespace-nowrap bg-[linear-gradient(135deg,hsl(var(--hero-ink)),hsl(var(--hero-right)/0.78))] bg-clip-text text-transparent"
<Link href={getLocalizedPath(locale, "/contact")}>{contactLabel}</Link> : index === 1
</Button> ? "self-end"
</div> : 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-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-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="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>
</div> </div>
</section> </section>
); );
+35 -13
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { Languages } from "lucide-react"; import Image from "next/image";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Button } from "@/components/ui/button"; 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 locales: AppLocale[] = ["de", "en", "ar"];
const currentPath = stripLocalePrefix(pathname); const currentPath = stripLocalePrefix(pathname);
const currentLocale = (["de", "en", "ar"].includes(locale) ? locale : "de") as AppLocale; 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 ( return (
<DropdownMenu> <DropdownMenu modal={false}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
type="button" type="button"
@@ -34,26 +41,41 @@ export function LocaleToggle({ locale, className, showLabel = false }: LocaleTog
size={showLabel ? "sm" : "icon"} size={showLabel ? "sm" : "icon"}
aria-label={`Locale: ${currentLocale.toUpperCase()}`} aria-label={`Locale: ${currentLocale.toUpperCase()}`}
className={cn( 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, showLabel ? "gap-2 px-3" : undefined,
className, className,
)} )}
> >
<Languages className="h-4 w-4" /> <Image
{showLabel ? <span className="text-xs font-semibold uppercase tracking-[0.22em]">{currentLocale}</span> : null} 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> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[8rem]"> <DropdownMenuContent align="end" className={cn("w-auto min-w-0", contentFontClassName)}>
{locales.map((targetLocale) => ( {availableLocales.map((targetLocale) => (
<DropdownMenuItem key={targetLocale} asChild> <DropdownMenuItem
key={targetLocale}
asChild
className={cn(
"cursor-pointer",
targetLocale === "ar" ? "font-arabic" : "font-latin",
)}
>
<a <a
href={getLocalizedPath(targetLocale, currentPath)} href={getLocalizedPath(targetLocale, currentPath)}
className="flex items-center justify-between gap-4" className="group flex items-center justify-center"
> >
<span>{targetLocale.toUpperCase()}</span> <Image
{targetLocale === currentLocale ? ( src={localeFlags[targetLocale].src}
<span className="text-xs text-muted-foreground">Active</span> alt={localeFlags[targetLocale].alt}
) : null} width={18}
height={18}
className="h-[18px] w-[18px] rounded-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
/>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
+8 -9
View File
@@ -1,5 +1,4 @@
import { HeroAtmosphere } from "@/components/layout/hero-atmosphere"; import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
import { MotionFade } from "@/components/motion-fade";
type PageHeroProps = { type PageHeroProps = {
title: string; title: string;
@@ -8,22 +7,22 @@ type PageHeroProps = {
export function PageHero({ title, description }: PageHeroProps) { export function PageHero({ title, description }: PageHeroProps) {
return ( return (
<section className="relative isolate flex min-h-[42vh] items-end overflow-hidden"> <section className="hero-surface hero-page relative isolate flex min-h-[42vh] items-end overflow-hidden">
<HeroAtmosphere compact /> <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"> <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]"> <div className="mx-auto max-w-[42rem] text-center">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-foreground/48"> <p className="text-sm font-medium tracking-[-0.02em] text-foreground/58">
{title} {title}
</p> </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} {title}
</h1> </h1>
{description ? ( {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} {description}
</p> </p>
) : null} ) : null}
</MotionFade> </div>
</div> </div>
</section> </section>
); );
+25 -18
View File
@@ -61,9 +61,11 @@ function NavLinks({
key={item.key} key={item.key}
href={getLocalizedPath(locale, itemPath)} href={getLocalizedPath(locale, itemPath)}
className={cn( className={cn(
"relative z-10 rounded-[var(--radius-pill)] text-sm font-medium transition-colors", "relative z-10 rounded-[var(--radius-pill)] transition-colors",
mobile ? "px-4 py-3" : "px-3.5 py-2 xl:px-4", 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-muted-foreground hover:text-foreground", 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} onClick={onNavigate}
> >
@@ -92,21 +94,23 @@ export function SiteHeader({
const pathname = usePathname(); const pathname = usePathname();
const locale = useLocale(); const locale = useLocale();
const t = useTranslations("navigation"); const t = useTranslations("navigation");
const isArabic = locale === "ar";
return ( return (
<header className="fixed inset-x-0 top-3 z-40 bg-transparent"> <header className="fixed inset-x-0 top-3 z-40 bg-transparent">
<Container className="max-w-[88rem]"> <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="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 <Link
href={getLocalizedPath(locale)} 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 <SiteLogo
lightLogoUrl={lightLogoUrl} lightLogoUrl={lightLogoUrl}
darkLogoUrl={darkLogoUrl} darkLogoUrl={darkLogoUrl}
alt="mohfarawati" alt="mohfarawati"
priority priority
className="h-7 opacity-88 sm:h-8 lg:h-9"
/> />
</Link> </Link>
</div> </div>
@@ -116,7 +120,10 @@ export function SiteHeader({
initial={{ opacity: 0, y: -8 }} initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.28, ease: "easeOut" }} 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 /> <NavLinks />
</motion.nav> </motion.nav>
@@ -127,15 +134,15 @@ export function SiteHeader({
initial={{ opacity: 0, y: -8 }} initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.28, ease: "easeOut", delay: 0.03 }} 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 <LocaleToggle
locale={locale} locale={locale}
showLabel 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 ? ( {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")}> <Link href="/root" aria-label={t("root")}>
<LayoutDashboard className="h-4 w-4" /> <LayoutDashboard className="h-4 w-4" />
</Link> </Link>
@@ -144,7 +151,7 @@ export function SiteHeader({
<ThemeToggle <ThemeToggle
ariaLabel={t("themeToggle")} ariaLabel={t("themeToggle")}
variant="ghost" 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> </motion.div>
</div> </div>
@@ -154,7 +161,7 @@ export function SiteHeader({
onClick={() => setIsOpen((open) => !open)} onClick={() => setIsOpen((open) => !open)}
variant="ghost" variant="ghost"
size="icon" 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")} aria-label={isOpen ? t("closeMenu") : t("openMenu")}
> >
<motion.span <motion.span
@@ -183,7 +190,7 @@ export function SiteHeader({
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }} exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: 0.2, ease: "easeOut" }} 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 <motion.nav
initial="closed" initial="closed"
@@ -223,8 +230,8 @@ export function SiteHeader({
className={cn( className={cn(
"flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors", "flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors",
isActive isActive
? "bg-primary text-primary-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-foreground hover:bg-accent hover:text-accent-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)} onClick={() => setIsOpen(false)}
> >
@@ -243,17 +250,17 @@ export function SiteHeader({
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }} exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.18, ease: "easeOut", delay: 0.04 }} 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"> <div className="flex items-center justify-between gap-2">
<LocaleToggle <LocaleToggle
locale={locale} locale={locale}
showLabel 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"> <div className="flex items-center gap-2">
{isAdmin ? ( {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)}> <Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}>
<LayoutDashboard className="h-4 w-4" /> <LayoutDashboard className="h-4 w-4" />
</Link> </Link>
@@ -262,7 +269,7 @@ export function SiteHeader({
<ThemeToggle <ThemeToggle
ariaLabel={t("themeToggle")} ariaLabel={t("themeToggle")}
variant="ghost" 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>
</div> </div>
+15 -3
View File
@@ -1,14 +1,18 @@
"use client"; "use client";
import { motion } from "framer-motion";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
type SiteLogoProps = { type SiteLogoProps = {
lightLogoUrl?: string | null; lightLogoUrl?: string | null;
darkLogoUrl?: string | null; darkLogoUrl?: string | null;
alt: string; alt: string;
priority?: boolean; priority?: boolean;
className?: string;
}; };
export function SiteLogo({ export function SiteLogo({
@@ -16,6 +20,7 @@ export function SiteLogo({
darkLogoUrl, darkLogoUrl,
alt, alt,
priority = false, priority = false,
className,
}: SiteLogoProps) { }: SiteLogoProps) {
const { theme, resolvedTheme } = useTheme(); const { theme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
@@ -30,16 +35,23 @@ export function SiteLogo({
const src = isDark ? darkLogoUrl || lightLogoUrl || fallbackSrc : lightLogoUrl || darkLogoUrl || fallbackSrc; const src = isDark ? darkLogoUrl || lightLogoUrl || fallbackSrc : lightLogoUrl || darkLogoUrl || fallbackSrc;
return ( return (
<span className="block"> <motion.span
className="block"
whileHover={{ scale: 1.04, y: -1 }}
transition={{ duration: 0.28, ease: "easeOut" }}
>
<Image <Image
src={src} src={src}
alt={alt} alt={alt}
width={360} width={360}
height={92} height={92}
sizes="(min-width: 1280px) 360px, (min-width: 1024px) 320px, (min-width: 640px) 280px, 240px" 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} priority={priority}
/> />
</span> </motion.span>
); );
} }
+8 -4
View File
@@ -36,9 +36,9 @@ export function ThemeToggle({
variant={variant} variant={variant}
size={label ? "sm" : "icon"} size={label ? "sm" : "icon"}
aria-label={ariaLabel} 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} {label ? <span>{label}</span> : null}
</Button> </Button>
); );
@@ -54,9 +54,13 @@ export function ThemeToggle({
size={label ? "sm" : "icon"} size={label ? "sm" : "icon"}
onClick={() => setTheme(isDark ? "light" : "dark")} onClick={() => setTheme(isDark ? "light" : "dark")}
aria-label={ariaLabel} 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} {label ? <span>{label}</span> : null}
</Button> </Button>
); );
+1 -1
View File
@@ -60,7 +60,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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, className,
)} )}
{...props} {...props}
+21 -17
View File
@@ -1,10 +1,10 @@
{ {
"navigation": { "navigation": {
"home": "الرئيسية", "home": "هومبيج",
"portfolio": "الأعمال", "portfolio": "معرض أعمالي",
"products": "المنتجات", "products": "منتجاتي",
"about": ن أنا", "about": علومات عني",
"contact": "تواصل", "contact": "تواصل معي",
"root": "Root", "root": "Root",
"openMenu": "فتح القائمة", "openMenu": "فتح القائمة",
"closeMenu": "إغلاق القائمة", "closeMenu": "إغلاق القائمة",
@@ -22,20 +22,24 @@
"note": "شكراً لصبرك. النسخة الجديدة ستصل قريباً بهوية أنضج وحضور أدق." "note": "شكراً لصبرك. النسخة الجديدة ستصل قريباً بهوية أنضج وحضور أدق."
}, },
"homepage": { "homepage": {
"heroKicker": "مرحباً، أنا مو", "heroKicker": "مرحبااا، أنا محمد",
"heroTitle": "frontend\n developer\n& designer", "heroTitle": "مطور واجهات ويب\nومصمم جرافيك",
"heroText": "أصمم وأبني تجارب رقمية نظيفة مع اهتمام دقيق بالتفاصيل والحركة وسهولة الاستخدام.", "heroText": "بشتغل على حلول رقمية لمشاكل حقيقية\nبتطلع ممتازة وبتشتغل بكفاءة.",
"portfolioTitle": "أعمال مميزة", "servicesEyebrow": "كيف بقدر ساعدك",
"portfolioText": "منطقة تجريبية لعرض مشاريع مختارة.", "servicesTitle": "أقدر أساعدك تبني حضور رقمي مرتب وفعّال.",
"productsTitle": "منتجات مميزة", "servicesText": "إذا بدك تطلق براند، موقع، متجر، إضافة ووردبريس، أو بدك ترتب السيو والتراكنغ وتبني سستم خاص لشغلك، بقدر ساعدك من الفكرة للتنفيذ بشكل واضح وعملي.",
"productsText": "منطقة تجريبية لأهم العروض والمنتجات.", "servicesPrimaryCta": "احكيلي عن مشروعك",
"ctaTitle": "جاهز للخطوة التالية؟", "servicesSecondaryCta": "شوف شغلي",
"ctaText": "فيك تراسلني لنحدد النطاق الأنسب لمشروعك.", "serviceBrand": "بقدر ساعدك تبني براند واضح ومرتب.",
"serviceWebsite": "بقدر ساعدك تبني موقع إلكتروني يليق بشغلك.",
"serviceStore": "بقدر ساعدك تبني متجر يبيع بشكل أوضح وأسهل.",
"serviceWordPress": "بقدر ساعدك تبني إضافة WordPress خاصة فيك.",
"serviceSeo": "بقدر ساعدك تحسن ظهور موقعك بمحركات البحث.",
"serviceTracking": "بقدر ساعدك تضبط التراكنغ وتفهم حركة الزوار.",
"serviceSystem": "بقدر ساعدك تبني سستم خاص لشركتك أو لموظفينك.",
"toPortfolio": "عرض الأعمال", "toPortfolio": "عرض الأعمال",
"toProducts": "عرض المنتجات", "toProducts": "عرض المنتجات",
"toContact": "تواصل معي", "toContact": "تواصل معي"
"heroCardTitle": "إطلاق سريع",
"heroCardText": "هيكل ومحتوى ومكونات تساعد على نمو سريع وواضح."
}, },
"portfolioPage": { "portfolioPage": {
"title": "الأعمال", "title": "الأعمال",
+16 -12
View File
@@ -1,6 +1,6 @@
{ {
"navigation": { "navigation": {
"home": "Start", "home": "Startseite",
"portfolio": "Portfolio", "portfolio": "Portfolio",
"products": "Produkte", "products": "Produkte",
"about": "Über mich", "about": "Über mich",
@@ -23,19 +23,23 @@
}, },
"homepage": { "homepage": {
"heroKicker": "Hi, ich bin Moh", "heroKicker": "Hi, ich bin Moh",
"heroTitle": "frontend\n developer\n& designer", "heroTitle": "frontend\ndeveloper\n& designer",
"heroText": "Ich entwerfe und entwickle klare digitale Erlebnisse mit Fokus auf Details, Motion und starke Interfaces.", "heroText": "Ich entwickle digitale Loesungen fuer echte Probleme,\ndie gut aussehen und sauber funktionieren.",
"portfolioTitle": "Featured Projects", "servicesEyebrow": "Wobei ich helfen kann",
"portfolioText": "Platzhalter fuer ausgewaehlte Kundenprojekte.", "servicesTitle": "Ich kann dir helfen, den richtigen digitalen Aufbau fuer dein Business zu schaffen.",
"productsTitle": "Featured Products", "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.",
"productsText": "Platzhalter fuer die wichtigsten Produktangebote.", "servicesPrimaryCta": "Erzaehl mir von deinem Projekt",
"ctaTitle": "Bereit fuer den naechsten Schritt?", "servicesSecondaryCta": "Meine Arbeiten ansehen",
"ctaText": "Wir planen zusammen den passenden Scope fuer dein Projekt.", "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", "toPortfolio": "Portfolio ansehen",
"toProducts": "Produkte ansehen", "toProducts": "Produkte ansehen",
"toContact": "Kontakt aufnehmen", "toContact": "Kontakt aufnehmen"
"heroCardTitle": "Schneller Rollout",
"heroCardText": "Struktur, Content und Komponenten fuer schnelles Wachstum."
}, },
"portfolioPage": { "portfolioPage": {
"title": "Portfolio", "title": "Portfolio",
+17 -13
View File
@@ -1,9 +1,9 @@
{ {
"navigation": { "navigation": {
"home": "Home", "home": "Home",
"portfolio": "Portfolio", "portfolio": "My Portfolio",
"products": "Products", "products": "Products",
"about": "About", "about": "About Me",
"contact": "Contact", "contact": "Contact",
"root": "Root", "root": "Root",
"openMenu": "Open menu", "openMenu": "Open menu",
@@ -23,19 +23,23 @@
}, },
"homepage": { "homepage": {
"heroKicker": "Hi, I am Moh", "heroKicker": "Hi, I am Moh",
"heroTitle": "frontend\ndelevoper\n& designer", "heroTitle": "frontend\ndeveloper\n& designer",
"heroText": "I design and build clean digital experiences with a sharp eye for detail, motion, and usable interfaces.", "heroText": "I craft digital solutions for human problems\nthat look good and work well.",
"portfolioTitle": "Featured Projects", "servicesEyebrow": "How I Can Help",
"portfolioText": "Placeholder area for highlighted client projects.", "servicesTitle": "I can help you build the right digital setup for your business.",
"productsTitle": "Featured Products", "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.",
"productsText": "Placeholder area for top product offerings.", "servicesPrimaryCta": "Tell me about your project",
"ctaTitle": "Ready for your next step?", "servicesSecondaryCta": "See my work",
"ctaText": "We can shape the right project scope together.", "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", "toPortfolio": "View portfolio",
"toProducts": "View products", "toProducts": "View products",
"toContact": "Contact me", "toContact": "Contact me"
"heroCardTitle": "Fast rollout",
"heroCardText": "Structure, content, and components for rapid growth."
}, },
"portfolioPage": { "portfolioPage": {
"title": "Portfolio", "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