Skip to content

Commit 3787131

Browse files
committed
fix(admin): add empty states for image and album management
1 parent 4c7cfee commit 3787131

7 files changed

Lines changed: 120 additions & 13 deletions

File tree

components/admin/album/album-list.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import React, { useState } from 'react'
44
import { useSwrHydrated } from '~/hooks/use-swr-hydrated'
5-
import { ArrowDown10 } from 'lucide-react'
5+
import { ArrowDown10, FolderOpen } from 'lucide-react'
66
import { toast } from 'sonner'
77
import type { AlbumType } from '~/types'
88
import type { HandleProps } from '~/types/props'
@@ -24,9 +24,10 @@ import { DeleteIcon } from '~/components/icons/delete'
2424
import { useTranslations } from 'next-intl'
2525
import { Badge } from '~/components/ui/badge'
2626
import { AnimatedIconTrigger, mergeAnimatedTriggerProps } from '~/components/icons/animated-trigger'
27+
import AdminEmptyState from '~/components/admin/empty-state'
2728

2829
export default function AlbumList(props : Readonly<HandleProps>) {
29-
const { data, mutate } = useSwrHydrated(props)
30+
const { data, isLoading, mutate } = useSwrHydrated(props)
3031
const [album, setAlbum] = useState({} as AlbumType)
3132
const [deleteLoading, setDeleteLoading] = useState(false)
3233
const [updateAlbumLoading, setUpdateAlbumLoading] = useState(false)
@@ -35,6 +36,8 @@ export default function AlbumList(props : Readonly<HandleProps>) {
3536
(state) => state,
3637
)
3738
const t = useTranslations()
39+
const albumList: AlbumType[] = data ?? []
40+
const isEmpty = !isLoading && albumList.length === 0
3841

3942
async function deleteAlbum() {
4043
setDeleteLoading(true)
@@ -86,7 +89,14 @@ export default function AlbumList(props : Readonly<HandleProps>) {
8689

8790
return (
8891
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
89-
{data && data.map((album: AlbumType) => (
92+
{isEmpty ? (
93+
<AdminEmptyState
94+
className="col-span-full min-h-[22rem]"
95+
icon={FolderOpen}
96+
title={t('Album.emptyTitle')}
97+
description={t('Album.emptyDescription')}
98+
/>
99+
) : albumList.map((album: AlbumType) => (
90100
<Card key={album.id} className="flex flex-col h-72 show-up-motion items-center gap-0 py-0">
91101
<div className="flex h-12 justify-start w-full p-2 space-x-2">
92102
<Badge aria-label={t('Album.albumName')}>{album.name}</Badge>

components/admin/empty-state.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { LucideIcon } from 'lucide-react'
2+
3+
import { cn } from '~/lib/utils'
4+
5+
type AdminEmptyStateProps = {
6+
icon: LucideIcon
7+
title: string
8+
description: string
9+
className?: string
10+
}
11+
12+
export default function AdminEmptyState({
13+
icon: Icon,
14+
title,
15+
description,
16+
className,
17+
}: Readonly<AdminEmptyStateProps>) {
18+
return (
19+
<div
20+
className={cn(
21+
'show-up-motion flex w-full items-center justify-center rounded-[1.35rem] border border-dashed border-border/80 bg-background/60 px-6 py-10 text-center',
22+
className
23+
)}
24+
>
25+
<div className='mx-auto flex max-w-md flex-col items-center'>
26+
<div className='flex size-12 items-center justify-center rounded-full bg-primary/10 text-primary/80'>
27+
<Icon className='size-5' />
28+
</div>
29+
<h3 className='mt-4 font-display text-[1.35rem] leading-none text-foreground'>
30+
{title}
31+
</h3>
32+
<p className='mt-2 max-w-sm text-sm leading-6 text-muted-foreground line-clamp-2'>
33+
{description}
34+
</p>
35+
</div>
36+
</div>
37+
)
38+
}

components/admin/list/list-props.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { ImageType, AlbumType } from '~/types'
55
import type { ImageListDataProps, ImageServerHandleProps } from '~/types/props'
66
import { useSwrInfiniteServerHook } from '~/hooks/use-swr-infinite-server-hook'
77
import { useSwrPageTotalServerHook } from '~/hooks/use-swr-page-total-server-hook'
8-
import { ArrowDown10, ScanSearch, Replace } from 'lucide-react'
8+
import { ArrowDown10, Images, Replace, ScanSearch } from 'lucide-react'
99
import { toast } from 'sonner'
1010
import { useButtonStore } from '~/app/providers/button-store-providers'
1111
import ImageEditSheet from '~/components/admin/list/image-edit-sheet'
@@ -48,6 +48,7 @@ import {
4848
PopoverContent,
4949
PopoverTrigger,
5050
} from '~/components/ui/popover'
51+
import AdminEmptyState from '~/components/admin/empty-state'
5152
import { RefreshCWIcon } from '~/components/icons/refresh-cw.tsx'
5253
import { CircleChevronDownIcon } from '~/components/icons/circle-chevron-down.tsx'
5354
import { AnimatedIconTrigger, mergeAnimatedTriggerProps } from '~/components/icons/animated-trigger'
@@ -106,6 +107,17 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
106107
}
107108
}, [adminConfig])
108109

110+
useEffect(() => {
111+
setPageNum((current) => current === 1 ? current : 1)
112+
}, [album, showStatus, selectedCamera, selectedLens])
113+
114+
useEffect(() => {
115+
const totalPages = Math.max(1, Math.ceil((total ?? 0) / pageSize))
116+
if (!isLoading && (total ?? 0) > 0 && pageNum > totalPages) {
117+
setPageNum(totalPages)
118+
}
119+
}, [isLoading, pageNum, pageSize, total])
120+
109121
const updateImageShow = useCallback(async (id: string, show: number) => {
110122
try {
111123
setUpdateShowLoading(true)
@@ -166,6 +178,14 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
166178
}
167179
}
168180

181+
const hasActiveFilters = [album, showStatus, selectedCamera, selectedLens].some(
182+
(value) => value !== '' && value !== 'all'
183+
)
184+
const imageCount = Array.isArray(data) ? data.length : 0
185+
const isCollectionEmpty = !isLoading && imageCount === 0 && (total ?? 0) === 0
186+
const isEmptyDatabase = isCollectionEmpty && !hasActiveFilters
187+
const isFilteredEmpty = isCollectionEmpty && hasActiveFilters
188+
169189
return (
170190
<div className="flex flex-col space-y-2 h-full flex-1">
171191
<div className="flex justify-between space-x-1">
@@ -270,6 +290,7 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
270290
variant="outline"
271291
size="icon"
272292
aria-label={t('Button.batchDelete')}
293+
disabled={isLoading || (total ?? 0) === 0}
273294
{...mergeAnimatedTriggerProps({
274295
onClick: () => setImageBatchDelete(true)
275296
}, triggerProps)}
@@ -386,7 +407,21 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
386407
</div>
387408
</div>
388409
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
389-
{Array.isArray(data) && data?.map((image: ImageType) => (
410+
{isEmptyDatabase ? (
411+
<AdminEmptyState
412+
className="col-span-full min-h-[22rem]"
413+
icon={Images}
414+
title={t('List.emptyTitle')}
415+
description={t('List.emptyDescription')}
416+
/>
417+
) : isFilteredEmpty ? (
418+
<AdminEmptyState
419+
className="col-span-full min-h-[22rem]"
420+
icon={Images}
421+
title={t('List.filteredEmptyTitle')}
422+
description={t('List.filteredEmptyDescription')}
423+
/>
424+
) : Array.isArray(data) && data?.map((image: ImageType) => (
390425
<Card key={image.id} className="flex flex-col h-72 show-up-motion items-center gap-0 py-0">
391426
<div className="flex h-12 justify-between w-full p-2 space-x-2">
392427
<Badge variant="secondary" aria-label={t('Words.album')}>{image.album_name}</Badge>

messages/en.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,11 @@
292292
"showStatus": "Show Status",
293293
"showStatusDesc": "Whether to display the image",
294294
"homepageShowStatus": "Homepage Show Status",
295-
"homepageShowStatusDesc": "Whether to display the image on the homepage"
295+
"homepageShowStatusDesc": "Whether to display the image on the homepage",
296+
"emptyTitle": "No images yet",
297+
"emptyDescription": "Uploaded images will appear here for management.",
298+
"filteredEmptyTitle": "No matching images",
299+
"filteredEmptyDescription": "There are no images under the current filters. Adjust them and try again."
296300
},
297301
"Album": {
298302
"router": "Router",
@@ -328,7 +332,9 @@
328332
"update": "Update",
329333
"requiredFields": "Please fill in the required fields first!",
330334
"routerStartWithSlash": "Router must start with /!",
331-
"rootRouterWarning": "/ router cannot be set to hidden!"
335+
"rootRouterWarning": "/ router cannot be set to hidden!",
336+
"emptyTitle": "No albums yet",
337+
"emptyDescription": "Your album list will appear here after you create one."
332338
},
333339
"Preferences": {
334340
"webSiteTitle": "Website Title",

messages/ja.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,11 @@
292292
"showStatus": "表示状態",
293293
"showStatusDesc": "画像を表示するかどうか",
294294
"homepageShowStatus": "ホーム表示状態",
295-
"homepageShowStatusDesc": "ホームページに画像を表示するかどうか"
295+
"homepageShowStatusDesc": "ホームページに画像を表示するかどうか",
296+
"emptyTitle": "画像はまだありません",
297+
"emptyDescription": "画像をアップロードすると、ここでまとめて管理できます。",
298+
"filteredEmptyTitle": "一致する画像がありません",
299+
"filteredEmptyDescription": "現在の絞り込み条件では結果がありません。条件を調整してもう一度確認してください。"
296300
},
297301
"Album": {
298302
"router": "ルーター",
@@ -328,7 +332,9 @@
328332
"update": "更新",
329333
"requiredFields": "必須項目を入力してください!",
330334
"routerStartWithSlash": "ルーターは / で始める必要があります!",
331-
"rootRouterWarning": "/ ルーターは非表示に設定できません!"
335+
"rootRouterWarning": "/ ルーターは非表示に設定できません!",
336+
"emptyTitle": "アルバムはまだありません",
337+
"emptyDescription": "アルバムを作成すると、ここに一覧が表示されます。"
332338
},
333339
"Preferences": {
334340
"webSiteTitle": "ウェブサイトタイトル",

messages/zh-TW.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,11 @@
292292
"showStatus": "顯示狀態",
293293
"showStatusDesc": "是否需要顯示圖片",
294294
"homepageShowStatus": "首頁顯示狀態",
295-
"homepageShowStatusDesc": "是否需要在首頁顯示圖片"
295+
"homepageShowStatusDesc": "是否需要在首頁顯示圖片",
296+
"emptyTitle": "還沒有圖片",
297+
"emptyDescription": "圖片上傳後會在這裡統一維護。",
298+
"filteredEmptyTitle": "沒有匹配的圖片",
299+
"filteredEmptyDescription": "目前篩選條件下沒有結果,調整篩選後再查看。"
296300
},
297301
"Album": {
298302
"router": "路由",
@@ -328,7 +332,9 @@
328332
"update": "更新",
329333
"requiredFields": "請先填寫必填項!",
330334
"routerStartWithSlash": "路由必須以 / 開頭!",
331-
"rootRouterWarning": "/ 路由不允許設置為不顯示!"
335+
"rootRouterWarning": "/ 路由不允許設置為不顯示!",
336+
"emptyTitle": "還沒有相簿",
337+
"emptyDescription": "建立相簿後,這裡會顯示你的相簿列表。"
332338
},
333339
"Preferences": {
334340
"webSiteTitle": "網站標題",

messages/zh.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,11 @@
292292
"showStatus": "显示状态",
293293
"showStatusDesc": "是否需要显示图片",
294294
"homepageShowStatus": "首页显示状态",
295-
"homepageShowStatusDesc": "是否需要在首页显示图片"
295+
"homepageShowStatusDesc": "是否需要在首页显示图片",
296+
"emptyTitle": "还没有图片",
297+
"emptyDescription": "图片上传后会在这里统一维护。",
298+
"filteredEmptyTitle": "没有匹配的图片",
299+
"filteredEmptyDescription": "当前筛选条件下暂无结果,调整筛选后再查看。"
296300
},
297301
"Album": {
298302
"router": "路由",
@@ -328,7 +332,9 @@
328332
"update": "更新",
329333
"requiredFields": "请先填写必填项!",
330334
"routerStartWithSlash": "路由必须以 / 开头!",
331-
"rootRouterWarning": "/ 路由不允许设置为不显示!"
335+
"rootRouterWarning": "/ 路由不允许设置为不显示!",
336+
"emptyTitle": "还没有相册",
337+
"emptyDescription": "创建相册后,这里会显示你的相册列表。"
332338
},
333339
"Preferences": {
334340
"webSiteTitle": "网站标题",

0 commit comments

Comments
 (0)