Introduce official error/warning components
This commit is contained in:
parent
e16dbb80a4
commit
9aa6546b90
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 detected—they may take several minutes to show up
|
||||
for visitors
|
||||
</Banner>}
|
||||
</Note>}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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>}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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',
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
/>
|
||||
@ -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}
|
||||
|
||||
22
src/components/WarningNote.tsx
Normal file
22
src/components/WarningNote.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user