merge: add Persian localization
Some checks failed
Test and deploy / Verify production build (push) Has been cancelled
Test and deploy / Deploy production (push) Has been cancelled

This commit is contained in:
2026-09-08 22:42:34 +03:30
44 changed files with 1882 additions and 353 deletions

View File

@@ -1,13 +1,50 @@
'use client';
import React from 'react';
import { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { BookOpenText, GraduationCap, MessageCircle, Moon, Sun } from 'lucide-react';
import Link from 'next/link';
import {
BookOpenText,
Check,
GraduationCap,
Languages,
MessageCircle,
Moon,
Sun,
} from 'lucide-react';
import { useTheme } from '@/context/ThemeContext';
import { messagesByLocale, type Locale } from '@/i18n/content';
import styles from './HeroSection.module.css';
export const HeroSection: React.FC = () => {
export const HeroSection = ({ locale }: { locale: Locale }) => {
const { theme, toggleTheme } = useTheme();
const copy = messagesByLocale[locale].hero;
const themeLabel = theme === 'light' ? copy.themeDarkLabel : copy.themeLightLabel;
const [isLanguageMenuOpen, setIsLanguageMenuOpen] = useState(false);
const languageMenuRef = useRef<HTMLDivElement>(null);
const languageButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!isLanguageMenuOpen) return;
const closeOnOutsideClick = (event: PointerEvent) => {
if (!languageMenuRef.current?.contains(event.target as Node)) {
setIsLanguageMenuOpen(false);
}
};
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
setIsLanguageMenuOpen(false);
languageButtonRef.current?.focus();
};
document.addEventListener('pointerdown', closeOnOutsideClick);
document.addEventListener('keydown', closeOnEscape);
return () => {
document.removeEventListener('pointerdown', closeOnOutsideClick);
document.removeEventListener('keydown', closeOnEscape);
};
}, [isLanguageMenuOpen]);
return (
<div className={styles.heroContentWrapper}>
@@ -17,8 +54,8 @@ export const HeroSection: React.FC = () => {
type="button"
onClick={toggleTheme}
className={styles.iconButton}
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
aria-label={themeLabel}
title={themeLabel}
>
{theme === 'light' ? (
<Moon size={28} strokeWidth={1.8} className={styles.themeIcon} />
@@ -26,6 +63,84 @@ export const HeroSection: React.FC = () => {
<Sun size={28} strokeWidth={1.8} className={styles.controlIcon} />
)}
</button>
<div className={styles.languageMenuWrapper} ref={languageMenuRef}>
<button
ref={languageButtonRef}
type="button"
className={`${styles.iconButton} ${styles.languageButton}`}
aria-label={copy.languageButtonLabel}
title={copy.languageButtonLabel}
aria-haspopup="menu"
aria-expanded={isLanguageMenuOpen}
aria-controls="language-menu"
onClick={() => setIsLanguageMenuOpen((isOpen) => !isOpen)}
data-language-switch
>
<Languages size={23} strokeWidth={1.9} aria-hidden="true" />
</button>
<div
id="language-menu"
className={styles.languageMenu}
role="menu"
aria-label={copy.languageMenuLabel}
data-open={isLanguageMenuOpen}
>
<Link
href="/"
hrefLang="en"
role="menuitem"
aria-current={locale === 'en' ? 'page' : undefined}
className={styles.languageOption}
>
<span className={styles.languageOptionLabel}>
<Image
src="/images/flags/english.svg"
alt=""
width={20}
height={20}
className={styles.languageFlag}
aria-hidden="true"
/>
<span lang="en" dir="ltr">{copy.englishLabel}</span>
</span>
{locale === 'en' && (
<Check
size={16}
className={styles.selectedIcon}
aria-hidden="true"
/>
)}
</Link>
<Link
href="/fa"
hrefLang="fa"
role="menuitem"
aria-current={locale === 'fa' ? 'page' : undefined}
className={styles.languageOption}
>
{locale === 'fa' && (
<Check
size={16}
className={styles.selectedIcon}
aria-hidden="true"
/>
)}
<span className={styles.languageOptionLabel}>
<Image
src="/images/flags/persian.svg"
alt=""
width={20}
height={20}
className={styles.languageFlag}
aria-hidden="true"
/>
<span lang="fa" dir="rtl">{copy.persianLabel}</span>
</span>
</Link>
</div>
</div>
</div>
{/* Hero Central Content */}
@@ -36,7 +151,7 @@ export const HeroSection: React.FC = () => {
<div className={styles.avatarContainer}>
<Image
src="/images/avatar.png"
alt="Amirhossein Khalili"
alt={copy.avatarAlt}
width={192}
height={192}
className={styles.avatarImg}
@@ -47,46 +162,44 @@ export const HeroSection: React.FC = () => {
</div>
{/* Display Typography */}
<h1 className={`font-display ${styles.heroTitle}`}>
<span className={styles.heroGreeting}>Hi, I&apos;m</span>
<span className={styles.heroName}>Amirhossein Khalili</span>
<h1 className={`font-display ${styles.heroTitle}`} aria-label={copy.titleLabel}>
<span className={styles.heroGreeting}>{copy.greeting}</span>
<span className={styles.heroName}>{copy.name}</span>
</h1>
<p className={styles.heroSubtitle}>
Product engineer building useful, resilient software.
</p>
<p className={styles.heroSubtitle}>{copy.subtitle}</p>
{/* Action Buttons */}
<div className={styles.actionButtons}>
<button
type="button"
className={`pill-btn ${styles.heroButton} ${styles.heroButtonUpcoming}`}
aria-label="Blog, coming soon"
title="Coming soon"
aria-label={copy.blogLabel}
title={copy.comingSoon}
disabled
>
<BookOpenText size={17} strokeWidth={2} aria-hidden="true" />
Blog
{copy.blog}
</button>
<button
type="button"
className={`pill-btn ${styles.heroButton} ${styles.heroButtonUpcoming}`}
aria-label="Courses, coming soon"
title="Coming soon"
aria-label={copy.coursesLabel}
title={copy.comingSoon}
disabled
>
<GraduationCap size={18} strokeWidth={2} aria-hidden="true" />
Courses
{copy.courses}
</button>
<a
href="https://www.linkedin.com/messaging/compose/?recipient=ACoAAEJsAwMBCv3L9dYcEXWam-l3pLOJ3UspX7w"
target="_blank"
rel="noreferrer"
className={`pill-btn ${styles.heroButton} ${styles.heroButtonPrimary}`}
aria-label="Get in touch on LinkedIn"
aria-label={copy.contactLabel}
>
<MessageCircle size={17} strokeWidth={2} aria-hidden="true" />
Get in touch
{copy.contact}
</a>
</div>