first commit

This commit is contained in:
2026-08-05 15:26:50 +05:30
parent c000e5dc95
commit 2c8c394795
126 changed files with 10824 additions and 302 deletions

View File

@@ -0,0 +1,60 @@
import {VStack} from '@astryxdesign/core/Layout';
import {Badge} from '@astryxdesign/core/Badge';
import {Text} from '@astryxdesign/core/Text';
import {Divider} from '@astryxdesign/core/Divider';
import {PageHeader} from '@/components/primitives/PageHeader';
import {StaticPanel} from '@/components/patterns/PanelCard';
import {StatPair, StatRow} from '@/components/patterns/StatPair';
import {EmptyPanel} from '@/components/patterns/EmptyPanel';
import {readProfile} from '@/lib/mock/settings';
import {formatInr} from '@/lib/format';
/**
* Billing is read-only for now.
*
* The plan and settlement figures are real shapes, but there is no payment
* provider wired up — so rather than render fake "Change plan" flows that do
* nothing, the actions are absent and the invoice list says plainly that it
* is not connected yet. A disabled button that never becomes enabled is worse
* than no button.
*/
export default function SettingsBillingPage() {
const profile = readProfile();
return (
<VStack gap={5}>
<PageHeader
title="Billing"
description="Plan, LYT settlement and invoices."
/>
<StaticPanel
title="Plan"
subtitle={profile.businessName}
actions={<Badge variant="success" label="Active" />}
>
<VStack gap={4}>
<StatRow>
<StatPair label="Plan" value="Growth — 5 stores" />
<StatPair label="Billed" value="Monthly" />
<StatPair label="Next charge" value="1 Sep 2026" />
<StatPair label="Amount" value={formatInr(14999)} align="end" />
</StatRow>
<Divider />
<Text size="sm" color="secondary">
LYTs are settled monthly against redemptions, at 1 LYT = ₹1. Your
outstanding liability is shown on the Lyts page.
</Text>
</VStack>
</StaticPanel>
<StaticPanel title="Invoices" subtitle="Past statements">
<EmptyPanel
icon="revenue"
title="No billing provider connected"
description="Invoices appear here once payments are wired up."
/>
</StaticPanel>
</VStack>
);
}

View File

@@ -0,0 +1,93 @@
'use client';
import {usePathname} from 'next/navigation';
import {
Layout,
LayoutContent,
LayoutPanel,
VStack,
} from '@astryxdesign/core/Layout';
import {SideNav, SideNavItem, SideNavSection} from '@astryxdesign/core/SideNav';
import {TabList, Tab} from '@astryxdesign/core/TabList';
import {Icon} from '@astryxdesign/core/Icon';
import {useRouter} from 'next/navigation';
import {useBreakpoint} from '@/lib/breakpoints';
import {SETTINGS_NAV, isSettingsActive} from '@/features/settings/settings-nav';
/**
* Settings gets its own sub-navigation.
*
* A nested Layout with a `start` panel rather than more entries in the primary
* sidebar: settings sections sit at a different level of the hierarchy, and
* promoting them would push Dashboard/Store/Lyts/Staff down among
* configuration screens they have nothing to do with.
*
* SideNav is reused (rather than a bespoke list) so a settings section looks
* and behaves exactly like a primary nav item — same selected treatment, same
* hover, same keyboard handling, no second definition to keep in sync.
*
* Below the laptop breakpoint a 220px panel would eat most of the width, so
* the sub-nav becomes a horizontal TabList above the content instead.
*/
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const bp = useBreakpoint();
const isNarrow = bp === 'mobile' || bp === 'tablet';
if (isNarrow) {
const active =
SETTINGS_NAV.find((s) => isSettingsActive(pathname, s.href))?.href ??
SETTINGS_NAV[0].href;
return (
<VStack gap={5}>
<TabList
value={active}
onChange={(href) => router.push(href)}
layout="fill"
size="sm"
hasDivider
>
{SETTINGS_NAV.map((s) => (
<Tab
key={s.href}
value={s.href}
label={s.label}
icon={<Icon icon={s.icon} size="sm" />}
/>
))}
</TabList>
{children}
</VStack>
);
}
return (
<Layout
height="fill"
start={
<LayoutPanel width={220} padding={0} role="navigation" label="Settings">
<SideNav>
<SideNavSection title="Settings" isHeaderHidden>
{SETTINGS_NAV.map((s) => (
<SideNavItem
key={s.href}
label={s.label}
icon={s.icon}
href={s.href}
isSelected={isSettingsActive(pathname, s.href)}
/>
))}
</SideNavSection>
</SideNav>
</LayoutPanel>
}
content={<LayoutContent padding={0}>{children}</LayoutContent>}
/>
);
}

View File

@@ -0,0 +1,19 @@
import {VStack} from '@astryxdesign/core/Layout';
import {PageHeader} from '@/components/primitives/PageHeader';
import {NotificationsForm} from '@/features/settings/NotificationsForm';
import {readProfile} from '@/lib/mock/settings';
/** Server Component — same direct-read hybrid as the profile page. */
export default function SettingsNotificationsPage() {
const {notifications} = readProfile();
return (
<VStack gap={5}>
<PageHeader
title="Notifications"
description="Choose which events are worth interrupting you for."
/>
<NotificationsForm initialData={notifications} />
</VStack>
);
}

View File

@@ -0,0 +1,30 @@
import {VStack} from '@astryxdesign/core/Layout';
import {PageHeader} from '@/components/primitives/PageHeader';
import {ProfileForm} from '@/features/settings/ProfileForm';
import {readProfile} from '@/lib/mock/settings';
/**
* A SERVER Component — the one place in the app that fetches on the server.
*
* Every other page fetches on the client because its scope (store, range)
* changes from controls that live above it in the tree, and a route
* navigation would remount the Copilot. Settings has none of that: there is
* exactly one server-known record at page load, so reading it directly here
* (no HTTP hop, no loading skeleton, no empty-input flash) is strictly better.
*
* The same readProfile() backs /api/settings/profile, so the two transports
* cannot drift.
*/
export default function SettingsProfilePage() {
const profile = readProfile();
return (
<VStack gap={5}>
<PageHeader
title="Settings"
description="Business details, contact information and LYT earn rate."
/>
<ProfileForm initialData={profile} />
</VStack>
);
}