156 lines
4.6 KiB
TypeScript
156 lines
4.6 KiB
TypeScript
import React from 'react';
|
|
import { YStack, XStack } from '@tamagui/stacks';
|
|
import { SizableText as Text } from '@tamagui/text';
|
|
import { Button } from '@tamagui/button';
|
|
import { useGuestThemeVariant } from '../lib/guestTheme';
|
|
|
|
type ToastAction = { label: string; onClick: () => void };
|
|
type ToastType = 'success' | 'error' | 'info';
|
|
|
|
export type GuestToast = {
|
|
id: number;
|
|
text: string;
|
|
type?: ToastType;
|
|
action?: ToastAction;
|
|
durationMs?: number;
|
|
};
|
|
|
|
type ToastPayload = Omit<GuestToast, 'id'>;
|
|
|
|
const DEFAULT_DURATION = 3000;
|
|
|
|
export default function ToastHost() {
|
|
const [list, setList] = React.useState<GuestToast[]>([]);
|
|
const timeouts = React.useRef<Map<number, number>>(new Map());
|
|
const { isDark } = useGuestThemeVariant();
|
|
|
|
const dismiss = React.useCallback((id: number) => {
|
|
setList((arr) => arr.filter((t) => t.id !== id));
|
|
const timeout = timeouts.current.get(id);
|
|
if (timeout) {
|
|
window.clearTimeout(timeout);
|
|
timeouts.current.delete(id);
|
|
}
|
|
}, []);
|
|
|
|
const push = React.useCallback(
|
|
(toast: ToastPayload) => {
|
|
const id = Date.now() + Math.floor(Math.random() * 1000);
|
|
const durationMs = toast.durationMs ?? DEFAULT_DURATION;
|
|
setList((arr) => [...arr, { id, ...toast, durationMs }]);
|
|
if (durationMs > 0 && typeof window !== 'undefined') {
|
|
const timeout = window.setTimeout(() => dismiss(id), durationMs);
|
|
timeouts.current.set(id, timeout);
|
|
}
|
|
},
|
|
[dismiss]
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
const onEvt = (e: Event) => {
|
|
const detail = (e as CustomEvent<ToastPayload>).detail;
|
|
if (detail?.text) {
|
|
push(detail);
|
|
}
|
|
};
|
|
window.addEventListener('guest-toast', onEvt as EventListener);
|
|
return () => window.removeEventListener('guest-toast', onEvt as EventListener);
|
|
}, [push]);
|
|
|
|
React.useEffect(() => {
|
|
return () => {
|
|
for (const timeout of timeouts.current.values()) {
|
|
window.clearTimeout(timeout);
|
|
}
|
|
timeouts.current.clear();
|
|
};
|
|
}, []);
|
|
|
|
const resolveTone = (type?: ToastType) => {
|
|
if (type === 'error') {
|
|
return {
|
|
border: isDark ? 'rgba(248, 113, 113, 0.5)' : 'rgba(248, 113, 113, 0.4)',
|
|
background: isDark ? 'rgba(127, 29, 29, 0.7)' : 'rgba(254, 226, 226, 0.95)',
|
|
text: isDark ? '#FEE2E2' : '#991B1B',
|
|
};
|
|
}
|
|
if (type === 'info') {
|
|
return {
|
|
border: isDark ? 'rgba(59, 130, 246, 0.45)' : 'rgba(59, 130, 246, 0.35)',
|
|
background: isDark ? 'rgba(30, 64, 175, 0.65)' : 'rgba(219, 234, 254, 0.95)',
|
|
text: isDark ? '#DBEAFE' : '#1D4ED8',
|
|
};
|
|
}
|
|
return {
|
|
border: isDark ? 'rgba(34, 197, 94, 0.45)' : 'rgba(34, 197, 94, 0.35)',
|
|
background: isDark ? 'rgba(22, 101, 52, 0.7)' : 'rgba(220, 252, 231, 0.95)',
|
|
text: isDark ? '#DCFCE7' : '#166534',
|
|
};
|
|
};
|
|
|
|
if (!list.length) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<YStack
|
|
position="fixed"
|
|
left={0}
|
|
right={0}
|
|
bottom={24}
|
|
zIndex={2000}
|
|
alignItems="center"
|
|
pointerEvents="none"
|
|
paddingHorizontal="$3"
|
|
>
|
|
<YStack width="100%" maxWidth={360} gap="$2">
|
|
{list.map((toast) => {
|
|
const tone = resolveTone(toast.type);
|
|
return (
|
|
<XStack
|
|
key={toast.id}
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
gap="$3"
|
|
padding="$3"
|
|
borderRadius="$4"
|
|
borderWidth={1}
|
|
pointerEvents="auto"
|
|
style={{
|
|
backgroundColor: tone.background,
|
|
borderColor: tone.border,
|
|
boxShadow: isDark ? '0 10px 20px rgba(2, 6, 23, 0.45)' : '0 10px 18px rgba(15, 23, 42, 0.12)',
|
|
}}
|
|
>
|
|
<Text fontSize="$2" color={tone.text} flex={1}>
|
|
{toast.text}
|
|
</Text>
|
|
{toast.action ? (
|
|
<Button
|
|
size="$2"
|
|
borderRadius="$pill"
|
|
backgroundColor="transparent"
|
|
borderWidth={1}
|
|
borderColor={tone.border}
|
|
onPress={() => {
|
|
try {
|
|
toast.action?.onClick();
|
|
} finally {
|
|
dismiss(toast.id);
|
|
}
|
|
}}
|
|
>
|
|
<Text fontSize="$1" fontWeight="$7" color={tone.text} textTransform="uppercase" letterSpacing={1.2}>
|
|
{toast.action.label}
|
|
</Text>
|
|
</Button>
|
|
) : null}
|
|
</XStack>
|
|
);
|
|
})}
|
|
</YStack>
|
|
</YStack>
|
|
);
|
|
}
|