Initial deploy
هذا الالتزام موجود في:
21
components/document-language-sync.tsx
Normal file
21
components/document-language-sync.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import type { Language } from "@/data/portfolio";
|
||||
|
||||
type DocumentLanguageSyncProps = {
|
||||
language: Language;
|
||||
dir: "ltr" | "rtl";
|
||||
};
|
||||
|
||||
export function DocumentLanguageSync({
|
||||
language,
|
||||
dir,
|
||||
}: DocumentLanguageSyncProps) {
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = language;
|
||||
document.documentElement.dir = dir;
|
||||
}, [dir, language]);
|
||||
|
||||
return null;
|
||||
}
|
||||
48
components/footer.tsx
Normal file
48
components/footer.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import Link from "next/link";
|
||||
import { portfolioContent, sharedProfile, type Language } from "@/data/portfolio";
|
||||
|
||||
export function Footer({ language }: { language: Language }) {
|
||||
const dir = language === "ar" ? "rtl" : "ltr";
|
||||
const t = portfolioContent[language];
|
||||
const brandName = language === "ar" ? sharedProfile.brandNameAr : sharedProfile.brandNameEn;
|
||||
|
||||
return (
|
||||
<footer className="py-10">
|
||||
<div className="site-container">
|
||||
<div className="editorial-rule" />
|
||||
</div>
|
||||
<div
|
||||
className={`site-container flex flex-col gap-6 py-8 md:flex-row md:items-end ${
|
||||
dir === "rtl" ? "md:flex-row-reverse md:justify-between" : "md:justify-between"
|
||||
}`}
|
||||
>
|
||||
<div className={dir === "rtl" ? "text-right" : ""}>
|
||||
<p className="type-footer-brand display-face text-[var(--color-ink)]">{brandName}</p>
|
||||
<p className="type-chip mt-2 max-w-md text-[var(--color-muted)]">{t.footer}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`type-chip flex flex-col gap-2 text-[var(--color-muted)] ${
|
||||
dir === "rtl" ? "md:items-start text-right" : "md:items-end"
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href={`mailto:${sharedProfile.email}`}
|
||||
dir="ltr"
|
||||
className="data-ltr hover:text-[var(--color-blue-500)]"
|
||||
>
|
||||
{sharedProfile.email}
|
||||
</Link>
|
||||
<Link
|
||||
href={`tel:${sharedProfile.phone.replace(/\s+/g, "")}`}
|
||||
dir="ltr"
|
||||
className="data-ltr hover:text-[var(--color-blue-500)]"
|
||||
>
|
||||
{sharedProfile.phone}
|
||||
</Link>
|
||||
<p>{t.ui.selectedWorks}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
412
components/home-page.tsx
Normal file
412
components/home-page.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ProjectCard } from "@/components/project-card";
|
||||
import { SectionHeading } from "@/components/section-heading";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import {
|
||||
getBasePath,
|
||||
getDirection,
|
||||
portfolioContent,
|
||||
resumeFile,
|
||||
sharedProfile,
|
||||
type Language,
|
||||
} from "@/data/portfolio";
|
||||
|
||||
export function HomePage({ language }: { language: Language }) {
|
||||
const dir = getDirection(language);
|
||||
const t = portfolioContent[language];
|
||||
const founderName = language === "ar" ? sharedProfile.founderNameAr : sharedProfile.founderNameEn;
|
||||
const address = language === "ar" ? sharedProfile.addressAr : sharedProfile.address;
|
||||
const heroImageAlt =
|
||||
language === "ar"
|
||||
? "لوحة بورتفوليو لمجمع إعلامي ومبنى التلفزيون"
|
||||
: "Portfolio board for Media Complex and TV Building";
|
||||
const basePath = getBasePath(language);
|
||||
const hasResume = resumeFile.available;
|
||||
const contactCards = [
|
||||
{
|
||||
kind: "phone",
|
||||
label: t.ui.phone,
|
||||
value: sharedProfile.phone,
|
||||
href: `tel:${sharedProfile.phone.replace(/\s+/g, "")}`,
|
||||
},
|
||||
{
|
||||
kind: "whatsapp",
|
||||
label: t.ui.whatsapp,
|
||||
value: sharedProfile.phone,
|
||||
href: sharedProfile.whatsappHref,
|
||||
},
|
||||
{
|
||||
kind: "email",
|
||||
label: t.ui.email,
|
||||
value: sharedProfile.email,
|
||||
href: `mailto:${sharedProfile.email}`,
|
||||
wide: true,
|
||||
},
|
||||
{ kind: "location", label: t.ui.basedIn, value: address, href: sharedProfile.locationHref },
|
||||
{ kind: "facebook", label: t.ui.facebook, value: sharedProfile.facebook, href: sharedProfile.facebookHref },
|
||||
];
|
||||
const hasWideContactCard = contactCards.some((item) => item.wide);
|
||||
|
||||
return (
|
||||
<SiteShell language={language}>
|
||||
<div className="top-wash pointer-events-none absolute inset-x-0 top-0 h-[720px]" />
|
||||
|
||||
<main className="site-container flex flex-col gap-12 py-6 md:gap-14 md:py-8 lg:gap-16 lg:py-10">
|
||||
<section className="grid gap-5">
|
||||
<div className="grid gap-8 lg:grid-cols-[0.92fr_1.08fr] lg:items-stretch">
|
||||
<div className="section-shell hero-glow line-frame h-full">
|
||||
<div className="section-padding editorial-grid flex h-full flex-col justify-center">
|
||||
<p className="section-kicker">{t.ui.editorialPortfolio}</p>
|
||||
<h1
|
||||
className={`type-hero-title mt-6 max-w-4xl text-[var(--color-ink)] ${
|
||||
language === "en" ? "type-hero-title-latin" : ""
|
||||
}`}
|
||||
>
|
||||
{founderName}
|
||||
</h1>
|
||||
|
||||
<div className="mt-8 max-w-2xl space-y-4">
|
||||
<p className="type-body-responsive uppercase tracking-[0.18em] text-[var(--color-muted)]">
|
||||
{t.ui.architectureEngineer}
|
||||
</p>
|
||||
<p className="type-body-responsive text-[var(--color-muted)]">
|
||||
{t.hero.subtitle} . {t.hero.supportingLine}
|
||||
</p>
|
||||
<p className="type-body-responsive max-w-xl text-[var(--color-muted)]">
|
||||
{t.hero.intro}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={`hero-action-group mt-10 flex flex-wrap gap-4 ${dir === "rtl" ? "justify-start" : ""}`}>
|
||||
<Link href={`${basePath}#projects`} className="button-primary">
|
||||
{t.ui.viewProjects}
|
||||
</Link>
|
||||
{hasResume ? (
|
||||
<Link href={resumeFile.href} className="button-secondary" download>
|
||||
{t.ui.downloadCv}
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`${basePath}/resume`} className="button-secondary">
|
||||
{t.ui.onlineResume}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="image-frame h-full">
|
||||
<div className="relative aspect-[5/6] overflow-hidden rounded-[28px]">
|
||||
<Image
|
||||
src={sharedProfile.heroImage}
|
||||
alt={heroImageAlt}
|
||||
fill
|
||||
priority
|
||||
sizes="(max-width: 1024px) 100vw, 52vw"
|
||||
className="object-cover saturate-[1.05] brightness-[0.96]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<div className="soft-card">
|
||||
<p className="eyebrow-note">{t.ui.profile}</p>
|
||||
<p className="type-card-title-small display-face mt-4 text-[var(--color-ink)]">
|
||||
{t.hero.profileBlurb}
|
||||
</p>
|
||||
</div>
|
||||
<div className="gradient-card">
|
||||
<p className="eyebrow-note">{t.hero.featuredProjectTitle}</p>
|
||||
<p className="type-card-title display-face mt-4 text-[var(--color-ink)]">
|
||||
{t.featured.projects[0]?.location ?? address}
|
||||
</p>
|
||||
<p className="type-body mt-3 text-[var(--color-muted)]">{t.hero.featuredProjectText}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="about" className="grid gap-8 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<div className="section-shell">
|
||||
<div className="section-padding">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.navAbout}
|
||||
title={t.about.title}
|
||||
description={t.about.description}
|
||||
/>
|
||||
|
||||
<div className="type-body-editorial mt-8 max-w-2xl space-y-5 text-[var(--color-muted)]">
|
||||
{t.about.paragraphs.map((paragraph) => (
|
||||
<p key={paragraph}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="philosophy" className="grid gap-5 scroll-mt-28">
|
||||
<div className="gradient-card">
|
||||
<p className="eyebrow-note">{t.ui.designPhilosophy}</p>
|
||||
<p className="type-card-title display-face mt-4 text-[var(--color-ink)]">
|
||||
"{t.philosophy.quote}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="soft-card">
|
||||
<p className="eyebrow-note">{t.ui.materialsMood}</p>
|
||||
<p className="type-body-editorial mt-4 text-[var(--color-muted)]">{t.philosophy.body}</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
{t.philosophy.tags.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="type-chip surface-chip px-4 py-2 font-medium text-[var(--color-ink)]"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-8 lg:grid-cols-[0.88fr_1.12fr]">
|
||||
<div className="section-shell">
|
||||
<div className="section-padding">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.sectors}
|
||||
title={t.sectors.title}
|
||||
description={t.sectors.description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||
{t.sectors.categories.map((category, index) => (
|
||||
<article key={category} className="soft-card min-h-[190px]">
|
||||
<p className="eyebrow-note">{String(index + 1).padStart(2, "0")}</p>
|
||||
<h3 className="type-card-title mt-8 text-[var(--color-ink)]">{category}</h3>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="projects" className="space-y-8 scroll-mt-28 md:space-y-10">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.featuredProjects}
|
||||
title={t.featured.title}
|
||||
description={t.featured.description}
|
||||
/>
|
||||
|
||||
<div className="space-y-10">
|
||||
{t.featured.projects.map((project, index) => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
reverse={index % 2 === 1}
|
||||
priority={index < 2}
|
||||
labels={{ year: t.ui.year, area: t.ui.area, role: t.ui.role, fullSheet: t.ui.viewFullSheet }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="awards" className="section-shell scroll-mt-28">
|
||||
<div className="section-padding">
|
||||
<div className="grid gap-10 lg:grid-cols-[0.82fr_1.18fr]">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.awards}
|
||||
title={t.awards.title}
|
||||
description={t.awards.description}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{t.awards.items.map((award, index) => (
|
||||
<article
|
||||
key={`${award.year}-${award.title}`}
|
||||
className="surface-card rounded-[24px] p-5"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<p className="eyebrow-note">{String(index + 1).padStart(2, "0")}</p>
|
||||
<p className="type-label font-semibold text-[var(--color-blue-400)]">{award.year}</p>
|
||||
</div>
|
||||
<h3 className="type-card-title-small mt-4 text-[var(--color-ink)]">{award.title}</h3>
|
||||
<p className="type-body mt-3 text-[var(--color-muted)]">{award.description}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-8 lg:grid-cols-[1.02fr_0.98fr]">
|
||||
<div className="section-shell">
|
||||
<div className="section-padding">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.craft}
|
||||
title={t.craft.title}
|
||||
description={t.craft.description}
|
||||
/>
|
||||
|
||||
<div className="mt-10 grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="eyebrow-note">{t.ui.coreSkills}</p>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{t.craft.skills.map((skill) => (
|
||||
<span
|
||||
key={skill}
|
||||
className="type-chip surface-chip px-4 py-2 font-medium text-[var(--color-ink)]"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="eyebrow-note">{t.ui.software}</p>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{t.craft.software.map((tool) => (
|
||||
<span
|
||||
key={tool}
|
||||
className="type-chip surface-chip-alt px-4 py-2 font-medium text-[var(--color-ink)]"
|
||||
>
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 editorial-rule" />
|
||||
|
||||
<div className="mt-10 grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<p className="eyebrow-note">{t.ui.documentation}</p>
|
||||
<p className="type-body text-[var(--color-muted)]">{t.craft.documentationText}</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="eyebrow-note">{t.ui.visualization}</p>
|
||||
<p className="type-body text-[var(--color-muted)]">{t.craft.visualizationText}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5">
|
||||
{t.craft.additionalProjects.map((project) => (
|
||||
<article key={project.title} className="gradient-card">
|
||||
<p className="eyebrow-note">{project.year}</p>
|
||||
<h3 className="type-card-title mt-4 text-[var(--color-ink)]">{project.title}</h3>
|
||||
<p className="type-body mt-3 text-[var(--color-muted)]">{project.type}</p>
|
||||
<p className="type-label-wide mt-8 font-semibold text-[var(--color-ink)]">
|
||||
{project.location}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section-shell">
|
||||
<div className="section-padding grid gap-8 lg:grid-cols-[0.95fr_1.05fr] lg:items-center">
|
||||
<div>
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.onlineResume}
|
||||
title={t.resume.title}
|
||||
description={t.resume.description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 sm:grid-cols-2">
|
||||
<Link href={`${basePath}/resume`} className="soft-card block">
|
||||
<p className="eyebrow-note">{t.ui.onlineResume}</p>
|
||||
<p className="type-card-title display-face mt-4 text-[var(--color-ink)]">
|
||||
{t.ui.viewFullResume}
|
||||
</p>
|
||||
</Link>
|
||||
{hasResume ? (
|
||||
<Link href={resumeFile.href} className="gradient-card block" download>
|
||||
<p className="eyebrow-note">{t.ui.pdfDownload}</p>
|
||||
<p className="type-card-title display-face mt-4 text-[var(--color-ink)]">
|
||||
{t.ui.downloadCv}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="gradient-card">
|
||||
<p className="eyebrow-note">{t.ui.pdfDownload}</p>
|
||||
<p className="type-card-title display-face mt-4 text-[var(--color-ink)]">
|
||||
{t.ui.downloadCv}
|
||||
</p>
|
||||
<p className="type-body mt-3 text-[var(--color-muted)]">{t.ui.resumeUnavailable}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
|
||||
{t.metrics.map((metric) => (
|
||||
<article key={metric.label} className="soft-card text-center">
|
||||
<p className="type-metric display-face text-[var(--color-ink)]">{metric.value}</p>
|
||||
<p className="type-label-wide mt-4 text-[var(--color-muted)]">
|
||||
{metric.label}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section id="contact" className="section-shell hero-glow scroll-mt-28">
|
||||
<div className="section-padding grid gap-10 lg:grid-cols-[minmax(0,1fr)_minmax(460px,0.78fr)] lg:items-start">
|
||||
<div className="contact-copy">
|
||||
<p className="section-kicker">{t.ui.contact}</p>
|
||||
<h2 className="contact-title">
|
||||
{t.ui.letsCreate}
|
||||
</h2>
|
||||
<p className="contact-description">
|
||||
{t.ui.availableFor}
|
||||
</p>
|
||||
<div className="mt-8 max-w-xl">
|
||||
<div className="editorial-rule" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-full gap-4 md:grid-cols-2 lg:min-w-[460px]">
|
||||
{contactCards.map((item, index) => {
|
||||
const shouldSpan =
|
||||
item.wide || (!hasWideContactCard && contactCards.length % 2 === 1 && index === contactCards.length - 1);
|
||||
const isEmail = item.kind === "email";
|
||||
const isPhoneValue = item.kind === "phone" || item.kind === "whatsapp";
|
||||
const isLtrValue = isEmail || isPhoneValue;
|
||||
const content = (
|
||||
<>
|
||||
<p className="contact-label text-[var(--color-muted)]">{item.label}</p>
|
||||
<p
|
||||
dir={isLtrValue ? "ltr" : undefined}
|
||||
className={`contact-value mt-3 break-words font-semibold text-[var(--color-ink)] ${
|
||||
isEmail ? "contact-value-email" : ""
|
||||
} ${isPhoneValue ? "contact-value-ltr" : ""}`}
|
||||
>
|
||||
{item.value}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
const cardClass =
|
||||
`contact-card ${shouldSpan ? "md:col-span-2" : ""}`;
|
||||
|
||||
return item.href ? (
|
||||
<Link key={item.label} href={item.href} className={cardClass}>
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
<div key={item.label} className={cardClass}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
30
components/language-toggle.tsx
Normal file
30
components/language-toggle.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { portfolioContent, type Language } from "@/data/portfolio";
|
||||
|
||||
export function LanguageToggle({ language }: { language: Language }) {
|
||||
const pathname = usePathname() ?? `/${language}`;
|
||||
const nextLanguage = language === "en" ? "ar" : "en";
|
||||
const { languageLabel, languageToggleAriaLabel } = portfolioContent[language].ui;
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
|
||||
if (segments[0] === "en" || segments[0] === "ar") {
|
||||
segments[0] = nextLanguage;
|
||||
} else {
|
||||
segments.unshift(nextLanguage);
|
||||
}
|
||||
|
||||
const nextPath = `/${segments.join("/")}`;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={nextPath}
|
||||
className="control-pill"
|
||||
aria-label={languageToggleAriaLabel}
|
||||
>
|
||||
{languageLabel}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
105
components/mobile-nav.tsx
Normal file
105
components/mobile-nav.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type NavItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
type MobileNavProps = {
|
||||
items: NavItem[];
|
||||
dir: "rtl" | "ltr";
|
||||
openLabel: string;
|
||||
closeLabel: string;
|
||||
};
|
||||
|
||||
export function MobileNav({ items, dir, openLabel, closeLabel }: MobileNavProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
const media = window.matchMedia("(min-width: 768px)");
|
||||
const handleHashChange = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMediaChange = (event: MediaQueryListEvent) => {
|
||||
if (event.matches) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.body.style.overflow = "hidden";
|
||||
window.addEventListener("hashchange", handleHashChange);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
media.addEventListener("change", handleMediaChange);
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener("hashchange", handleHashChange);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
media.removeEventListener("change", handleMediaChange);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const label = open ? closeLabel : openLabel;
|
||||
|
||||
return (
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="theme-toggle mobile-menu-toggle"
|
||||
aria-expanded={open}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
{open ? (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
|
||||
<path d="M6 6 18 18" />
|
||||
<path d="M18 6 6 18" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
|
||||
<path d="M4 7h16" />
|
||||
<path d="M4 12h16" />
|
||||
<path d="M4 17h16" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<div className="mobile-menu-panel">
|
||||
<nav aria-label="Mobile primary">
|
||||
<ul className="mobile-menu-list">
|
||||
{items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`mobile-menu-link ${dir === "rtl" ? "text-right" : ""}`}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
components/nav-links.tsx
Normal file
83
components/nav-links.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type NavItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
type NavLinksProps = {
|
||||
items: NavItem[];
|
||||
alignEnd: boolean;
|
||||
};
|
||||
|
||||
function getHashId(href: string) {
|
||||
return href.split("#")[1] ?? "";
|
||||
}
|
||||
|
||||
export function NavLinks({ items, alignEnd }: NavLinksProps) {
|
||||
const [activeId, setActiveId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const sectionIds = items.map((item) => getHashId(item.href)).filter(Boolean);
|
||||
const sections = sectionIds
|
||||
.map((id) => document.getElementById(id))
|
||||
.filter((section): section is HTMLElement => Boolean(section));
|
||||
|
||||
if (sections.length === 0) {
|
||||
setActiveId("");
|
||||
return;
|
||||
}
|
||||
|
||||
function syncFromHash() {
|
||||
const nextId = window.location.hash.replace("#", "");
|
||||
if (sectionIds.includes(nextId)) {
|
||||
setActiveId(nextId);
|
||||
}
|
||||
}
|
||||
|
||||
syncFromHash();
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
||||
|
||||
if (visible?.target.id) {
|
||||
setActiveId(visible.target.id);
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-28% 0px -58% 0px", threshold: [0.12, 0.24, 0.36] }
|
||||
);
|
||||
|
||||
sections.forEach((section) => observer.observe(section));
|
||||
window.addEventListener("hashchange", syncFromHash);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("hashchange", syncFromHash);
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<nav aria-label="Primary" className="nav-scroll">
|
||||
<ul className={`nav-list ${alignEnd ? "md:justify-end" : "md:justify-start"}`}>
|
||||
{items.map((item) => {
|
||||
const itemId = getHashId(item.href);
|
||||
const isActive = activeId === itemId;
|
||||
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link href={item.href} className="nav-link-pill" data-active={isActive || undefined}>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
78
components/navbar.tsx
Normal file
78
components/navbar.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { LanguageToggle } from "@/components/language-toggle";
|
||||
import { MobileNav } from "@/components/mobile-nav";
|
||||
import { NavLinks } from "@/components/nav-links";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { getBasePath, getDirection, portfolioContent, sharedProfile, type Language } from "@/data/portfolio";
|
||||
|
||||
export function Navbar({ language }: { language: Language }) {
|
||||
const dir = getDirection(language);
|
||||
const t = portfolioContent[language].ui;
|
||||
const brandName = language === "ar" ? sharedProfile.brandNameAr : sharedProfile.brandNameEn;
|
||||
const basePath = getBasePath(language);
|
||||
|
||||
const navItems = [
|
||||
{ label: t.navAbout, href: `${basePath}#about` },
|
||||
{ label: t.navPhilosophy, href: `${basePath}#philosophy` },
|
||||
{ label: t.navProjects, href: `${basePath}#projects` },
|
||||
{ label: t.awards, href: `${basePath}#awards` },
|
||||
{ label: t.navContact, href: `${basePath}#contact` },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="navbar-shell sticky top-0 z-50 border-b backdrop-blur-xl">
|
||||
<div
|
||||
className={`site-container relative flex items-center justify-between gap-4 py-4 ${
|
||||
dir === "rtl" ? "md:flex-row-reverse md:justify-between" : "md:justify-between"
|
||||
}`}
|
||||
>
|
||||
<Link href={basePath} className="min-w-0 max-w-[68vw] md:max-w-fit">
|
||||
<div className={`flex items-center gap-4 ${dir === "rtl" ? "flex-row-reverse" : ""}`}>
|
||||
<div className="logo-shell overflow-hidden rounded-[20px] border p-2">
|
||||
<Image
|
||||
src={sharedProfile.logoImage}
|
||||
alt={language === "ar" ? "\u0634\u0639\u0627\u0631 \u063a\u0631\u064a\u0633 \u0628\u0637\u0631\u0633 \u0633\u0644\u0645\u0648\u0646" : "Grace Butrus Salmoun logo"}
|
||||
width={60}
|
||||
height={60}
|
||||
className="h-[60px] w-[60px] rounded-[14px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className={`min-w-0 ${dir === "rtl" ? "text-right" : ""}`}>
|
||||
<p className="type-brand display-face text-[var(--color-ink)]">
|
||||
{brandName}
|
||||
</p>
|
||||
<p className="type-label mt-1 hidden tracking-[0.18em] text-[var(--color-muted)] sm:block">
|
||||
{t.architectureEngineer}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div
|
||||
className={`hidden gap-3 md:flex md:flex-row md:items-center ${
|
||||
dir === "rtl" ? "md:flex-row-reverse" : ""
|
||||
}`}
|
||||
>
|
||||
<NavLinks items={navItems} alignEnd={dir !== "rtl"} />
|
||||
|
||||
<div className={`flex items-center gap-2 ${dir === "rtl" ? "flex-row-reverse" : ""}`}>
|
||||
<ThemeToggle language={language} />
|
||||
<LanguageToggle language={language} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center gap-2 md:hidden ${dir === "rtl" ? "flex-row-reverse" : ""}`}>
|
||||
<ThemeToggle language={language} />
|
||||
<LanguageToggle language={language} />
|
||||
<MobileNav
|
||||
items={navItems}
|
||||
dir={dir}
|
||||
openLabel={language === "ar" ? "فتح القائمة" : "Open menu"}
|
||||
closeLabel={language === "ar" ? "إغلاق القائمة" : "Close menu"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
103
components/project-card.tsx
Normal file
103
components/project-card.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import Image from "next/image";
|
||||
|
||||
type ProjectCardProps = {
|
||||
project: {
|
||||
id: string;
|
||||
title: string;
|
||||
location: string;
|
||||
year: string;
|
||||
area: string;
|
||||
role: string;
|
||||
description: string;
|
||||
highlights: readonly string[];
|
||||
image: string;
|
||||
sheetImage?: string;
|
||||
imageAlt: string;
|
||||
};
|
||||
reverse?: boolean;
|
||||
priority?: boolean;
|
||||
labels?: {
|
||||
year: string;
|
||||
area: string;
|
||||
role: string;
|
||||
fullSheet: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function ProjectCard({
|
||||
project,
|
||||
reverse = false,
|
||||
priority = false,
|
||||
labels,
|
||||
}: ProjectCardProps) {
|
||||
return (
|
||||
<article
|
||||
className={`project-card-shell grid gap-8 rounded-[34px] border p-4 md:p-5 lg:grid-cols-[0.78fr_1fr] lg:items-center lg:gap-14 ${
|
||||
reverse ? "lg:[&>*:first-child]:order-2 lg:[&>*:last-child]:order-1" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="image-frame mx-auto w-full max-w-[540px]">
|
||||
<div className="relative aspect-[3/5] overflow-hidden rounded-[26px]">
|
||||
<Image
|
||||
src={project.image}
|
||||
alt={project.imageAlt}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 540px"
|
||||
className="bg-white object-cover transition-transform duration-700 hover:scale-[1.03]"
|
||||
priority={priority}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="type-label-wide flex flex-wrap items-center gap-x-3 gap-y-2 text-[var(--color-muted)] sm:flex-nowrap">
|
||||
<span>{project.id}</span>
|
||||
<span className="project-rule-line h-px w-12 shrink-0" />
|
||||
<span>{project.location}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="type-project-title mt-5 text-[var(--color-ink)]">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="type-body-responsive mt-5 max-w-xl text-[var(--color-muted)]">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
<dl className="project-meta-list mt-8 grid gap-4 border-y py-6 sm:grid-cols-3">
|
||||
<div>
|
||||
<dt className="eyebrow-note">{labels?.year ?? "Year"}</dt>
|
||||
<dd className="type-body mt-2 font-semibold text-[var(--color-ink)]">{project.year}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="eyebrow-note">{labels?.area ?? "Area"}</dt>
|
||||
<dd className="type-body mt-2 font-semibold text-[var(--color-ink)]">{project.area}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="eyebrow-note">{labels?.role ?? "Role"}</dt>
|
||||
<dd className="type-body mt-2 font-semibold text-[var(--color-ink)]">{project.role}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<ul className="type-chip mt-8 grid gap-3 text-[var(--color-muted)] sm:grid-cols-2">
|
||||
{project.highlights.map((highlight) => (
|
||||
<li key={highlight} className="flex items-center gap-3">
|
||||
<span className="h-2 w-2 rounded-full bg-[var(--color-blue-500)]" />
|
||||
<span>{highlight}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{project.sheetImage ? (
|
||||
<a
|
||||
href={project.sheetImage}
|
||||
className="button-secondary mt-8 w-fit"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{labels?.fullSheet ?? "View full sheet"}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
166
components/resume-page-content.tsx
Normal file
166
components/resume-page-content.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import Link from "next/link";
|
||||
import { SectionHeading } from "@/components/section-heading";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { portfolioContent, resumeFile, sharedProfile, type Language } from "@/data/portfolio";
|
||||
|
||||
export function ResumePageContent({ language }: { language: Language }) {
|
||||
const t = portfolioContent[language];
|
||||
const brandName = language === "ar" ? sharedProfile.brandNameAr : sharedProfile.brandNameEn;
|
||||
const address = language === "ar" ? sharedProfile.addressAr : sharedProfile.address;
|
||||
const hasResume = resumeFile.available;
|
||||
|
||||
return (
|
||||
<SiteShell language={language}>
|
||||
<main className="site-container flex flex-col gap-10 py-8 md:py-10 lg:gap-12 lg:py-12">
|
||||
<section className="section-shell hero-glow">
|
||||
<div className="section-padding grid gap-8 lg:grid-cols-[1fr_auto] lg:items-end">
|
||||
<div>
|
||||
<p className="section-kicker">{t.ui.onlineResume}</p>
|
||||
<h1 className="type-page-title mt-4 max-w-4xl text-[var(--color-ink)]">
|
||||
{brandName}
|
||||
</h1>
|
||||
<p className="type-body-responsive mt-4 uppercase tracking-[0.18em] text-[var(--color-muted)]">
|
||||
{t.ui.architectureEngineer}
|
||||
</p>
|
||||
<p className="type-body-responsive mt-6 max-w-2xl text-[var(--color-muted)]">
|
||||
{t.resume.resumeIntro}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{hasResume ? (
|
||||
<Link href={resumeFile.href} className="button-primary" download>
|
||||
{t.ui.downloadCv}
|
||||
</Link>
|
||||
) : (
|
||||
<p className="notice-card max-w-xs">
|
||||
{t.ui.resumeUnavailable}
|
||||
</p>
|
||||
)}
|
||||
<Link href={`/${language}`} className="button-secondary">
|
||||
{t.ui.backToPortfolio}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-10 lg:grid-cols-[0.82fr_1.18fr]">
|
||||
<section className="section-shell">
|
||||
<div className="section-padding">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.profile}
|
||||
title={t.ui.summary}
|
||||
description={t.resume.onlineResumeDescription}
|
||||
/>
|
||||
|
||||
<div className="type-body-editorial mt-8 space-y-4 text-[var(--color-muted)]">
|
||||
<p>{t.about.description}</p>
|
||||
<p>{t.resume.profileParagraph}</p>
|
||||
</div>
|
||||
|
||||
<dl className="type-chip mt-8 grid gap-4 text-[var(--color-muted)]">
|
||||
<div className="surface-card rounded-[22px] p-5">
|
||||
<dt className="type-label-wide">{t.ui.phone}</dt>
|
||||
<dd dir="ltr" className="type-body data-ltr mt-2 font-semibold text-[var(--color-ink)]">
|
||||
{sharedProfile.phone}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="surface-card-alt rounded-[22px] p-5">
|
||||
<dt className="type-label-wide">{t.ui.email}</dt>
|
||||
<dd dir="ltr" className="type-body data-ltr mt-2 font-semibold text-[var(--color-ink)]">
|
||||
{sharedProfile.email}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="surface-card rounded-[22px] p-5">
|
||||
<dt className="type-label-wide">{t.ui.links}</dt>
|
||||
<dd className="type-body mt-2 space-y-2 font-semibold text-[var(--color-ink)]">
|
||||
<p>{address}</p>
|
||||
<p>Facebook: {sharedProfile.facebook}</p>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section-shell">
|
||||
<div className="section-padding">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.experience}
|
||||
title={t.ui.professionalTimeline}
|
||||
description={t.resume.experienceDescription}
|
||||
/>
|
||||
|
||||
<div className="mt-10 space-y-5">
|
||||
{t.resume.experience.map((item) => (
|
||||
<article
|
||||
key={`${item.period}-${item.role}`}
|
||||
className="resume-timeline-card"
|
||||
>
|
||||
<p className="type-label-wide text-[var(--color-muted)]">
|
||||
{item.period}
|
||||
</p>
|
||||
<h2 className="type-card-title-small mt-3 text-[var(--color-ink)]">{item.role}</h2>
|
||||
<p className="type-body mt-3 text-[var(--color-muted)]">{item.detail}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-10 lg:grid-cols-2">
|
||||
<section className="section-shell">
|
||||
<div className="section-padding">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.education}
|
||||
title={t.resume.education.degree}
|
||||
description={`${t.resume.education.school} - ${t.resume.education.location}`}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section-shell">
|
||||
<div className="section-padding">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.coreSkills}
|
||||
title={t.ui.designDelivery}
|
||||
description={t.resume.designDeliveryDescription}
|
||||
/>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
{t.craft.skills.map((skill) => (
|
||||
<span
|
||||
key={skill}
|
||||
className="type-chip surface-chip px-4 py-3 font-semibold text-[var(--color-ink)]"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section className="section-shell">
|
||||
<div className="section-padding">
|
||||
<SectionHeading
|
||||
eyebrow={t.ui.software}
|
||||
title={t.ui.productionTools}
|
||||
description={t.resume.productionToolsDescription}
|
||||
/>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
{t.craft.software.map((tool) => (
|
||||
<span
|
||||
key={tool}
|
||||
className="type-chip surface-chip-alt px-4 py-3 font-semibold text-[var(--color-ink)]"
|
||||
>
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
27
components/section-heading.tsx
Normal file
27
components/section-heading.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
type SectionHeadingProps = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
align?: "left" | "center";
|
||||
};
|
||||
|
||||
export function SectionHeading({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
align = "left",
|
||||
}: SectionHeadingProps) {
|
||||
const alignment = align === "center" ? "mx-auto max-w-3xl text-center" : "max-w-3xl";
|
||||
|
||||
return (
|
||||
<div className={alignment}>
|
||||
<p className="section-kicker">{eyebrow}</p>
|
||||
<h2 className="type-section-title mt-4 text-[var(--color-ink)]">
|
||||
{title}
|
||||
</h2>
|
||||
{description ? (
|
||||
<p className="type-body-responsive mt-5 text-[var(--color-muted)]">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
components/site-shell.tsx
Normal file
22
components/site-shell.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { DocumentLanguageSync } from "@/components/document-language-sync";
|
||||
import { getDirection, type Language } from "@/data/portfolio";
|
||||
|
||||
type SiteShellProps = {
|
||||
language: Language;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function SiteShell({ language, children }: SiteShellProps) {
|
||||
const dir = getDirection(language);
|
||||
|
||||
return (
|
||||
<div dir={dir} className={`relative overflow-hidden ${dir === "rtl" ? "text-right" : ""}`}>
|
||||
<DocumentLanguageSync language={language} dir={dir} />
|
||||
<Navbar language={language} />
|
||||
{children}
|
||||
<Footer language={language} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
78
components/theme-toggle.tsx
Normal file
78
components/theme-toggle.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Language } from "@/data/portfolio";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
const STORAGE_KEY = "grace-portfolio-theme";
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
}
|
||||
|
||||
export function ThemeToggle({ language }: { language: Language }) {
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = window.localStorage.getItem(STORAGE_KEY);
|
||||
const nextTheme = savedTheme === "dark" ? "dark" : "light";
|
||||
applyTheme(nextTheme);
|
||||
setTheme(nextTheme);
|
||||
}, []);
|
||||
|
||||
const nextTheme: Theme = theme === "dark" ? "light" : "dark";
|
||||
const label =
|
||||
language === "ar"
|
||||
? theme === "dark"
|
||||
? "\u062a\u0641\u0639\u064a\u0644 \u0627\u0644\u0648\u0636\u0639 \u0627\u0644\u0641\u0627\u062a\u062d"
|
||||
: "\u062a\u0641\u0639\u064a\u0644 \u0627\u0644\u0648\u0636\u0639 \u0627\u0644\u062f\u0627\u0643\u0646"
|
||||
: theme === "dark"
|
||||
? "Switch to light mode"
|
||||
: "Switch to dark mode";
|
||||
|
||||
function handleToggle() {
|
||||
applyTheme(nextTheme);
|
||||
window.localStorage.setItem(STORAGE_KEY, nextTheme);
|
||||
setTheme(nextTheme);
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className="theme-toggle" onClick={handleToggle} aria-label={label} title={label}>
|
||||
<span className="sr-only">{label}</span>
|
||||
<svg
|
||||
className="sun-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
<svg
|
||||
className="moon-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M20.8 14.7A8.5 8.5 0 0 1 9.3 3.2 8.5 8.5 0 1 0 20.8 14.7Z" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم