Introduce official error/warning components

This commit is contained in:
Sam Becker 2024-06-20 19:25:15 -05:00
parent e16dbb80a4
commit 9aa6546b90
14 changed files with 113 additions and 65 deletions

View File

@ -2,7 +2,7 @@
import ErrorNote from '@/components/ErrorNote';
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import InfoBlock from '@/components/InfoBlock';
import Container from '@/components/Container';
import LoaderButton from '@/components/primitives/LoaderButton';
import { addAllUploadsAction } from '@/photo/actions';
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
@ -84,7 +84,7 @@ export default function AdminAddAllUploads({
<>
{actionErrorMessage &&
<ErrorNote>{actionErrorMessage}</ErrorNote>}
<InfoBlock padding="tight">
<Container padding="tight">
<div className="w-full space-y-4 py-1">
<div className="flex">
<div className={clsx(
@ -173,7 +173,7 @@ export default function AdminAddAllUploads({
</div>}
</div>
</div>
</InfoBlock>
</Container>
</>
);
}

View File

@ -1,6 +1,6 @@
'use client';
import Banner from '@/components/Banner';
import Note from '@/components/Note';
import SiteGrid from '@/components/SiteGrid';
import {
PATH_ADMIN_CONFIGURATION,
@ -96,10 +96,10 @@ export default function AdminNavClient({
</Link>
</div>
{shouldShowBanner &&
<Banner icon={<FaRegClock className="flex-shrink-0" />}>
<Note icon={<FaRegClock className="flex-shrink-0" />}>
Photo updates detectedthey may take several minutes to show up
for visitors
</Banner>}
</Note>}
</div>
}
/>

View File

@ -4,7 +4,7 @@ import { OUTDATED_THRESHOLD, Photo } from '@/photo';
import AdminPhotosTable from '@/admin/AdminPhotosTable';
import LoaderButton from '@/components/primitives/LoaderButton';
import IconGrSync from '@/site/IconGrSync';
import Banner from '@/components/Banner';
import Note from '@/components/Note';
import AdminChildPage from '@/components/AdminChildPage';
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
import { useState } from 'react';
@ -74,7 +74,7 @@ export default function AdminOutdatedClient({
</LoaderButton>}
>
<div className="space-y-6">
<Banner>
<Note>
<div className="space-y-1.5">
{photos.length}
{' '}
@ -88,7 +88,7 @@ export default function AdminOutdatedClient({
undesired privacy settings
{hasAiTextGeneration && ', missing AI-generated text'}
</div>
</Banner>
</Note>
<div className="space-y-4">
<AdminPhotosTable
photos={photos}

View File

@ -1,5 +1,5 @@
import ClearCacheButton from '@/admin/ClearCacheButton';
import InfoBlock from '@/components/InfoBlock';
import Container from '@/components/Container';
import SiteGrid from '@/components/SiteGrid';
import SiteChecklist from '@/site/SiteChecklist';
@ -14,9 +14,9 @@ export default async function AdminConfigurationPage() {
</div>
<ClearCacheButton />
</div>
<InfoBlock spaceChildren={false}>
<Container spaceChildren={false}>
<SiteChecklist />
</InfoBlock>
</Container>
</div>}
/>
);

View File

@ -1,5 +1,5 @@
import AnimateItems from '@/components/AnimateItems';
import Banner from '@/components/Banner';
import Note from '@/components/Note';
import SiteGrid from '@/components/SiteGrid';
import PhotoGrid from '@/photo/PhotoGrid';
import { getPhotosNoStore } from '@/photo/cache';
@ -63,9 +63,9 @@ export default async function HiddenTagPage() {
animateOnFirstLoadOnly
/>
<div className="space-y-6">
<Banner animate>
<Note animate>
Only visible to authenticated admins
</Banner>
</Note>
<PhotoGrid {...{ photos }} />
</div>
</div>}

View File

@ -1,7 +1,7 @@
'use client';
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import InfoBlock from '@/components/InfoBlock';
import Container from '@/components/Container';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { getAuthAction, signInAction } from './actions';
@ -40,7 +40,7 @@ export default function SignInForm() {
password.length > 0;
return (
<InfoBlock className={clsx(
<Container className={clsx(
'w-[calc(100vw-1.5rem)] sm:w-[min(360px,90vw)]',
'px-6 py-5',
)}>
@ -89,6 +89,6 @@ export default function SignInForm() {
</SubmitButtonWithStatus>
</div>
</form>
</InfoBlock>
</Container>
);
}

View File

@ -8,6 +8,7 @@ export default function ChecklistRow({
status,
isPending,
optional,
showWarning,
experimental,
children,
}: {
@ -15,6 +16,7 @@ export default function ChecklistRow({
status: boolean
isPending?: boolean
optional?: boolean
showWarning?: boolean
experimental?: boolean
children: ReactNode
}) {
@ -24,7 +26,11 @@ export default function ChecklistRow({
'px-4 pt-2 pb-2.5',
)}>
<StatusIcon
type={status ? 'checked' : optional ? 'optional' : 'missing'}
type={status
? 'checked'
: showWarning
? 'warning'
: optional ? 'optional' : 'missing'}
loading={isPending}
/>
<div className="flex flex-col min-w-0 flex-grow">

View File

@ -1,7 +1,7 @@
import { clsx } from 'clsx/lite';
import { ReactNode } from 'react';
export default function InfoBlock({
export default function Container({
children,
className,
color = 'gray',
@ -11,7 +11,7 @@ export default function InfoBlock({
}: {
children: ReactNode
className?: string
color?: 'gray' | 'blue'
color?: 'gray' | 'blue' | 'red' | 'yellow'
padding?: 'loose' | 'normal' | 'tight'
centered?: boolean
spaceChildren?: boolean
@ -28,6 +28,16 @@ export default function InfoBlock({
'bg-blue-50/50 border-blue-200',
'dark:bg-blue-950/30 dark:border-blue-600/50',
];
case 'red': return [
'text-red-600 dark:text-red-500/90',
'bg-red-50/50 dark:bg-red-950/50',
'border-red-100 dark:border-red-950',
];
case 'yellow': return [
'text-amber-700 dark:text-amber-500/90',
'bg-amber-50/50 dark:bg-amber-950/30',
'border-amber-200/80 dark:border-amber-800/30',
];
}
};

View File

@ -1,33 +1,22 @@
import { clsx } from 'clsx/lite';
import { ReactNode } from 'react';
import { BiErrorAlt } from 'react-icons/bi';
import Note from './Note';
export default function ErrorNote({
className,
children,
size = 'medium',
}: {
className?: string
children: ReactNode
size?: 'small' | 'medium'
}) {
return (
<div className={clsx(
'flex w-full items-center gap-3 border',
size === 'medium'
? 'px-3 py-2'
: 'px-1.5 py-1',
'text-red-600 dark:text-red-500/90',
'bg-red-50/50 dark:bg-red-950/50',
'border-red-100 dark:border-red-950',
'rounded-md',
className,
)}>
<BiErrorAlt
size={18}
className="text-red-600/80 dark:text-red-500/70 shrink-0"
/>
<Note
color="red"
padding="tight"
className={className}
icon={<BiErrorAlt size={18} />}
>
{children}
</div>
</Note>
);
}

View File

@ -1,38 +1,39 @@
import { ReactNode } from 'react';
import InfoBlock from './InfoBlock';
import { ComponentProps, ReactNode } from 'react';
import Container from './Container';
import AnimateItems from './AnimateItems';
import { IoInformationCircleOutline } from 'react-icons/io5';
export default function Banner({
icon,
export default function Note({
children,
animate,
className,
color = 'blue',
icon,
animate,
}: {
icon?: ReactNode
children: ReactNode
animate?: boolean
className?: string
}) {
} & ComponentProps<typeof Container>) {
return (
<AnimateItems
type={animate ? 'bottom' : 'none'}
items={[
<InfoBlock
<Container
key="Banner"
className={className}
centered={false}
padding="tight"
color="blue"
color={color}
>
<div className="flex items-center gap-2.5">
{icon ?? <IoInformationCircleOutline
size={18}
className="translate-y-[1px] shrink-0"
/>}
<span className="shrink-0 opacity-90">
{icon ?? <IoInformationCircleOutline
size={18}
className="translate-y-[1px]"
/>}
</span>
{children}
</div>
</InfoBlock>,
</Container>,
]}
animateOnFirstLoadOnly
/>

View File

@ -9,7 +9,7 @@ export default function StatusIcon({
type,
loading,
}: {
type: 'checked' | 'missing' | 'optional'
type: 'checked' | 'missing' | 'warning' | 'optional'
loading?: boolean
}) {
const getIcon = () => {
@ -24,6 +24,11 @@ export default function StatusIcon({
size={14}
className="text-red-400 translate-x-[2px] translate-y-[1.5px]"
/>;
case 'warning':
return <BiSolidXSquare
size={14}
className="text-amber-400 translate-x-[2px] translate-y-[1.5px]"
/>;
case 'optional':
return <BiSolidCheckboxMinus
size={18}

View File

@ -0,0 +1,22 @@
import { ReactNode } from 'react';
import { PiWarningBold } from 'react-icons/pi';
import Note from './Note';
export default function WarningNote({
className,
children,
}: {
className?: string
children: ReactNode
}) {
return (
<Note
color="yellow"
padding="tight"
className={className}
icon={<PiWarningBold size={18} />}
>
{children}
</Note>
);
}

View File

@ -1,5 +1,5 @@
import AdminCTA from '@/admin/AdminCTA';
import InfoBlock from '@/components/InfoBlock';
import Container from '@/components/Container';
import SiteGrid from '@/components/SiteGrid';
import { IS_SITE_READY } from '@/site/config';
import { PATH_ADMIN_CONFIGURATION } from '@/site/paths';
@ -12,7 +12,7 @@ export default function PhotosEmptyState() {
return (
<SiteGrid
contentMain={
<InfoBlock
<Container
className="min-h-[20rem] sm:min-h-[30rem] px-8"
padding="loose"
>
@ -47,7 +47,7 @@ export default function PhotosEmptyState() {
</Link>
</div>
</div>}
</InfoBlock>}
</Container>}
/>
);
};

View File

@ -14,7 +14,7 @@ import {
BiLockAlt,
BiPencil,
} from 'react-icons/bi';
import InfoBlock from '@/components/InfoBlock';
import Container from '@/components/Container';
import Checklist from '@/components/Checklist';
import { toastSuccess } from '@/toast';
import { ConfigChecklistStatus } from './config';
@ -25,6 +25,7 @@ import LoaderButton from '@/components/primitives/LoaderButton';
import { testConnectionsAction } from '@/admin/actions';
import ErrorNote from '@/components/ErrorNote';
import Spinner from '@/components/Spinner';
import WarningNote from '@/components/WarningNote';
export default function SiteChecklistClient({
// Config checklist
@ -167,13 +168,27 @@ export default function SiteChecklistClient({
connection?: { provider: string, error: string }
message?: string
}) =>
<ErrorNote size="small" className="mt-2 mb-3">
<ErrorNote className="mt-2 mb-3">
{connection && <>
{connection.provider} connection error: {`"${connection.error}"`}
</>}
{message}
</ErrorNote>;
const renderWarning = ({
connection,
message,
}: {
connection?: { provider: string, error: string }
message?: string
}) =>
<WarningNote className="mt-2 mb-3">
{connection && <>
{connection.provider} connection error: {`"${connection.error}"`}
</>}
{message}
</WarningNote>;
return (
<div className="max-w-xl w-full">
<div className="space-y-6">
@ -277,7 +292,7 @@ export default function SiteChecklistClient({
Store auth secret in environment variable:
{!hasAuthSecret &&
<div className="overflow-x-auto">
<InfoBlock className="my-1.5 inline-flex" padding="tight">
<Container className="my-1.5 inline-flex" padding="tight">
<div className={clsx(
'flex flex-nowrap items-center gap-2 leading-none -mx-1',
)}>
@ -288,7 +303,7 @@ export default function SiteChecklistClient({
{renderCopyButton('Secret', secret)}
</div>
</div>
</InfoBlock>
</Container>
</div>}
{renderEnvVars(['AUTH_SECRET'])}
</ChecklistRow>
@ -314,8 +329,8 @@ export default function SiteChecklistClient({
status={hasDomain}
>
{!hasDomain &&
renderError({message:
'Not configuring a domain may cause ' +
renderWarning({message:
'Not explicitly setting a domain may cause ' +
'certain features to behave unexpectedly',
})}
Store in environment variable (displayed in top-right nav):