feat: new resource dashboard (#1346)

* feat: refator the file dashboard

* feat: support select resouce file

* feat: suppor delete select files

* feat: support share menu, implement rename and delete

* chore: change the color of hover

* chore: refator file dashboard to page

* feat: add i18n for button

* feat: beautify the button

* fix: the error position of button

* feat: only select when click circle instead of whole card

* feat: beautify file dashboard

* chore: factor the filecard code

* feat: using dropdown component intead of component

* feat: add i18n for delete selected resource button

* feat: delete the unused style of title

* chore: refactor file cover

* feat: support more type file cover

* feat: use memo to reduce unused computing in filecover

* feat: when no file be selected, click the delete will error

* feat: store the select resource id instead of source to save memory

* chore: delete unused code

* feat: refactor the file card

* chore: delete unused style file

* chore: change file to resource

* chore: delete unused import

* chore: fix the typo

* fix: the error of handle check click

* fix: the error of handle of uncheck

* chore: change the name of selectList to selectedList

* chore: change the name of selectList to selectedList

* chore: change the name of selectList to selectedList

* chore: delete unused import

* feat: support Responsive Design

* feat: min display two card in a line

* feat: adjust the num of a line in responsive design

* feat: adjust the num of a line to 6 when using md

* feat: add the color of hover source card when dark

* chore: refactor resource cover css to reduce code

* chore: delete unnessnary change

* chore: change the type of callback function

* chore: delete unused css code

* feat: add zh-hant i18n

* feat: change the position of buttons

* feat: add title for the icon button

* feat: add opacity for icon

* feat: refactor searchbar

* feat:move Debounce to search

* feat: new resource search bar

* feat: reduce the size of cover

* support file search

* Update web/src/pages/ResourcesDashboard.tsx

Co-authored-by: boojack <stevenlgtm@gmail.com>

* Update web/src/components/ResourceCard.tsx

Co-authored-by: boojack <stevenlgtm@gmail.com>

* chore: reduce css code

* feat: support lowcase and uppercase search

* chore: reserve the searchbar

* feat: refator resource Search bar

* chore: change the param name

* feat: resource bar support dark mode

* feat: beautify the UI of dashboard

* chore: extract positionClassName from actionsClassName

* feat: reduce the length of search bar

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
This commit is contained in:
CorrectRoadH 2023-03-15 20:06:30 +08:00 committed by GitHub
parent e129b122a4
commit 0a66c5c269
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 444 additions and 277 deletions

View File

@ -8,5 +8,6 @@
],
"files.associations": {
"*.less": "postcss"
}
},
"i18n-ally.keystyle": "nested"
}

View File

