179 lines
5.5 KiB
TypeScript
179 lines
5.5 KiB
TypeScript
import { Trans, useLingui } from "@lingui/react/macro";
|
|
import type { EvooPose } from "@rakazo/ui-web";
|
|
import { BotAvatar, Button } from "@rakazo/ui-web";
|
|
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
|
import {
|
|
findLookCategory,
|
|
LOOK_CATEGORIES,
|
|
type LookCategoryId,
|
|
type LookPreset,
|
|
lookPresetToAvatarValue,
|
|
} from "../../lib/onboarding-look-categories";
|
|
import { OoAtelierDialog } from "../OoAtelier";
|
|
import { LookPreviewCarousel } from "./LookPreviewCarousel";
|
|
|
|
export interface OnboardingEmojiConfig {
|
|
pose: string;
|
|
avatarValue: string;
|
|
}
|
|
|
|
type OnboardingOoDesignerProps = {
|
|
config: OnboardingEmojiConfig;
|
|
onChange: (config: OnboardingEmojiConfig) => void;
|
|
onContinue: () => void;
|
|
};
|
|
|
|
function categoryLabel(id: LookCategoryId): ReactNode {
|
|
switch (id) {
|
|
case "work":
|
|
return <Trans>Work</Trans>;
|
|
case "hobby":
|
|
return <Trans>Hobby</Trans>;
|
|
case "scene":
|
|
return <Trans>Scene</Trans>;
|
|
case "coder":
|
|
return <Trans>Coder</Trans>;
|
|
case "calm":
|
|
return <Trans>Calm helper</Trans>;
|
|
default: {
|
|
const never: never = id;
|
|
return never;
|
|
}
|
|
}
|
|
}
|
|
|
|
function configFromPreset(preset: LookPreset): OnboardingEmojiConfig {
|
|
return { pose: preset.pose, avatarValue: lookPresetToAvatarValue(preset) };
|
|
}
|
|
|
|
export function OnboardingOoDesigner({ config, onChange, onContinue }: OnboardingOoDesignerProps) {
|
|
const { t } = useLingui();
|
|
const [categoryId, setCategoryId] = useState<LookCategoryId>("calm");
|
|
const category = useMemo(() => findLookCategory(categoryId), [categoryId]);
|
|
const [lookIndex, setLookIndex] = useState(0);
|
|
|
|
const syncCategorySelection = useCallback(
|
|
(nextCategoryId: LookCategoryId, index = 0) => {
|
|
const nextCategory = findLookCategory(nextCategoryId);
|
|
const preset = nextCategory.looks[index] ?? nextCategory.looks[0];
|
|
if (!preset) return;
|
|
setCategoryId(nextCategoryId);
|
|
setLookIndex(index);
|
|
onChange(configFromPreset(preset));
|
|
},
|
|
[onChange],
|
|
);
|
|
|
|
const [atelierOpen, setAtelierOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const match = LOOK_CATEGORIES.find((entry) =>
|
|
entry.looks.some((look) => look.avatar === config.avatarValue),
|
|
);
|
|
if (match) {
|
|
const index = match.looks.findIndex((look) => look.avatar === config.avatarValue);
|
|
setCategoryId(match.id);
|
|
setLookIndex(index >= 0 ? index : 0);
|
|
}
|
|
}, []);
|
|
|
|
const handleLookIndexChange = useCallback(
|
|
(index: number) => {
|
|
const preset = category.looks[index];
|
|
if (!preset) return;
|
|
setLookIndex(index);
|
|
onChange(configFromPreset(preset));
|
|
},
|
|
[category.looks, onChange],
|
|
);
|
|
|
|
function resetLook() {
|
|
syncCategorySelection("calm", 0);
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto max-w-xl" data-testid="onboarding-oo-designer">
|
|
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
|
<Trans>Design your Oo</Trans>
|
|
</h2>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
<Trans>Pick a vibe, swipe through looks, and watch your Oo come alive.</Trans>
|
|
</p>
|
|
|
|
<LookPreviewCarousel
|
|
looks={category.looks}
|
|
activeIndex={lookIndex}
|
|
onIndexChange={handleLookIndexChange}
|
|
preview={
|
|
<BotAvatar
|
|
color={config.avatarValue}
|
|
size={168}
|
|
identity="onboarding-preview"
|
|
pose={config.pose as EvooPose}
|
|
interactive
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<div className="mt-8">
|
|
<p className="text-sm font-medium text-foreground">
|
|
<Trans>What feels like you?</Trans>
|
|
</p>
|
|
<div
|
|
className="mt-3 flex flex-wrap gap-2 pb-1"
|
|
role="tablist"
|
|
aria-label={t`Look families`}
|
|
>
|
|
{LOOK_CATEGORIES.map((entry) => {
|
|
const selected = entry.id === categoryId;
|
|
return (
|
|
<button
|
|
key={entry.id}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={selected}
|
|
data-testid={`onboarding-look-category-${entry.id}`}
|
|
onClick={() => syncCategorySelection(entry.id, 0)}
|
|
className={`shrink-0 rounded-full border px-4 py-2 text-sm font-medium transition-all duration-300 ${
|
|
selected
|
|
? "border-primary bg-primary text-primary-foreground shadow-sm scale-[1.02]"
|
|
: "border-border bg-card text-muted-foreground hover:border-muted hover:bg-muted active:scale-95"
|
|
}`}
|
|
style={{ transitionTimingFunction: "cubic-bezier(0.34, 1.56, 0.64, 1)" }}
|
|
>
|
|
{categoryLabel(entry.id)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 flex items-center justify-between">
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="sm" onClick={resetLook}>
|
|
<Trans>Reset</Trans>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setAtelierOpen(true)}
|
|
data-testid="onboarding-oo-customize"
|
|
>
|
|
<Trans>Customize</Trans>
|
|
</Button>
|
|
</div>
|
|
<Button onClick={onContinue}>
|
|
<Trans>Continue</Trans>
|
|
</Button>
|
|
</div>
|
|
<OoAtelierDialog
|
|
open={atelierOpen}
|
|
onOpenChange={setAtelierOpen}
|
|
value={config.avatarValue}
|
|
identity="onboarding-preview"
|
|
onChange={(avatarValue) => onChange({ ...config, avatarValue })}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|