merge design protal and backoffice

This commit is contained in:
mengstabketemaw
2026-06-17 13:50:20 +03:00
parent 36234a2948
commit 85e070db33
25 changed files with 2618 additions and 847 deletions

View File

@@ -0,0 +1,124 @@
import { useState } from 'react';
import { ActionIcon, Button, Popover, TextInput } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { DayPicker as EthiopicDayPicker } from '@daypicker/ethiopic';
import { DayPicker as GregorianDayPicker } from '@daypicker/react';
import { IconCalendarEvent } from '@tabler/icons-react';
import { EthDateTime } from 'ethiopian-calendar-date-converter';
import '@daypicker/react/dist/style.css';
const EC_MONTHS_AM = [
'መስከረም', 'ጥቅምት', 'ኅዳር', 'ታህሳስ', 'ጥር', 'የካቲት',
'መጋቢት', 'ሚያዝያ', 'ግንቦት', 'ሰኔ', 'ሐምሌ', 'ነሐሴ', 'ጳጉሜ',
];
function toAmharicDisplay(date: Date): string {
try {
const eth = EthDateTime.fromEuropeanDate(date);
return `${EC_MONTHS_AM[eth.month - 1]} ${eth.date}/${eth.year}`;
} catch {
return date.toLocaleDateString('en-US');
}
}
export function toEthiopicDateLabel(date: Date): string {
try {
const eth = EthDateTime.fromEuropeanDate(date);
return `EC ${eth.year}-${String(eth.month).padStart(2, '0')}-${String(eth.date).padStart(2, '0')}`;
} catch {
return date.toLocaleDateString('en-US');
}
}
export interface AmharicDatePickerProps {
label?: string;
value?: Date | null;
onChange?: (date: Date | null) => void;
required?: boolean;
placeholder?: string;
}
export function AmharicDatePicker({
label,
value,
onChange,
required,
placeholder,
}: AmharicDatePickerProps) {
const [calendarType, setCalendarType] = useState<'EN' | 'AMH'>('AMH');
const [opened, { close, toggle }] = useDisclosure(false);
const displayValue = value
? calendarType === 'EN'
? value.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
: toAmharicDisplay(value)
: '';
return (
<Popover
opened={opened}
onChange={close}
position="bottom"
width="auto"
trapFocus
withArrow
>
<Popover.Target>
<TextInput
label={label}
required={required}
value={displayValue}
readOnly
placeholder={placeholder}
onClick={toggle}
leftSection={
<Button
variant="light"
size="compact-xs"
onClick={(e) => {
e.stopPropagation();
setCalendarType((prev) => (prev === 'EN' ? 'AMH' : 'EN'));
}}
aria-label="Switch calendar type"
>
{calendarType}
</Button>
}
leftSectionWidth="calc(4.375rem * var(--mantine-scale))"
rightSection={
<ActionIcon size="md" variant="transparent" onClick={toggle}>
<IconCalendarEvent size={20} />
</ActionIcon>
}
/>
</Popover.Target>
<Popover.Dropdown p="md">
{calendarType === 'AMH' ? (
<EthiopicDayPicker
mode="single"
selected={value ?? undefined}
numerals="latn"
onSelect={(date: Date | undefined) => {
onChange?.(date ?? null);
close();
}}
/>
) : (
<GregorianDayPicker
mode="single"
selected={value ?? undefined}
onSelect={(date: Date | undefined) => {
onChange?.(date ?? null);
close();
}}
/>
)}
</Popover.Dropdown>
</Popover>
);
}

View File