@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
import { useLayoutStore, useUserStore } from "../store/module";
import { resolution } from "../utils/layout";
import Icon from "./Icon";
import showResourcesDialog from "./ResourcesDialog";
import showSettingDialog from "./SettingDialog";
import showAskAIDialog from "./AskAIDialog";
import showArchivedMemoDialog from "./ArchivedMemoDialog";
@ -76,6 +75,18 @@ const Header = () => {
<Icon.Calendar className="mr-4 w-6 h-auto opacity-80" /> {t("common.daily-review")}
</>
</NavLink>
<NavLink
to="/resources"
className={({ isActive }) =>
`${
isActive && "bg-white dark:bg-zinc-700 shadow"
} px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
}
>
<>
<Icon.Paperclip className="mr-4 w-6 h-auto opacity-80" /> {t("common.resources")}
</>
</NavLink>
</>
)}
<NavLink
@ -98,12 +109,6 @@ const Header = () => {
>
<Icon.Bot className="mr-4 w-6 h-auto opacity-80" /> Ask AI
</button>
<button
className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
onClick={() => showResourcesDialog()}
>
<Icon.Paperclip className="mr-4 w-6 h-auto opacity-80" /> {t("common.resources")}
</button>
<button
className="px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
onClick={() => showArchivedMemoDialog()}

View File

@ -0,0 +1,131 @@
import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-hot-toast";
import { useResourceStore } from "../store/module";
import Icon from "./Icon";
import copy from "copy-to-clipboard";
import { getResourceUrl } from "../utils/resource";
import showPreviewImageDialog from "./PreviewImageDialog";
import Dropdown from "./base/Dropdown";
import ResourceCover from "./ResourceCover";
import { showCommonDialog } from "./Dialog/CommonDialog";
import showChangeResourceFilenameDialog from "./ChangeResourceFilenameDialog";
import "../less/resource-card.less";
interface ResourceProps {
resource: Resource;
handlecheckClick: () => void;
handleUncheckClick: () => void;
}
const ResourceCard = ({ resource, handlecheckClick, handleUncheckClick }: ResourceProps) => {
const [isSelected, setIsSelected] = useState<boolean>(false);
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
const { t } = useTranslation();
const handleRenameBtnClick = (resource: Resource) => {
showChangeResourceFilenameDialog(resource.id, resource.filename);
};
const handleDeleteResourceBtnClick = (resource: Resource) => {
let warningText = t("resources.warning-text");
if (resource.linkedMemoAmount > 0) {
warningText = warningText + `\n${t("resources.linked-amount")}: ${resource.linkedMemoAmount}`;
}
showCommonDialog({
title: t("resources.delete-resource"),
content: warningText,
style: "warning",
dialogName: "delete-resource-dialog",
onConfirm: async () => {
await resourceStore.deleteResourceById(resource.id);
},
});
};
const handlePreviewBtnClick = (resource: Resource) => {
const resourceUrl = getResourceUrl(resource);
if (resource.type.startsWith("image")) {
showPreviewImageDialog(
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
resources.findIndex((r) => r.id === resource.id)
);
} else {
window.open(resourceUrl);
}
};
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
const url = getResourceUrl(resource);
copy(url);
toast.success(t("message.succeed-copy-resource-link"));
};
const handleSelectBtnClick = () => {
if (isSelected) {
handleUncheckClick();
} else {
handlecheckClick();
}
setIsSelected(!isSelected);
};
return (
<div className="resource-card">
<div className="btns-container">
<div onClick={() => handleSelectBtnClick()}>
{isSelected ? (
<Icon.CheckCircle2 className="m-2 text-gray-500 hover:text-black absolute top-1" />
) : (
<Icon.Circle className="resource-checkbox" />
)}
</div>
<Dropdown
className="more-action-btn"
actionsClassName="!w-28"
actions={
<>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handlePreviewBtnClick(resource)}
>
{t("resources.preview")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleCopyResourceLinkBtnClick(resource)}
>
{t("resources.copy-link")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleRenameBtnClick(resource)}
>
{t("resources.rename")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleDeleteResourceBtnClick(resource)}
>
{t("common.delete")}
</button>
</>
}
/>
</div>
<div className="p-5">
<ResourceCover resource={resource} />
</div>
<div>
<div className="text-base overflow-x-auto whitespace-nowrap text-center">{resource.filename}</div>
<div className="text-sm text-gray-400 text-center">{dayjs(resource.createdTs).locale("en").format("YYYY/MM/DD HH:mm:ss")}</div>
</div>
</div>
);
};
export default ResourceCard;

View File

@ -0,0 +1,32 @@
import React from "react";
import Icon from "./Icon";
import "../less/resource-cover.less";
interface ResourceCoverProps {
resource: Resource;
}
const ResourceCover = ({ resource }: ResourceCoverProps) => {
switch (resource.type) {
case "image/*":
return <Icon.FileImage className="resource-cover" />;
case "video/*":
return <Icon.FileVideo2 className="resource-cover" />;
case "audio/*":
return <Icon.FileAudio className="resource-cover" />;
case "text/*":
return <Icon.FileText className="resource-cover" />;
case "application/epub+zip":
return <Icon.Book className="resource-cover" />;
case "application/pdf":
return <Icon.Book className="resource-cover" />;
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return <Icon.FileEdit className="resource-cover" />;
case "application/msword":
return <Icon.FileEdit className="resource-cover" />;
default:
return <Icon.File className="resource-cover" />;
}
};
export default React.memo(ResourceCover);

View File

@ -0,0 +1,42 @@
import { useState, useRef } from "react";
import Icon from "./Icon";
import useDebounce from "../hooks/useDebounce";
interface ResourceSearchBarProps {
setQuery: (queryText: string) => void;
}
const ResourceSearchBar = ({ setQuery }: ResourceSearchBarProps) => {
const [queryText, setQueryText] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const handleTextQueryInput = (event: React.FormEvent<HTMLInputElement>) => {
const text = event.currentTarget.value;
setQueryText(text);
};
useDebounce(
() => {
setQuery(queryText);
},
200,
[queryText]
);
return (
<div className="w-4/12">
<div className="w-full h-9 flex flex-row justify-start items-center py-2 px-3 rounded-md bg-gray-200 dark:bg-zinc-800">
<Icon.Search className="w-4 h-auto opacity-30 dark:text-gray-200" />
<input
className="flex ml-2 w-24 grow text-sm outline-none bg-transparent dark:text-gray-200"
type="text"
placeholder="Search resource "
ref={inputRef}
value={queryText}
onChange={handleTextQueryInput}
/>
</div>
</div>
);
};
export default ResourceSearchBar;

View File

@ -1,191 +0,0 @@
import { Button } from "@mui/joy";
import copy from "copy-to-clipboard";
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import useLoading from "../hooks/useLoading";
import { useResourceStore } from "../store/module";
import { getResourceUrl } from "../utils/resource";
import Icon from "./Icon";
import Dropdown from "./base/Dropdown";
import { generateDialog } from "./Dialog";
import { showCommonDialog } from "./Dialog/CommonDialog";
import showPreviewImageDialog from "./PreviewImageDialog";
import showCreateResourceDialog from "./CreateResourceDialog";
import showChangeResourceFilenameDialog from "./ChangeResourceFilenameDialog";
import "../less/resources-dialog.less";
type Props = DialogProps;
const ResourcesDialog: React.FC<Props> = (props: Props) => {
const { destroy } = props;
const { t } = useTranslation();
const loadingState = useLoading();
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
useEffect(() => {
resourceStore
.fetchResourceList()
.catch((error) => {
console.error(error);
toast.error(error.response.data.message);
})
.finally(() => {
loadingState.setFinish();
});
}, []);
const handlePreviewBtnClick = (resource: Resource) => {
const resourceUrl = getResourceUrl(resource);
if (resource.type.startsWith("image")) {
showPreviewImageDialog(
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
resources.findIndex((r) => r.id === resource.id)
);
} else {
window.open(resourceUrl);
}
};
const handleRenameBtnClick = (resource: Resource) => {
showChangeResourceFilenameDialog(resource.id, resource.filename);
};
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
const url = getResourceUrl(resource);
copy(url);
toast.success(t("message.succeed-copy-resource-link"));
};
const handleDeleteUnusedResourcesBtnClick = () => {
let warningText = t("resources.warning-text-unused");
const unusedResources = resources.filter((resource) => {
if (resource.linkedMemoAmount === 0) {
warningText = warningText + `\n- ${resource.filename}`;
return true;
}
return false;
});
if (unusedResources.length === 0) {
toast.success(t("resources.no-unused-resources"));
return;
}
showCommonDialog({
title: t("resources.delete-resource"),
content: warningText,
style: "warning",
dialogName: "delete-unused-resources",
onConfirm: async () => {
for (const resource of unusedResources) {
await resourceStore.deleteResourceById(resource.id);
}
},
});
};
const handleDeleteResourceBtnClick = (resource: Resource) => {
let warningText = t("resources.warning-text");
if (resource.linkedMemoAmount > 0) {
warningText = warningText + `\n${t("resources.linked-amount")}: ${resource.linkedMemoAmount}`;
}
showCommonDialog({
title: t("resources.delete-resource"),
content: warningText,
style: "warning",
dialogName: "delete-resource-dialog",
onConfirm: async () => {
await resourceStore.deleteResourceById(resource.id);
},
});
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">{t("common.resources")}</p>
<button className="btn close-btn" onClick={destroy}>
<Icon.X className="icon-img" />
</button>
</div>
<div className="dialog-content-container">
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row justify-start items-center space-x-2">
<Button onClick={() => showCreateResourceDialog({})} startDecorator={<Icon.Plus className="w-5 h-auto" />}>
{t("common.create")}
</Button>
</div>
<div className="flex flex-row justify-end items-center">
<Button color="danger" onClick={handleDeleteUnusedResourcesBtnClick} startDecorator={<Icon.Trash2 className="w-4 h-auto" />}>
<span>{t("resources.clear")}</span>
</Button>
</div>
</div>
{loadingState.isLoading ? (
<div className="loading-text-container">
<p className="tip-text">{t("resources.fetching-data")}</p>
</div>
) : (
<div className="resource-table-container">
<div className="fields-container">
<span className="field-text id-text">ID</span>
<span className="field-text name-text">{t("resources.name")}</span>
<span></span>
</div>
{resources.length === 0 ? (
<p className="tip-text">{t("resources.no-resources")}</p>
) : (
resources.map((resource) => (
<div key={resource.id} className="resource-container">
<span className="field-text id-text">{resource.id}</span>
<span className="field-text name-text" onClick={() => handleRenameBtnClick(resource)}>
{resource.filename}
</span>
<div className="buttons-container">
<Dropdown
actionsClassName="!w-28"
actions={
<>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handlePreviewBtnClick(resource)}
>
{t("resources.preview")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleCopyResourceLinkBtnClick(resource)}
>
{t("resources.copy-link")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleDeleteResourceBtnClick(resource)}
>
{t("common.delete")}
</button>
</>
}
/>
</div>
</div>
))
)}
</div>
)}
</div>
</>
);
};
export default function showResourcesDialog() {
generateDialog(
{
className: "resources-dialog",
dialogName: "resources-dialog",
},
ResourcesDialog,
{}
);
}

View File

@ -0,0 +1,25 @@
.resource-card{
@apply flex flex-col relative justify-start hover:bg-slate-200 dark:hover:bg-slate-600 w-full m-4 rounded-md;
&:hover {
.resource-checkbox {
@apply flex m-2 text-gray-500 absolute top-1 hover:text-black;
}
.more-action-btn{
@apply flex absolute right-0 top-1;
}
}
.resource-checkbox{
@apply hidden hover:flex ;
}
.more-action-btn{
@apply hidden m-2;
&:hover {
& + .more-action-btns-wrapper {
display: flex;
}
}
}
}

View File

@ -0,0 +1,3 @@
.resource-cover{
@apply w-full h-full ml-auto mr-auto mt-5 opacity-60;
}

View File

@ -1,75 +0,0 @@
.resources-dialog {
@apply px-4;
> .dialog-container {
@apply w-112 max-w-full mb-8;
> .dialog-content-container {
@apply flex flex-col justify-start items-start w-full;
> .action-buttons-container {
@apply w-full flex flex-row justify-between items-center mb-2;
> .buttons-wrapper {
@apply flex flex-row justify-start items-center;
> .upload-resource-btn {
@apply text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-blue-600 text-blue-600 bg-blue-50 hover:opacity-80;
> .icon-img {
@apply w-4 h-auto mr-1;
}
}
> .delete-unused-resource-btn {
@apply text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-red-600 text-red-600 bg-red-100 hover:opacity-80;
> .icon-img {
@apply w-4 h-auto mr-1;
}
}
}
}
> .loading-text-container {
@apply flex flex-col justify-center items-center w-full h-32;
}
> .resource-table-container {
@apply flex flex-col justify-start items-start w-full;
> .fields-container {
@apply px-2 py-2 w-full grid grid-cols-7 border-b dark:border-b-zinc-600;
> .field-text {
@apply font-mono text-gray-400;
}
}
> .tip-text {
@apply w-full text-center text-base my-6 mt-8;
}
> .resource-container {
@apply px-2 py-2 w-full grid grid-cols-7;
> .buttons-container {
@apply w-full flex flex-row justify-end items-center;
}
}
.field-text {
@apply w-full truncate text-base pr-2 last:pr-0;
&.id-text {
@apply col-span-1;
}
&.name-text {
@apply col-span-5;
}
}
}
}
}
}

View File

@ -77,7 +77,9 @@
"clear": "Clear",
"warning-text-unused": "Are you sure to delete these unused resources? THIS ACTION IS IRREVERSIBLE❗",
"no-unused-resources": "No unused resources",
"name": "Name"
"name": "Name",
"delete-selected-resources": "Delete Selected Resources",
"no-files-selected": "No files selected❗"
},
"archived": {
"archived-memos": "Archived Memos",

View File

@ -77,7 +77,9 @@
"clear": "清除",
"warning-text-unused": "確定刪除這些無用資源嗎?此操作不可逆❗",
"no-unused-resources": "無可刪除的資源",
"name": "資源名稱"
"name": "資源名稱",
"delete-selected-resources": "刪除選中資源",
"no-files-selected": "沒有文件被選中❗"
},
"archived": {
"archived-memos": "已封存的 Memo",

View File

@ -77,7 +77,9 @@
"clear": "清除",
"warning-text-unused": "确定删除这些无用资源么?此操作不可逆❗",
"no-unused-resources": "无可删除的资源",
"name": "资源名称"
"name": "资源名称",
"delete-selected-resources": "删除选中资源",
"no-files-selected": "没有文件被选中❗"
},
"archived": {
"archived-memos": "已归档的 Memo",

View File

@ -0,0 +1,168 @@
import { Button } from "@mui/joy";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import useLoading from "../hooks/useLoading";
import { useResourceStore } from "../store/module";
import Icon from "../components/Icon";
import ResourceCard from "../components/ResourceCard";
import ResourceSearchBar from "../components/ResourceSearchBar";
import { showCommonDialog } from "../components/Dialog/CommonDialog";
import showCreateResourceDialog from "../components/CreateResourceDialog";
import MobileHeader from "../components/MobileHeader";
import Dropdown from "../components/base/Dropdown";
const ResourcesDashboard = () => {
const { t } = useTranslation();
const loadingState = useLoading();
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
const [selectedList, setSelectedList] = useState<Array<ResourceId>>([]);
const [isVisiable, setIsVisiable] = useState<boolean>(false);
const [queryText, setQueryText] = useState<string>("");
useEffect(() => {
resourceStore
.fetchResourceList()
.catch((error) => {
console.error(error);
toast.error(error.response.data.message);
})
.finally(() => {
loadingState.setFinish();
});
}, []);
useEffect(() => {
if (selectedList.length === 0) {
setIsVisiable(false);
} else {
setIsVisiable(true);
}
}, [selectedList]);
const handleCheckBtnClick = (resourceId: ResourceId) => {
setSelectedList([...selectedList, resourceId]);
};
const handleUncheckBtnClick = (resourceId: ResourceId) => {
setSelectedList(selectedList.filter((resId) => resId !== resourceId));
};
const handleDeleteUnusedResourcesBtnClick = () => {
let warningText = t("resources.warning-text-unused");
const unusedResources = resources.filter((resource) => {
if (resource.linkedMemoAmount === 0) {
warningText = warningText + `\n- ${resource.filename}`;
return true;
}
return false;
});
if (unusedResources.length === 0) {
toast.success(t("resources.no-unused-resources"));
return;
}
showCommonDialog({
title: t("resources.delete-resource"),
content: warningText,
style: "warning",
dialogName: "delete-unused-resources",
onConfirm: async () => {
for (const resource of unusedResources) {
await resourceStore.deleteResourceById(resource.id);
}
},
});
};
const handleDeleteSelectedBtnClick = () => {
if (selectedList.length == 0) {
toast.error(t("resources.no-files-selected"));
} else {
const warningText = t("resources.warning-text");
showCommonDialog({
title: t("resources.delete-resource"),
content: warningText,
style: "warning",
dialogName: "delete-resource-dialog",
onConfirm: async () => {
selectedList.map(async (resourceId: ResourceId) => {
await resourceStore.deleteResourceById(resourceId);
});
},
});
}
};
return (
<section className="w-full min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<MobileHeader showSearch={false} />
<div className="w-full flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300">
<div className="relative w-full flex flex-row justify-between items-center">
<p className="px-2 py-1 flex flex-row justify-start items-center select-none rounded">
<Icon.Paperclip className="w-5 h-auto mr-1" /> {t("common.resources")}
</p>
<ResourceSearchBar setQuery={setQueryText} />
</div>
<div className=" flex flex-col justify-start items-start w-full">
<div className="w-full flex flex-row justify-end items-center">
<div className="flex flex-row justify-start items-center space-x-2"></div>
<div className="m-2">
{isVisiable && (
<Button onClick={() => handleDeleteSelectedBtnClick()} color="danger" title={t("resources.delete-selected-resources")}>
<Icon.Trash2 className="w-4 h-auto" />
</Button>
)}
</div>
<div className="m-2">
<Button onClick={() => showCreateResourceDialog({})} title={t("common.create")}>
<Icon.Plus className="w-4 h-auto" />
</Button>
</div>
<Dropdown
className="drop-shadow-none m-2 "
actionsClassName="!w-28 rounded-lg drop-shadow-md dark:bg-zinc-800"
positionClassName="mt-2 top-full right-0"
actions={
<>
<button
className="w-full content-center text-sm whitespace-nowrap leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={handleDeleteUnusedResourcesBtnClick}
>
{t("resources.clear")}
</button>
</>
}
/>
</div>
{loadingState.isLoading ? (
<div className="flex flex-col justify-center items-center w-full h-32">
<p className="w-full text-center text-base my-6 mt-8">{t("resources.fetching-data")}</p>
</div>
) : (
<div className="w-full h-full grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4">
{resources.length === 0 ? (
<p className="w-full text-center text-base my-6 mt-8">{t("resources.no-resources")}</p>
) : (
resources
.filter((res: Resource) => (queryText === "" ? true : res.filename.toLowerCase().includes(queryText.toLowerCase())))
.map((resource) => (
<ResourceCard
key={resource.id}
resource={resource}
handlecheckClick={() => handleCheckBtnClick(resource.id)}
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
></ResourceCard>
))
)}
</div>
)}
</div>
</div>
</section>
);
};
export default ResourcesDashboard;

View File

@ -4,6 +4,7 @@ import { isNullorUndefined } from "../helpers/utils";
import store from "../store";
import { initialGlobalState, initialUserState } from "../store/module";
import DailyReview from "../pages/DailyReview";
import ResourcesDashboard from "../pages/ResourcesDashboard";
const Root = lazy(() => import("../layouts/Root"));
const Auth = lazy(() => import("../pages/Auth"));
@ -118,6 +119,25 @@ const router = createBrowserRouter([
// do nth
}
const { host } = store.getState().user;
if (isNullorUndefined(host)) {
return redirect("/auth");
}
return null;
},
},
{
path: "resources",
element: <ResourcesDashboard />,
loader: async () => {
await initialGlobalStateLoader();
try {
await initialUserState();
} catch (error) {
// do nth
}
const { host } = store.getState().user;
if (isNullorUndefined(host)) {
return redirect("/auth");