merge: add Persian localization
This commit is contained in:
@@ -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'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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user