@@ -1,45 +0,0 @@
import { UnstyledButton, useMantineColorScheme, useComputedColorScheme, rem } from '@mantine/core';
import { IconSun, IconMoon } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
export function ColorSchemeToggle() {
const { t } = useTranslation();
const { setColorScheme } = useMantineColorScheme();
const computed = useComputedColorScheme('light', { getInitialValueInEffect: true });
const isDark = computed === 'dark';
return (
<UnstyledButton
aria-label={t('common.toggleTheme')}
onClick={() => setColorScheme(isDark ? 'light' : 'dark')}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: rem(38),
height: rem(38),
borderRadius: rem(12),
background: 'var(--mantine-color-body)',
color: 'var(--mantine-color-gray-6)',
boxShadow: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
transition: 'all 150ms ease',
cursor: 'pointer',
border: 'none',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--mantine-color-primary-light)';
e.currentTarget.style.color = 'var(--mantine-color-primary-6)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-color-primary-2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--mantine-color-body)';
e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)';
}}
>
{isDark ? <IconSun size={19} /> : <IconMoon size={19} />}
</UnstyledButton>
);
}

View File

@@ -1,83 +0,0 @@
import { Menu, UnstyledButton, Text, rem } from '@mantine/core';
import { IconWorld, IconCheck, IconChevronDown } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { SUPPORTED_LANGUAGES, type AppLanguage } from '../i18n/config';
interface LanguageSwitcherProps {
/** `icon` renders a compact globe button; `button` shows the language label. */
variant?: 'icon' | 'button';
}
export function LanguageSwitcher({ variant = 'icon' }: LanguageSwitcherProps) {
const { t, i18n } = useTranslation();
const current = i18n.language as AppLanguage;
const change = (lng: AppLanguage) => {
if (lng !== current) i18n.changeLanguage(lng);
};
return (
<Menu shadow="md" width={160} position="bottom-end" withinPortal>
<Menu.Target>
<UnstyledButton
aria-label={t('language.label')}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: rem(4),
width: variant === 'icon' ? rem(38) : 'auto',
height: rem(38),
padding: variant === 'icon' ? 0 : '0 12px',
borderRadius: rem(12),
background: 'var(--mantine-color-body)',
color: 'var(--mantine-color-gray-6)',
boxShadow: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
transition: 'all 150ms ease',
cursor: 'pointer',
border: 'none',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--mantine-color-primary-light)';
e.currentTarget.style.color = 'var(--mantine-color-primary-6)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-color-primary-2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--mantine-color-body)';
e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
e.currentTarget.style.boxShadow =
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)';
}}
>
<IconWorld size={19} />
{variant !== 'icon' && (
<>
<Text size="sm" fw={500}>
{t(`language.${current}`)}
</Text>
<IconChevronDown size={14} />
</>
)}
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown
style={{ borderRadius: rem(12), padding: rem(6) }}
>
<Menu.Label>{t('language.label')}</Menu.Label>
{SUPPORTED_LANGUAGES.map((lng) => (
<Menu.Item
key={lng}
onClick={() => change(lng)}
rightSection={
current === lng ? <IconCheck size={16} /> : undefined
}
style={{ borderRadius: rem(8) }}
>
{t(`language.${lng}`)}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
);
}

View File

@@ -1,25 +0,0 @@
import { Group, Stack, Text, Title } from '@mantine/core';
import type { ReactNode } from 'react';
interface PageHeaderProps {
title: string;
subtitle?: string;
/** Right-aligned actions (e.g. a primary button). */
action?: ReactNode;
}
export function PageHeader({ title, subtitle, action }: PageHeaderProps) {
return (
<Group justify="space-between" align="flex-end" wrap="wrap" gap="sm">
<Stack gap={2}>
<Title order={2}>{title}</Title>
{subtitle && (
<Text c="dimmed" size="sm">
{subtitle}
</Text>
)}
</Stack>
{action}
</Group>
);
}

View File

@@ -1,30 +0,0 @@
import { Box, useMantineTheme } from '@mantine/core';
export function BrandAvatar({
initials = 'AB',
size = 38,
}: {
initials?: string;
size?: number;
}) {
const theme = useMantineTheme();
return (
<Box
w={size}
h={size}
style={{
borderRadius: '50%',
background: theme.other.heroGradient as string,
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: size * 0.36,
fontWeight: 700,
flexShrink: 0,
}}
>
{initials}
</Box>
);
}