update files-state, add toast, add zustand

This commit is contained in:
smanylov
2025-12-29 14:00:02 +07:00
parent fffd38a812
commit 198a01d026
9 changed files with 134 additions and 52 deletions
+11
View File
@@ -21,6 +21,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"recharts": "^3.5.1",
"sonner": "^2.0.7",
"tailwindcss": "^4.1.17",
"zod": "^4.1.13",
"zustand": "^5.0.9"
@@ -7119,6 +7120,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "no-copy-frontend",
"version": "0.9.0",
"version": "0.10.0",
"private": true,
"scripts": {
"dev": "next dev -p 2999",
@@ -22,6 +22,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"recharts": "^3.5.1",
"sonner": "^2.0.7",
"tailwindcss": "^4.1.17",
"zod": "^4.1.13",
"zustand": "^5.0.9"
+2
View File
@@ -3,6 +3,7 @@ import type { Metadata } from "next";
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import { ToastProvider } from '@/app/providers/ToastProvider';
import "../styles/globals.css";
import "../styles/global-styles.scss";
@@ -33,6 +34,7 @@ export default async function RootLayout({
<NextIntlClientProvider>
<Providers>
{children}
<ToastProvider />
</Providers>
</NextIntlClientProvider>
</body>
+69 -47
View File
@@ -14,23 +14,38 @@ import {
import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileOpen, IconShieldExclamation, IconDelete } from '@/app/ui/icons/icons';
import { useTranslations } from 'next-intl';
import DropDownList from '@/app/components/dropDownList';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getUserFilesData, removeUserFile } from '@/app/actions/fileEntity';
import ModalWindow from '@/app/components/modalWindow';
import { toast } from 'sonner';
// Типы данных
type FileType = 'image' | 'video' | 'audio';
type FileItem = {
id: string;
fileName: string;
fileType: FileType;
fileType: string;
violations?: number | undefined;
checks?: number | undefined;
lastCheck?: number; // timestamp в миллисекундах
lastCheck?: number;
_original?: ApiFile;
};
type ApiFile = {
id: string;
originalFileName: string;
mimeType: string;
violations: number;
checks: number;
updatedAt: string;
};
type ApiResponse = {
files?: ApiFile[];
};
// Иконки для типов файлов
const FileTypeIcon = ({ type }: { type: FileType }) => {
const FileTypeIcon = ({ type }: { type: string }) => {
switch (type) {
case 'image':
return <span className="text-[#f08c00]">
@@ -69,23 +84,18 @@ const formatDateTime = (timestamp: number) => {
export default function TanstakFilesTable() {
const {
data: userFilesData,
data: tableData,
isLoading,
isError,
error,
refetch
} = useQuery({
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData']>({
queryKey: ['userFilesData'],
queryFn: () => {
return getUserFilesData();
}
});
queryFn: getUserFilesData,
const [tableData, setTableData] = useState<FileItem[]>([]);
select: (data: ApiResponse): FileItem[] => {
if (!data?.files) return [];
useEffect(() => {
if (userFilesData?.files) {
const formattedData = userFilesData.files.map((item: any) => {
return data.files.map((item: ApiFile) => {
const [datePart, timePart] = item.updatedAt.split(' ');
const [day, month, year] = datePart.split('-').map(Number);
const [hours, minutes, seconds] = timePart.split(':').map(Number);
@@ -97,13 +107,14 @@ export default function TanstakFilesTable() {
fileType: item.mimeType.toLocaleLowerCase(),
violations: item.violations,
checks: item.checks,
lastCheck: newDate
lastCheck: newDate,
_original: item
};
});
},
});
setTableData(formattedData);
}
}, [userFilesData]);
const queryClient = useQueryClient();
// Состояния
const [sorting, setSorting] = useState<SortingState>([]);
@@ -343,7 +354,7 @@ export default function TanstakFilesTable() {
<IconShieldExclamation />
</button>
<button
onClick={() => handleRemove(row.original)}
onClick={() => handleOpenWindowForRemove(row.original)}
className="px-3 py-1 bg-red-700 text-white rounded hover:bg-red-800 text-sm"
>
<IconDelete />
@@ -370,7 +381,7 @@ export default function TanstakFilesTable() {
console.log(`Щиток: ${file.fileName}`);
};
const handleRemove = async (file: FileItem) => {
const handleOpenWindowForRemove = async (file: FileItem) => {
setOpenWindowChildren(() => {
return (
@@ -381,18 +392,12 @@ export default function TanstakFilesTable() {
<div className="mt-2 mb-8 text-center">{file.fileName}</div>
<div className="flex justify-center gap-4">
<button className="btn-primary btn-modal"
onClick={async () => {
const response = await removeUserFile(file.id);
if (response === file.id) {
setTableData(
prev => prev.filter(item => item.id !== file.id)
)
setOpenWindow(false);
setOpenWindowChildren(null);
}
onClick={() => {
deleteMutation.mutate(file.id)
}}
disabled={deleteMutation.isPending}
>
{t('yes')}
{deleteMutation.isPending ? '...' : t('yes')}
</button>
<button className="btn-primary btn-modal"
onClick={() => {
@@ -409,9 +414,41 @@ export default function TanstakFilesTable() {
setOpenWindow(true);
};
const deleteMutation = useMutation({
mutationFn: removeUserFile,
onMutate: async (fileId: string) => {
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
if (!old?.files) return old;
return {
...old,
files: old.files.filter(file => file.id !== fileId)
};
});
},
onError: () => {
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
toast.error(t('error'));
},
onSuccess: (response, fileId) => {
if (response === fileId) {
setOpenWindow(false);
setOpenWindowChildren(null);
toast.success(t('file-has-been-deleted'));
}
},
});
// Фильтрация по типу файла и дате
const filteredData = useMemo(() => {
let result = tableData;
if (!result) {
return [];
}
// Фильтр по типу файла
if (typeFilter !== 'all') {
@@ -503,21 +540,6 @@ export default function TanstakFilesTable() {
<div className="p-6 mx-auto tanstak-table">
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow}></ModalWindow>
{/* Фильтры */}
<button
className="mb-2 btn-primary rounded disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => refetch()}
disabled={isLoading}
>
{isLoading ? (
<span className="flex items-center gap-2">
refreshing...
</span>
) : (
<span className="flex items-center gap-2">
refresh
</span>
)}
</button>
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 pb-4 pt-0 px-0">
<div className="flex flex-col md:flex-row gap-4 w-full md:w-auto">
<div className="flex flex-col w-50">
+4 -2
View File
@@ -5,6 +5,7 @@ import { IconShieldAdd } from '@/app/ui/icons/icons';
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload';
import { useTranslations } from 'next-intl';
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
import { useQueryClient } from '@tanstack/react-query';
interface SelectedFile {
file: File;
preview: string | undefined;
@@ -38,7 +39,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
const [uploadId, setUploadId] = useState<string | null>(null);
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
const [uploadProgress, setUploadProgress] = useState<number>(0);
const queryClient = useQueryClient();
const isCancelledRef = useRef(false);
const t = useTranslations('Global');
@@ -92,7 +93,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
default:
break;
}
};
const acceptString = useMemo(() => {
if (!allowedExtensions || !Array.isArray(allowedExtensions)) {
@@ -292,6 +293,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
const chunkStatus = await checkChunkStatus(response.upload_id);
if (chunkStatus.message_body.missing_chunks === 0) {
setIsFileUploaded(true);
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
} else {
throw new Error('Not all chunks were uploaded');
}
+17
View File
@@ -0,0 +1,17 @@
'use client';
import { Toaster } from 'sonner';
export function ToastProvider() {
return (
<Toaster
position="bottom-left"
expand={false}
richColors
closeButton={false}
duration={1000}
visibleToasts={3}
gap={12}
/>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface Store {
value: any
setValue: (value: any) => void
}
export const useStoreWithDevtools = create<Store>()(
devtools(
(set) => ({
value: 0,
setValue: (value) => set({ value }),
}),
{
name: 'DevStore', // имя для DevTools
}
)
)
/* пример */
/* const setText = useStoreWithDevtools(s => s.setValue); */
/* const text = useStoreWithDevtools(s => s.value); */
+3 -1
View File
@@ -148,7 +148,9 @@
"yes": "yes",
"no": "no",
"you-sure-you-want-to-delete": "Are you sure you want to delete?",
"reports-and-analytics": "Reports and analytics"
"reports-and-analytics": "Reports and analytics",
"file-has-been-deleted": "The file has been deleted",
"error": "Error"
},
"Login-register-form": {
"and": "and",
+3 -1
View File
@@ -148,7 +148,9 @@
"yes": "да",
"no": "нет",
"you-sure-you-want-to-delete": "Уверены, что хотите удалить?",
"reports-and-analytics": "Отчёты и аналитика"
"reports-and-analytics": "Отчёты и аналитика",
"file-has-been-deleted": "Файл удален",
"error": "Ошбка"
},
"Login-register-form": {
"and": "и",