update files-state, add toast, add zustand
This commit is contained in:
Generated
+11
@@ -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
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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); */
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "и",
|
||||
|
||||
Reference in New Issue
Block a user