mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-29 07:54:44 +00:00
241 lines
6.9 KiB
TypeScript
241 lines
6.9 KiB
TypeScript
import { JSX, createSignal, Show } from "solid-js";
|
|
|
|
export type CharacterPose =
|
|
| "main"
|
|
| "hello"
|
|
| "laptop"
|
|
| "sleep"
|
|
| "lookup"
|
|
| "success"
|
|
| "like"
|
|
| "walk"
|
|
| "announcement"
|
|
| "educate"
|
|
| "happy_note"
|
|
| "maintenance"
|
|
| "sad"
|
|
// New mascot poses
|
|
| "time"
|
|
| "flag"
|
|
| "error"
|
|
| "headphones"
|
|
| "checkbox"
|
|
| "coffee"
|
|
| "coffe"
|
|
| "angry_bulb"
|
|
| "anoyed_lamp"
|
|
// Grayscaled error states
|
|
| "404"
|
|
| "forbidden"
|
|
| "connection_error"
|
|
| "fixing";
|
|
|
|
interface BookraCharacterProps {
|
|
pose: CharacterPose;
|
|
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
|
animate?: boolean;
|
|
class?: string;
|
|
alt?: string;
|
|
}
|
|
|
|
const sizeMap = {
|
|
xs: "w-16 h-16",
|
|
sm: "w-24 h-24",
|
|
md: "w-32 h-32",
|
|
lg: "w-48 h-48",
|
|
xl: "w-64 h-64",
|
|
"2xl": "w-80 h-80",
|
|
};
|
|
|
|
const illustrationMap: Record<CharacterPose, string> = {
|
|
main: "/bookra-illustrations/main.svg",
|
|
hello: "/bookra-illustrations/hello.svg",
|
|
laptop: "/bookra-illustrations/laptop.svg",
|
|
sleep: "/bookra-illustrations/sleep.svg",
|
|
lookup: "/bookra-illustrations/lookup.svg",
|
|
success: "/bookra-illustrations/success.svg",
|
|
like: "/bookra-illustrations/like.svg",
|
|
walk: "/bookra-illustrations/walk.svg",
|
|
announcement: "/bookra-illustrations/announcement.svg",
|
|
educate: "/bookra-illustrations/educate.svg",
|
|
happy_note: "/bookra-illustrations/happy-note.svg",
|
|
maintenance: "/bookra-illustrations/maintenance.svg",
|
|
sad: "/bookra-illustrations/sad.svg",
|
|
time: "/bookra-illustrations/clock.svg",
|
|
flag: "/bookra-illustrations/flag.svg",
|
|
error: "/bookra-illustrations/sad.svg",
|
|
headphones: "/bookra-illustrations/headphones.svg",
|
|
checkbox: "/bookra-illustrations/happy-note.svg",
|
|
coffee: "/bookra-illustrations/coffee.svg",
|
|
coffe: "/bookra-illustrations/coffee.svg",
|
|
angry_bulb: "/bookra-illustrations/angry-bulb.svg",
|
|
anoyed_lamp: "/bookra-illustrations/angry-bulb.svg",
|
|
"404": "/bookra-illustrations/404.svg",
|
|
forbidden: "/bookra-illustrations/forbidden.svg",
|
|
connection_error: "/bookra-illustrations/connection-error.svg",
|
|
fixing: "/bookra-illustrations/maintenance.svg",
|
|
};
|
|
|
|
export function BookraCharacter(props: BookraCharacterProps): JSX.Element {
|
|
const [isLoaded, setIsLoaded] = createSignal(false);
|
|
const [hasError, setHasError] = createSignal(false);
|
|
|
|
const sizeClass = () => sizeMap[props.size || "md"];
|
|
const animationClass = () => props.animate !== false ? "animate-float" : "";
|
|
|
|
const characterPath = () => illustrationMap[props.pose];
|
|
const label = () => props.alt || `Bookra illustration - ${props.pose.replaceAll("_", " ")}`;
|
|
|
|
return (
|
|
<div
|
|
class={`relative inline-flex items-center justify-center ${sizeClass()} ${props.class || ""}`}
|
|
aria-label={label()}
|
|
>
|
|
<Show when={!hasError()}>
|
|
<img
|
|
src={characterPath()}
|
|
alt={label()}
|
|
class={`w-full h-full object-contain transition-all duration-500 ${
|
|
isLoaded() ? "opacity-100 scale-100" : "opacity-0 scale-95"
|
|
} ${animationClass()}`}
|
|
onLoad={() => setIsLoaded(true)}
|
|
onError={() => setHasError(true)}
|
|
/>
|
|
</Show>
|
|
<Show when={hasError()}>
|
|
<div class="w-full h-full rounded-2xl bg-accent/10 flex items-center justify-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-accent">
|
|
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
<line x1="16" y1="2" x2="16" y2="6" />
|
|
<line x1="8" y1="2" x2="8" y2="6" />
|
|
<line x1="3" y1="10" x2="21" y2="10" />
|
|
</svg>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Character with speech bubble for contextual messages
|
|
interface CharacterMessageProps extends BookraCharacterProps {
|
|
message: string;
|
|
position?: "left" | "right" | "top" | "bottom";
|
|
}
|
|
|
|
export function CharacterWithMessage(props: CharacterMessageProps): JSX.Element {
|
|
const positionClass = () => {
|
|
switch (props.position || "right") {
|
|
case "left": return "flex-row-reverse";
|
|
case "right": return "flex-row";
|
|
case "top": return "flex-col-reverse";
|
|
case "bottom": return "flex-col";
|
|
}
|
|
};
|
|
|
|
const bubblePosition = () => {
|
|
switch (props.position || "right") {
|
|
case "left": return "mr-4";
|
|
case "right": return "ml-4";
|
|
case "top": return "mb-3";
|
|
case "bottom": return "mt-3";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div class={`flex items-center ${positionClass()}`}>
|
|
<BookraCharacter
|
|
pose={props.pose}
|
|
size={props.size}
|
|
animate={props.animate}
|
|
class={props.class}
|
|
alt={props.alt}
|
|
/>
|
|
<div class={`surface-elevated px-4 py-3 rounded-2xl rounded-${
|
|
props.position === "left" ? "r" :
|
|
props.position === "right" ? "l" :
|
|
props.position === "top" ? "b" : "t"
|
|
}-lg max-w-xs ${bubblePosition()}`}>
|
|
<p class="text-ink text-sm leading-relaxed">{props.message}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Empty state with character
|
|
interface CharacterEmptyStateProps {
|
|
pose: CharacterPose;
|
|
title: string;
|
|
message: string;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
size?: "sm" | "md" | "lg";
|
|
}
|
|
|
|
export function CharacterEmptyState(props: CharacterEmptyStateProps): JSX.Element {
|
|
return (
|
|
<div class="flex flex-col items-center text-center py-12 px-4">
|
|
<BookraCharacter
|
|
pose={props.pose}
|
|
size={props.size || "lg"}
|
|
animate={true}
|
|
class="mb-6"
|
|
/>
|
|
<h3 class="font-display text-xl font-semibold text-ink mb-2">
|
|
{props.title}
|
|
</h3>
|
|
<p class="text-ink-muted max-w-sm mb-6 leading-relaxed">
|
|
{props.message}
|
|
</p>
|
|
{props.action && (
|
|
<button
|
|
onClick={props.action.onClick}
|
|
class="btn-primary"
|
|
>
|
|
{props.action.label}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Success state with character celebration
|
|
interface CharacterSuccessStateProps {
|
|
title: string;
|
|
message: string;
|
|
action?: {
|
|
label: string;
|
|
onClick: () => void;
|
|
};
|
|
}
|
|
|
|
export function CharacterSuccessState(props: CharacterSuccessStateProps): JSX.Element {
|
|
return (
|
|
<div class="flex flex-col items-center text-center py-12 px-4 animate-fade-in">
|
|
<div class="relative mb-6">
|
|
<BookraCharacter
|
|
pose="success"
|
|
size="xl"
|
|
animate={true}
|
|
/>
|
|
<div class="absolute -top-2 -right-2 text-3xl animate-bounce">✨</div>
|
|
</div>
|
|
<h3 class="font-display text-2xl font-semibold text-ink mb-3">
|
|
{props.title}
|
|
</h3>
|
|
<p class="text-ink-muted max-w-md mb-8 leading-relaxed">
|
|
{props.message}
|
|
</p>
|
|
{props.action && (
|
|
<button
|
|
onClick={props.action.onClick}
|
|
class="btn-primary inline-flex items-center gap-2"
|
|
>
|
|
{props.action.label}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|