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 {
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,116 +82,64 @@ 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"
>
<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")}
<AppCard
level="inverse"
className="overflow-hidden border border-white/6 bg-[linear-gradient(145deg,hsl(var(--foreground)),hsl(221_42%_10%))] text-primary-foreground"
>
<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>
<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-5 max-w-[34rem] text-sm leading-7 text-primary-foreground/76 sm:text-base">
{t("servicesText")}
</p>
<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("servicesPrimaryCta")}
<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">
<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"
</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"
>
<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>
<Link href={getLocalizedPath(localeKey, "/portfolio")}>
{t("servicesSecondaryCta")}
</Link>
</Button>
</div>
</div>
<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 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>
<Button asChild variant="secondary">
<Link href={getLocalizedPath(localeKey, "/contact")}>
{t("toContact")}
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</AppCard>
</CardContent>
</AppCard>
</MotionFade>
</Container>
</>
+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>;
}