178 lines
5.4 KiB
TypeScript
178 lines
5.4 KiB
TypeScript
import { Button } from '@astryxdesign/core/Button';
|
|
import { Card } from '@astryxdesign/core/Card';
|
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
import { Text } from '@astryxdesign/core/Text';
|
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
import { ArrowRight, Boxes, Rocket, Store, type LucideIcon } from 'lucide-react';
|
|
|
|
/**
|
|
* The first screen, and the only one whose job is not to collect anything.
|
|
*
|
|
* It exists to get somebody to begin, so it says what setup is worth and what
|
|
* it costs, and nothing else. The temptation is to explain the whole product
|
|
* here; a shopkeeper who has just signed in has no way to use any of it yet,
|
|
* and a wall of text before the first field is what makes people close the tab.
|
|
*
|
|
* The one promise made in words is the one the state layer actually keeps:
|
|
* progress is saved, so leaving is safe.
|
|
*/
|
|
/** One of the three cards under the greeting. */
|
|
export interface WelcomeBenefit {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
body: string;
|
|
}
|
|
|
|
export interface WelcomeStepProps {
|
|
shopName?: string;
|
|
done: number;
|
|
total: number;
|
|
isReturning: boolean;
|
|
/**
|
|
* What setup is worth, in three cards.
|
|
*
|
|
* Given by the caller because the two roles are promised different things: a
|
|
* merchant is told about the catalogue and going on sale, a branch user about
|
|
* their own account and their shelf. The SHAPE is shared — that is the point
|
|
* of this component — and only the words differ.
|
|
*/
|
|
benefits?: readonly WelcomeBenefit[];
|
|
/** The greeting, when the default merchant wording is not the right one. */
|
|
greeting?: string;
|
|
blurb?: string;
|
|
onStart: () => void;
|
|
}
|
|
|
|
const BENEFITS = [
|
|
{
|
|
icon: Rocket,
|
|
title: 'Quick setup',
|
|
body: 'Five short steps. Most shops are through it in a few minutes.',
|
|
},
|
|
{
|
|
icon: Boxes,
|
|
title: 'Manage your catalogue',
|
|
body: 'Upload a spreadsheet or add products one at a time — whichever you have.',
|
|
},
|
|
{
|
|
icon: Store,
|
|
title: 'Ready for customers',
|
|
body: 'Finish, and your products are on sale in the app.',
|
|
},
|
|
] as const;
|
|
|
|
export function WelcomeStep({
|
|
shopName,
|
|
done,
|
|
total,
|
|
isReturning,
|
|
benefits = BENEFITS,
|
|
greeting,
|
|
blurb,
|
|
onStart,
|
|
}: WelcomeStepProps) {
|
|
const pct = total === 0 ? 0 : Math.round((done / total) * 100);
|
|
|
|
return (
|
|
<VStack gap={4} className="ob-panel">
|
|
<VStack gap={1.5} style={{ textAlign: 'center', alignItems: 'center' }}>
|
|
{/* A mark rather than an illustration. A stock drawing would be the one
|
|
thing on screen not drawn from the console's own design language. */}
|
|
<span
|
|
aria-hidden
|
|
style={{
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: 20,
|
|
display: 'grid',
|
|
placeItems: 'center',
|
|
background: 'var(--color-brand-tint)',
|
|
color: 'var(--color-brand)',
|
|
}}
|
|
>
|
|
<Store size={28} />
|
|
</span>
|
|
|
|
<Text
|
|
type="display-2"
|
|
weight="semibold"
|
|
style={{ fontFamily: 'var(--font-display)', textWrap: 'balance' }}
|
|
>
|
|
{isReturning ? 'Welcome back 👋' : (greeting ?? `Welcome${shopName ? ` to ${shopName}` : ''}! 👋`)}
|
|
</Text>
|
|
<Text
|
|
type="body"
|
|
color="secondary"
|
|
style={{ maxWidth: '46ch', lineHeight: 1.65 }}
|
|
>
|
|
{isReturning
|
|
? `You have completed ${done} of ${total} setup steps. Pick up where you left off.`
|
|
: (blurb ?? "Let's get your store ready to start selling.")}
|
|
</Text>
|
|
</VStack>
|
|
|
|
<div className="ob-benefits">
|
|
{benefits.map(({ icon: Icon, title, body }) => (
|
|
<Card key={title} padding={3}>
|
|
<VStack gap={1}>
|
|
<span style={{ color: 'var(--color-brand)' }} aria-hidden>
|
|
<Icon size={20} />
|
|
</span>
|
|
<Text type="label" size="sm" weight="semibold">
|
|
{title}
|
|
</Text>
|
|
<Text type="body" size="sm" color="secondary" style={{ lineHeight: 1.6 }}>
|
|
{body}
|
|
</Text>
|
|
</VStack>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<VStack gap={1} style={{ alignItems: 'center' }}>
|
|
<Text type="body" size="sm" color="secondary" hasTabularNumbers>
|
|
{done} of {total} steps completed
|
|
</Text>
|
|
<div
|
|
role="progressbar"
|
|
aria-valuenow={done}
|
|
aria-valuemin={0}
|
|
aria-valuemax={total}
|
|
aria-label="Setup progress"
|
|
style={{
|
|
width: 'min(320px, 100%)',
|
|
height: 6,
|
|
borderRadius: 999,
|
|
background: 'var(--color-line)',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: `${pct}%`,
|
|
height: '100%',
|
|
background: 'var(--color-brand)',
|
|
transition: 'width .35s cubic-bezier(.4,0,.2,1)',
|
|
}}
|
|
/>
|
|
</div>
|
|
</VStack>
|
|
|
|
<VStack gap={1} style={{ alignItems: 'center' }}>
|
|
<HStack justify="center">
|
|
<Button
|
|
label={isReturning ? 'Continue setup' : 'Start setup'}
|
|
variant="primary"
|
|
size="lg"
|
|
endContent={<ArrowRight size={15} />}
|
|
onClick={onStart}
|
|
/>
|
|
</HStack>
|
|
<Text type="body" size="xsm" color="secondary">
|
|
You can save your progress and continue later.
|
|
</Text>
|
|
</VStack>
|
|
</VStack>
|
|
);
|
|
}
|