Adding license generation functionality

This commit is contained in:
Muluhabt
2026-07-29 16:53:50 +03:00
parent 26cc1f4fd2
commit 5161e278ce
102 changed files with 5995 additions and 21326 deletions

View File

@@ -2,6 +2,7 @@ export * from './lib/input/BilingualInput';
export * from './lib/feedback/ConfirmModal';
export * from './lib/feedback/ApiErrorAlert';
export * from './lib/feedback/notify';
export * from './lib/feedback/FeatureUnavailable';
export * from './lib/layout/AppHeader';
export * from './lib/layout/AppSidebar';
export * from './lib/layout/BrandAvatar';

View File

@@ -0,0 +1,45 @@
import { Card, Center, Stack, Text, ThemeIcon, Title } from '@mantine/core';
import { IconTool } from '@tabler/icons-react';
import type { ReactNode } from 'react';
interface FeatureUnavailableProps {
/** What the screen will eventually do, e.g. "Seaman Book applications". */
title: string;
/** Optional extra context — what is missing, or what to use instead. */
description?: string;
action?: ReactNode;
}
/**
* Placeholder for a screen whose backend does not exist yet.
*
* These pages previously rendered hardcoded sample rows, which read as real
* records: reviewers saw queues of applications that had never been filed and
* dashboards counting things nobody had done. Showing nothing is the honest
* option — an empty screen cannot be mistaken for data.
*/
export function FeatureUnavailable({
title,
description,
action,
}: FeatureUnavailableProps) {
return (
<Center mih={320}>
<Card withBorder radius="md" padding="xl" maw={520} w="100%">
<Stack align="center" gap="sm">
<ThemeIcon size={48} radius="xl" variant="light" color="gray">
<IconTool size={24} stroke={1.5} />
</ThemeIcon>
<Title order={4} ta="center">
{title}
</Title>
<Text size="sm" c="dimmed" ta="center">
{description ??
'This feature is not connected to the backend yet, so there is nothing to show.'}
</Text>
{action}
</Stack>
</Card>
</Center>
);
}

View File

@@ -1,5 +1,6 @@
import {
AppShell,
Badge,
NavLink,
ScrollArea,
Stack,
@@ -20,11 +21,30 @@ export interface NavItem {
label: string;
icon: Icon;
to?: string;
/** Not yet connected to real data — surfaced as a "Soon" badge. */
soon?: boolean;
}
/** A labelled group of nav items. */
export interface NavSection {
/** i18n key or literal heading. Omit for an ungrouped leading block. */
label?: string;
items: NavItem[];
}
/** Both shapes are accepted so callers can migrate to sections gradually. */
export type NavEntries = NavItem[] | NavSection[];
function toSections(entries: NavEntries): NavSection[] {
if (entries.length === 0) return [];
const isSectioned = (entries as NavSection[])[0]?.items !== undefined;
return isSectioned
? (entries as NavSection[])
: [{ items: entries as NavItem[] }];
}
interface AppSidebarProps {
navItems: NavItem[];
navItems: NavEntries;
collapsed: boolean;
activePath: string;
onToggleCollapse: () => void;
@@ -96,14 +116,43 @@ export function AppSidebar({
{/* Navigation items */}
<AppShell.Section grow component={ScrollArea} p="md">
<Stack gap={4}>
{navItems.map((item) => {
<Stack gap={2}>
{toSections(navItems).map((section, sectionIndex) => (
<Stack gap={2} key={section.label ?? `section-${sectionIndex}`}>
{/* Headings are noise when only icons are visible. */}
{section.label && !collapsed && (
<Text
size="xs"
fw={700}
c="dimmed"
mt={sectionIndex === 0 ? 0 : rem(14)}
pl={rem(12)}
style={{ textTransform: 'uppercase', letterSpacing: '0.06em' }}
>
{t(section.label)}
</Text>
)}
{section.label && collapsed && sectionIndex > 0 && (
<div
style={{
height: 1,
margin: `${rem(8)} ${rem(6)}`,
backgroundColor: 'var(--mantine-color-gray-2)',
}}
/>
)}
{section.items.map((item) => {
const ItemIcon = item.icon;
const active = activeNavItem(item);
if (collapsed) {
return (
<Tooltip key={item.label} label={t(item.label)} position="right" withArrow>
<Tooltip
key={item.label}
label={item.soon ? `${t(item.label)}${t('nav.soon', 'Soon')}` : t(item.label)}
position="right"
withArrow
>
<UnstyledButton
onClick={() => onNavigate(item)}
style={{
@@ -113,6 +162,7 @@ export function AppSidebar({
width: '100%',
height: rem(40),
borderRadius: rem(10),
opacity: item.soon ? 0.55 : 1,
color: active ? 'var(--mantine-color-blue-6)' : undefined,
backgroundColor: active ? 'var(--mantine-color-blue-light)' : undefined,
}}
@@ -129,12 +179,25 @@ export function AppSidebar({
active={active}
label={t(item.label)}
leftSection={<ItemIcon size={19} stroke={1.6} />}
// Tells a reviewer at a glance which screens are wired up.
rightSection={
item.soon ? (
<Badge size="xs" variant="light" color="gray" radius="sm">
{t('nav.soon', 'Soon')}
</Badge>
) : undefined
}
onClick={() => onNavigate(item)}
variant="light"
styles={{ root: { borderRadius: rem(10) }, label: { fontWeight: 500 } }}
styles={{
root: { borderRadius: rem(10), opacity: item.soon ? 0.7 : 1 },
label: { fontWeight: 500 },
}}
/>
);
})}
})}
</Stack>
))}
</Stack>
</AppShell.Section>