update files-state, add toast, add zustand
This commit is contained in:
Generated
+11
@@ -21,6 +21,7 @@
|
|||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"recharts": "^3.5.1",
|
"recharts": "^3.5.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"zod": "^4.1.13",
|
"zod": "^4.1.13",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
@@ -7119,6 +7120,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"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",
|
"name": "no-copy-frontend",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 2999",
|
"dev": "next dev -p 2999",
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"recharts": "^3.5.1",
|
"recharts": "^3.5.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"zod": "^4.1.13",
|
"zod": "^4.1.13",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Metadata } from "next";
|
|||||||
import { NextIntlClientProvider, hasLocale } from 'next-intl';
|
import { NextIntlClientProvider, hasLocale } from 'next-intl';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { routing } from '@/i18n/routing';
|
import { routing } from '@/i18n/routing';
|
||||||
|
import { ToastProvider } from '@/app/providers/ToastProvider';
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
import "../styles/global-styles.scss";
|
import "../styles/global-styles.scss";
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export default async function RootLayout({
|
|||||||
<NextIntlClientProvider>
|
<NextIntlClientProvider>
|
||||||
<Providers>
|
<Providers>
|
||||||
{children}
|
{children}
|
||||||
|
<ToastProvider />
|
||||||
</Providers>
|
</Providers>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</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 { 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 { useTranslations } from 'next-intl';
|
||||||
import DropDownList from '@/app/components/dropDownList';
|
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 { getUserFilesData, removeUserFile } from '@/app/actions/fileEntity';
|
||||||
import ModalWindow from '@/app/components/modalWindow';
|
import ModalWindow from '@/app/components/modalWindow';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
// Типы данных
|
|
||||||
type FileType = 'image' | 'video' | 'audio';
|
type FileType = 'image' | 'video' | 'audio';
|
||||||
|
|
||||||
type FileItem = {
|
type FileItem = {
|
||||||
id: string;
|
id: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
fileType: FileType;
|
fileType: string;
|
||||||
violations?: number | undefined;
|
violations?: number | undefined;
|
||||||
checks?: 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) {
|
switch (type) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return <span className="text-[#f08c00]">
|
return <span className="text-[#f08c00]">
|
||||||
@@ -69,23 +84,18 @@ const formatDateTime = (timestamp: number) => {
|
|||||||
|
|
||||||
export default function TanstakFilesTable() {
|
export default function TanstakFilesTable() {
|
||||||
const {
|
const {
|
||||||
data: userFilesData,
|
data: tableData,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
refetch
|
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData']>({
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['userFilesData'],
|
queryKey: ['userFilesData'],
|
||||||
queryFn: () => {
|
queryFn: getUserFilesData,
|
||||||
return getUserFilesData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const [tableData, setTableData] = useState<FileItem[]>([]);
|
select: (data: ApiResponse): FileItem[] => {
|
||||||
|
if (!data?.files) return [];
|
||||||
|
|
||||||
useEffect(() => {
|
return data.files.map((item: ApiFile) => {
|
||||||
if (userFilesData?.files) {
|
|
||||||
const formattedData = userFilesData.files.map((item: any) => {
|
|
||||||
const [datePart, timePart] = item.updatedAt.split(' ');
|
const [datePart, timePart] = item.updatedAt.split(' ');
|
||||||
const [day, month, year] = datePart.split('-').map(Number);
|
const [day, month, year] = datePart.split('-').map(Number);
|
||||||
const [hours, minutes, seconds] = timePart.split(':').map(Number);
|
const [hours, minutes, seconds] = timePart.split(':').map(Number);
|
||||||
@@ -97,13 +107,14 @@ export default function TanstakFilesTable() {
|
|||||||
fileType: item.mimeType.toLocaleLowerCase(),
|
fileType: item.mimeType.toLocaleLowerCase(),
|
||||||
violations: item.violations,
|
violations: item.violations,
|
||||||
checks: item.checks,
|
checks: item.checks,
|
||||||
lastCheck: newDate
|
lastCheck: newDate,
|
||||||
|
_original: item
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
setTableData(formattedData);
|
const queryClient = useQueryClient();
|
||||||
}
|
|
||||||
}, [userFilesData]);
|
|
||||||
|
|
||||||
// Состояния
|
// Состояния
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -343,7 +354,7 @@ export default function TanstakFilesTable() {
|
|||||||
<IconShieldExclamation />
|
<IconShieldExclamation />
|
||||||
</button>
|
</button>
|
||||||
<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"
|
className="px-3 py-1 bg-red-700 text-white rounded hover:bg-red-800 text-sm"
|
||||||
>
|
>
|
||||||
<IconDelete />
|
<IconDelete />
|
||||||
@@ -370,7 +381,7 @@ export default function TanstakFilesTable() {
|
|||||||
console.log(`Щиток: ${file.fileName}`);
|
console.log(`Щиток: ${file.fileName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = async (file: FileItem) => {
|
const handleOpenWindowForRemove = async (file: FileItem) => {
|
||||||
|
|
||||||
setOpenWindowChildren(() => {
|
setOpenWindowChildren(() => {
|
||||||
return (
|
return (
|
||||||
@@ -381,18 +392,12 @@ export default function TanstakFilesTable() {
|
|||||||
<div className="mt-2 mb-8 text-center">{file.fileName}</div>
|
<div className="mt-2 mb-8 text-center">{file.fileName}</div>
|
||||||
<div className="flex justify-center gap-4">
|
<div className="flex justify-center gap-4">
|
||||||
<button className="btn-primary btn-modal"
|
<button className="btn-primary btn-modal"
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
const response = await removeUserFile(file.id);
|
deleteMutation.mutate(file.id)
|
||||||
if (response === file.id) {
|
|
||||||
setTableData(
|
|
||||||
prev => prev.filter(item => item.id !== file.id)
|
|
||||||
)
|
|
||||||
setOpenWindow(false);
|
|
||||||
setOpenWindowChildren(null);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
>
|
>
|
||||||
{t('yes')}
|
{deleteMutation.isPending ? '...' : t('yes')}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-primary btn-modal"
|
<button className="btn-primary btn-modal"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -409,9 +414,41 @@ export default function TanstakFilesTable() {
|
|||||||
setOpenWindow(true);
|
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(() => {
|
const filteredData = useMemo(() => {
|
||||||
let result = tableData;
|
let result = tableData;
|
||||||
|
if (!result) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// Фильтр по типу файла
|
// Фильтр по типу файла
|
||||||
if (typeFilter !== 'all') {
|
if (typeFilter !== 'all') {
|
||||||
@@ -503,21 +540,6 @@ export default function TanstakFilesTable() {
|
|||||||
<div className="p-6 mx-auto tanstak-table">
|
<div className="p-6 mx-auto tanstak-table">
|
||||||
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow}></ModalWindow>
|
<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 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 md:flex-row gap-4 w-full md:w-auto">
|
||||||
<div className="flex flex-col w-50">
|
<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 { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
interface SelectedFile {
|
interface SelectedFile {
|
||||||
file: File;
|
file: File;
|
||||||
preview: string | undefined;
|
preview: string | undefined;
|
||||||
@@ -38,7 +39,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
const [uploadId, setUploadId] = useState<string | null>(null);
|
const [uploadId, setUploadId] = useState<string | null>(null);
|
||||||
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
||||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const isCancelledRef = useRef(false);
|
const isCancelledRef = useRef(false);
|
||||||
|
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
@@ -92,7 +93,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
};
|
||||||
|
|
||||||
const acceptString = useMemo(() => {
|
const acceptString = useMemo(() => {
|
||||||
if (!allowedExtensions || !Array.isArray(allowedExtensions)) {
|
if (!allowedExtensions || !Array.isArray(allowedExtensions)) {
|
||||||
@@ -292,6 +293,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
const chunkStatus = await checkChunkStatus(response.upload_id);
|
const chunkStatus = await checkChunkStatus(response.upload_id);
|
||||||
if (chunkStatus.message_body.missing_chunks === 0) {
|
if (chunkStatus.message_body.missing_chunks === 0) {
|
||||||
setIsFileUploaded(true);
|
setIsFileUploaded(true);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Not all chunks were uploaded');
|
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",
|
"yes": "yes",
|
||||||
"no": "no",
|
"no": "no",
|
||||||
"you-sure-you-want-to-delete": "Are you sure you want to delete?",
|
"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": {
|
"Login-register-form": {
|
||||||
"and": "and",
|
"and": "and",
|
||||||
|
|||||||
@@ -148,7 +148,9 @@
|
|||||||
"yes": "да",
|
"yes": "да",
|
||||||
"no": "нет",
|
"no": "нет",
|
||||||
"you-sure-you-want-to-delete": "Уверены, что хотите удалить?",
|
"you-sure-you-want-to-delete": "Уверены, что хотите удалить?",
|
||||||
"reports-and-analytics": "Отчёты и аналитика"
|
"reports-and-analytics": "Отчёты и аналитика",
|
||||||
|
"file-has-been-deleted": "Файл удален",
|
||||||
|
"error": "Ошбка"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "и",
|
"and": "и",
|
||||||
|
|||||||
Reference in New Issue
Block a user