added error handling, fixed authenticaton, added toaster

This commit is contained in:
lnn0q 2025-03-30 17:31:12 +03:00
parent 40c35bda13
commit b6b7cea210
18 changed files with 1327 additions and 68 deletions

View file

@ -0,0 +1,134 @@
'use client';
import type { ButtonProps, GroupProps, InputProps, StackProps } from '@chakra-ui/react';
import {
Box,
HStack,
IconButton,
Input,
InputGroup,
Stack,
mergeRefs,
useControllableState
} from '@chakra-ui/react';
import * as React from 'react';
import { LuEye, LuEyeOff } from 'react-icons/lu';
export interface PasswordVisibilityProps {
defaultVisible?: boolean;
visible?: boolean;
onVisibleChange?: (visible: boolean) => void;
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode };
}
export interface PasswordInputProps extends InputProps, PasswordVisibilityProps {
rootProps?: GroupProps;
}
export const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
function PasswordInput(props, ref) {
const {
rootProps,
defaultVisible,
visible: visibleProp,
onVisibleChange,
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
...rest
} = props;
const [visible, setVisible] = useControllableState({
value: visibleProp,
defaultValue: defaultVisible || false,
onChange: onVisibleChange
});
const inputRef = React.useRef<HTMLInputElement>(null);
return (
<InputGroup
endElement={
<VisibilityTrigger
disabled={rest.disabled}
onPointerDown={e => {
if (rest.disabled) return;
if (e.button !== 0) return;
e.preventDefault();
setVisible(!visible);
}}
>
{visible ? visibilityIcon.off : visibilityIcon.on}
</VisibilityTrigger>
}
{...rootProps}
>
<Input {...rest} ref={mergeRefs(ref, inputRef)} type={visible ? 'text' : 'password'} />
</InputGroup>
);
}
);
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
function VisibilityTrigger(props, ref) {
return (
<IconButton
tabIndex={-1}
ref={ref}
me="-2"
aspectRatio="square"
size="sm"
variant="ghost"
height="calc(100% - {spacing.2})"
aria-label="Toggle password visibility"
{...props}
/>
);
}
);
interface PasswordStrengthMeterProps extends StackProps {
max?: number;
value: number;
}
export const PasswordStrengthMeter = React.forwardRef<HTMLDivElement, PasswordStrengthMeterProps>(
function PasswordStrengthMeter(props, ref) {
const { max = 4, value, ...rest } = props;
const percent = (value / max) * 100;
const { label, colorPalette } = getColorPalette(percent);
return (
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
<HStack width="full" ref={ref} {...rest}>
{Array.from({ length: max }).map((_, index) => (
<Box
key={index}
height="1"
flex="1"
rounded="sm"
data-selected={index < value ? '' : undefined}
layerStyle="fill.subtle"
colorPalette="gray"
_selected={{
colorPalette,
layerStyle: 'fill.solid'
}}
/>
))}
</HStack>
{label && <HStack textStyle="xs">{label}</HStack>}
</Stack>
);
}
);
function getColorPalette(percent: number) {
switch (true) {
case percent < 33:
return { label: 'Low', colorPalette: 'red' };
case percent < 66:
return { label: 'Medium', colorPalette: 'orange' };
default:
return { label: 'High', colorPalette: 'green' };
}
}

View file

@ -0,0 +1,39 @@
'use client';
import {
Toaster as ChakraToaster,
Portal,
Spinner,
Stack,
Toast,
createToaster
} from '@chakra-ui/react';
export const toaster = createToaster({
placement: 'bottom-end',
pauseOnPageIdle: true
});
export const Toaster = () => {
return (
<Portal>
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
{toast => (
<Toast.Root width={{ md: 'sm' }}>
{toast.type === 'loading' ? (
<Spinner size="sm" color="blue.solid" />
) : (
<Toast.Indicator />
)}
<Stack gap="1" flex="1" maxWidth="100%">
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
{toast.description && <Toast.Description>{toast.description}</Toast.Description>}
</Stack>
{toast.action && <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>}
{toast.meta?.closable && <Toast.CloseTrigger />}
</Toast.Root>
)}
</ChakraToaster>
</Portal>
);
};