fix the homepage pages
Build & Deploy / Build & Push Docker Image (push) Failing after 49s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Has been cancelled
Test / Admin Unit Tests (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled
Build & Deploy / Build & Push Docker Image (push) Failing after 49s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Has been cancelled
Test / Admin Unit Tests (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 836 KiB |
@@ -60,12 +60,13 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
|
|||||||
dir={getDirection(locale)}
|
dir={getDirection(locale)}
|
||||||
data-theme-preference={preference}
|
data-theme-preference={preference}
|
||||||
data-theme={resolvedTheme}
|
data-theme={resolvedTheme}
|
||||||
|
data-scroll-behavior="smooth"
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
<script
|
<script
|
||||||
id="theme-bootstrap"
|
id="theme-bootstrap"
|
||||||
nonce={nonce}
|
nonce={nonce || undefined}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
dangerouslySetInnerHTML={{ __html: themeBootstrapScript }}
|
dangerouslySetInnerHTML={{ __html: themeBootstrapScript }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" rx="8" fill="#0f172a" />
|
||||||
|
<text
|
||||||
|
x="16"
|
||||||
|
y="21"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-family="Inter, Arial, sans-serif"
|
||||||
|
font-size="18"
|
||||||
|
font-weight="700"
|
||||||
|
fill="#ffffff"
|
||||||
|
>
|
||||||
|
R
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 302 B |
@@ -1,75 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { localizedPath, locales, type Locale } from '@/lib/localization/config';
|
import { localizedPath, type Locale } from '@/lib/localization/config';
|
||||||
import { persistLocale } from '@/lib/localization/client';
|
|
||||||
import { persistTheme, themePreferenceEvent } from '@/lib/theme/client';
|
|
||||||
import { type ThemePreference } from '@/lib/theme/config';
|
import { type ThemePreference } from '@/lib/theme/config';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { LocalePill } from './LocalePill';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { ThemePill } from './ThemePill';
|
||||||
import styles from './AuthHeader.module.css';
|
import styles from './AuthHeader.module.css';
|
||||||
|
|
||||||
const langLabels: Record<Locale, string> = { en: 'EN', fr: 'FR', ar: 'AR' };
|
|
||||||
const langFlags: Record<Locale, string> = { en: '🇺🇸', fr: '🇫🇷', ar: '🇲🇦' };
|
|
||||||
|
|
||||||
const themeDict: Record<Locale, Record<string, string>> = {
|
|
||||||
en: { theme: 'Theme', light: 'Light', dark: 'Dark' },
|
|
||||||
fr: { theme: 'Mode', light: 'Clair', dark: 'Sombre' },
|
|
||||||
ar: { theme: 'الوضع', light: 'فاتح', dark: 'داكن' },
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
themePreference: ThemePreference;
|
themePreference: ThemePreference;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AuthHeader({ locale, themePreference: initialPref }: Props) {
|
export function AuthHeader({ locale, themePreference }: Props) {
|
||||||
const pathname = usePathname();
|
|
||||||
const router = useRouter();
|
|
||||||
const [pref, setPref] = useState<ThemePreference>(initialPref);
|
|
||||||
|
|
||||||
const t = themeDict[locale] ?? themeDict.en;
|
|
||||||
const themeLabel = pref === 'dark' ? t.dark : t.light;
|
|
||||||
|
|
||||||
const localeMenuRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const [localeOpen, setLocaleOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPref(initialPref);
|
|
||||||
}, [initialPref]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = () => {
|
|
||||||
const stored = localStorage.getItem('hpc.theme.preference');
|
|
||||||
if (stored === 'light' || stored === 'dark' || stored === 'system') setPref(stored);
|
|
||||||
};
|
|
||||||
window.addEventListener(themePreferenceEvent, handler);
|
|
||||||
return () => window.removeEventListener(themePreferenceEvent, handler);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleClick(e: MouseEvent) {
|
|
||||||
if (!localeMenuRef.current?.contains(e.target as Node)) setLocaleOpen(false);
|
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', handleClick);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClick);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const cycleTheme = () => {
|
|
||||||
const next = pref === 'dark' ? 'light' : 'dark';
|
|
||||||
persistTheme(next);
|
|
||||||
setPref(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
const switchLocale = (next: Locale) => {
|
|
||||||
setLocaleOpen(false);
|
|
||||||
persistLocale(next);
|
|
||||||
const segments = pathname.split('/').filter(Boolean);
|
|
||||||
if (segments.length >= 2) segments[1] = next;
|
|
||||||
router.push('/' + segments.join('/'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const otherLocales = locales.filter((l) => l !== locale);
|
|
||||||
const homePath = localizedPath('home', locale);
|
const homePath = localizedPath('home', locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,54 +35,11 @@ export function AuthHeader({ locale, themePreference: initialPref }: Props) {
|
|||||||
|
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
<div className={styles.pill}>
|
<div className={styles.pill}>
|
||||||
<div ref={localeMenuRef} className={styles.localeMenuWrap}>
|
<LocalePill locale={locale} />
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setLocaleOpen((v) => !v)}
|
|
||||||
className={styles.localeBtn}
|
|
||||||
aria-expanded={localeOpen}
|
|
||||||
aria-haspopup="menu"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">{langFlags[locale]}</span>
|
|
||||||
<span>{langLabels[locale]}</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={`${styles.chevron} ${localeOpen ? styles.chevronOpen : ''}`}
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{localeOpen && (
|
|
||||||
<div className={styles.localeMenu}>
|
|
||||||
{otherLocales.map((l) => (
|
|
||||||
<button
|
|
||||||
key={l}
|
|
||||||
type="button"
|
|
||||||
onClick={() => switchLocale(l)}
|
|
||||||
className={styles.localeOption}
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">{langFlags[l]}</span>
|
|
||||||
<span>{langLabels[l]}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className={styles.divider} aria-hidden="true" />
|
<span className={styles.divider} aria-hidden="true" />
|
||||||
|
|
||||||
<button
|
<ThemePill initialPreference={themePreference} />
|
||||||
type="button"
|
|
||||||
onClick={cycleTheme}
|
|
||||||
className={styles.themeBtn}
|
|
||||||
aria-label={themeLabel}
|
|
||||||
>
|
|
||||||
<span className={styles.themeLabel}>{themeLabel}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
.wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 4.75rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #44403c;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: #f5f5f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .button {
|
||||||
|
color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .button:hover {
|
||||||
|
background: rgb(30 58 138 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.button {
|
||||||
|
min-width: 5.25rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevronOpen {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
z-index: 30;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
width: 12rem;
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid #e7e5e4;
|
||||||
|
background: rgb(255 255 255 / 0.95);
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.menu {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .menu {
|
||||||
|
border-color: #1e3a8a;
|
||||||
|
background: rgb(23 37 84 / 0.95);
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #44403c;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option:hover {
|
||||||
|
background: #f5f5f4;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .option {
|
||||||
|
color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .option:hover {
|
||||||
|
background: rgb(30 58 138 / 0.4);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { persistLocale } from '@/lib/localization/client';
|
||||||
|
import {
|
||||||
|
localeSwitchUrl,
|
||||||
|
locales,
|
||||||
|
routeIdFromPathname,
|
||||||
|
type Locale,
|
||||||
|
} from '@/lib/localization/config';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import styles from './LocalePill.module.css';
|
||||||
|
|
||||||
|
const langLabels: Record<Locale, string> = { en: 'EN', fr: 'FR', ar: 'AR' };
|
||||||
|
const langFlags: Record<Locale, string> = { en: '🇺🇸', fr: '🇫🇷', ar: '🇲🇦' };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
locale: Locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalePill({ locale }: Props) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClick(e: MouseEvent) {
|
||||||
|
if (!menuRef.current?.contains(e.target as Node)) setOpen(false);
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClick);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const switchLocale = (next: Locale) => {
|
||||||
|
setOpen(false);
|
||||||
|
persistLocale(next);
|
||||||
|
const routeId = routeIdFromPathname(pathname) ?? 'home';
|
||||||
|
router.push(
|
||||||
|
localeSwitchUrl(new URL(window.location.href), next, routeId).pathname +
|
||||||
|
new URL(window.location.href).search +
|
||||||
|
new URL(window.location.href).hash,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherLocales = locales.filter((l) => l !== locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menuRef} className={styles.wrap}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
className={styles.button}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">{langFlags[locale]}</span>
|
||||||
|
<span>{langLabels[locale]}</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className={styles.menu}>
|
||||||
|
{otherLocales.map((l) => (
|
||||||
|
<button
|
||||||
|
key={l}
|
||||||
|
type="button"
|
||||||
|
onClick={() => switchLocale(l)}
|
||||||
|
className={styles.option}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">{langFlags[l]}</span>
|
||||||
|
<span>{langLabels[l]}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,10 +5,10 @@ import { ActionLink } from '@/components/actions/ActionLink';
|
|||||||
import { accountCreateUrl } from '@/lib/account-urls';
|
import { accountCreateUrl } from '@/lib/account-urls';
|
||||||
import { BrandLink } from './BrandLink';
|
import { BrandLink } from './BrandLink';
|
||||||
import { HeaderNavigation } from './HeaderNavigation';
|
import { HeaderNavigation } from './HeaderNavigation';
|
||||||
import { LocaleSelector } from './LocaleSelector';
|
import { LocalePill } from './LocalePill';
|
||||||
import { MobileNavigation } from './MobileNavigation';
|
import { MobileNavigation } from './MobileNavigation';
|
||||||
import styles from './SiteHeader.module.css';
|
import styles from './SiteHeader.module.css';
|
||||||
import { ThemeSelector } from './ThemeSelector';
|
import { ThemePill } from './ThemePill';
|
||||||
|
|
||||||
function signInUrl(locale: Locale): string {
|
function signInUrl(locale: Locale): string {
|
||||||
return localizedPath('sign-in', locale);
|
return localizedPath('sign-in', locale);
|
||||||
@@ -39,19 +39,8 @@ export function SiteHeader({ locale, messages, themePreference }: SiteHeaderProp
|
|||||||
role="group"
|
role="group"
|
||||||
aria-label={messages.header.controlsLabel}
|
aria-label={messages.header.controlsLabel}
|
||||||
>
|
>
|
||||||
<LocaleSelector
|
<LocalePill locale={locale} />
|
||||||
currentLocale={locale}
|
<ThemePill initialPreference={themePreference} />
|
||||||
label={messages.controls.language}
|
|
||||||
localeNames={messages.controls.localeNames}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
<ThemeSelector
|
|
||||||
initialPreference={themePreference}
|
|
||||||
label={messages.controls.theme}
|
|
||||||
statusTemplate={messages.controls.themeStatus}
|
|
||||||
optionLabels={messages.controls.themes}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
<ActionLink href={signInUrl(locale)} variant="button-primary">
|
<ActionLink href={signInUrl(locale)} variant="button-primary">
|
||||||
{messages.header.login}
|
{messages.header.login}
|
||||||
</ActionLink>
|
</ActionLink>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
.wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: #f5f5f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .button:hover {
|
||||||
|
background: rgb(30 58 138 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.button {
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #1e3a8a;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .label {
|
||||||
|
background: #fb923c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.label {
|
||||||
|
padding: 0.375rem 0.875rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
color: #78716c;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .chevron {
|
||||||
|
color: #a8a29e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevronOpen {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
z-index: 30;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
width: 11rem;
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid #e7e5e4;
|
||||||
|
background: rgb(255 255 255 / 0.95);
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .menu {
|
||||||
|
border-color: #1e3a8a;
|
||||||
|
background: rgb(23 37 84 / 0.95);
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #44403c;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option:hover {
|
||||||
|
background: #f5f5f4;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .option {
|
||||||
|
color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .option:hover {
|
||||||
|
background: rgb(30 58 138 / 0.4);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { persistTheme, themePreferenceEvent } from '@/lib/theme/client';
|
||||||
|
import { type ThemePreference } from '@/lib/theme/config';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import styles from './ThemePill.module.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initialPreference: ThemePreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels: Record<ThemePreference, string> = {
|
||||||
|
light: 'Light',
|
||||||
|
dark: 'Dark',
|
||||||
|
system: 'System',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ThemePill({ initialPreference }: Props) {
|
||||||
|
const [pref, setPref] = useState<ThemePreference>(initialPreference);
|
||||||
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPref(initialPreference);
|
||||||
|
}, [initialPreference]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
const stored = localStorage.getItem('hpc.theme.preference');
|
||||||
|
if (stored === 'light' || stored === 'dark' || stored === 'system') setPref(stored);
|
||||||
|
};
|
||||||
|
window.addEventListener(themePreferenceEvent, handler);
|
||||||
|
return () => window.removeEventListener(themePreferenceEvent, handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClick(e: MouseEvent) {
|
||||||
|
if (!menuRef.current?.contains(e.target as Node)) setOpen(false);
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClick);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClick);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectTheme = (next: ThemePreference) => {
|
||||||
|
setOpen(false);
|
||||||
|
persistTheme(next);
|
||||||
|
setPref(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: ThemePreference[] = ['light', 'dark', 'system'];
|
||||||
|
const otherOptions = options.filter((o) => o !== pref);
|
||||||
|
|
||||||
|
const displayLabel = pref === 'dark' ? 'Dark' : 'Light';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menuRef} className={styles.wrap}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
className={styles.button}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label={displayLabel}
|
||||||
|
>
|
||||||
|
<span className={styles.label}>{displayLabel}</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className={styles.menu}>
|
||||||
|
{otherOptions.map((o) => (
|
||||||
|
<button
|
||||||
|
key={o}
|
||||||
|
type="button"
|
||||||
|
onClick={() => selectTheme(o)}
|
||||||
|
className={styles.option}
|
||||||
|
>
|
||||||
|
<span>{labels[o]}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
.brandImage {
|
.brandImage {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
background: var(--surface-page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
.heroGrid {
|
.heroGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.02fr) minmax(22rem, 0.98fr);
|
grid-template-columns: minmax(0, 1fr) minmax(26rem, 1.15fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: clamp(var(--space-10), 7vw, var(--space-20));
|
gap: clamp(var(--space-10), 7vw, var(--space-20));
|
||||||
}
|
}
|
||||||
@@ -53,11 +53,10 @@
|
|||||||
.previewStage {
|
.previewStage {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-inline-size: 0;
|
min-inline-size: 0;
|
||||||
padding: clamp(var(--space-3), 2.5vw, var(--space-6));
|
padding: clamp(var(--space-4), 3vw, var(--space-8));
|
||||||
}
|
}
|
||||||
|
|
||||||
.previewStage::before,
|
.previewStage::before {
|
||||||
.previewStage::after {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
@@ -70,17 +69,9 @@
|
|||||||
transform: rotate(-2deg);
|
transform: rotate(-2deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.previewStage::after {
|
|
||||||
inset-block-start: 8%;
|
|
||||||
inset-inline: 8%;
|
|
||||||
block-size: 88%;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
transform: rotate(2deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.heroGrid {
|
.heroGrid {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(18rem, 0.9fr);
|
grid-template-columns: minmax(0, 1fr) minmax(21rem, 1.05fr);
|
||||||
gap: var(--space-10);
|
gap: var(--space-10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,8 +94,7 @@
|
|||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.previewStage::before,
|
.previewStage::before {
|
||||||
.previewStage::after {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,15 +106,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
@media (forced-colors: active) {
|
||||||
.previewStage::before,
|
.previewStage::before {
|
||||||
.previewStage::after {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.previewStage::before,
|
.previewStage::before {
|
||||||
.previewStage::after {
|
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export function buildHomepageContent(
|
|||||||
image: localizedImage(
|
image: localizedImage(
|
||||||
'rentaldrivego',
|
'rentaldrivego',
|
||||||
locale,
|
locale,
|
||||||
'jpeg',
|
'png',
|
||||||
{ width: 1220, height: 1167 },
|
{ width: 1220, height: 1167 },
|
||||||
'RentalDriveGo',
|
'RentalDriveGo',
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user