add file download for tracking page

This commit is contained in:
smanylov
2026-05-08 13:39:37 +07:00
parent 07e1237f05
commit b8a76bd6a9
8 changed files with 109 additions and 41 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.109.0", "version": "0.110.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
+8 -7
View File
@@ -1,9 +1,9 @@
import Providers from '@/app/providers/getQueryServer'
import type { Metadata } from "next"; 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 ClientProviders from '@/app/providers/ClientProviders';
import { Suspense } from 'react';
import "../styles/globals.css"; import "../styles/globals.css";
import "../styles/global-styles.scss"; import "../styles/global-styles.scss";
@@ -30,14 +30,15 @@ export default async function RootLayout({
} }
return ( return (
<html lang={locale}> <html>
<body> <body>
<NextIntlClientProvider> <Suspense fallback={<div>Loading...</div>}>
<Providers> <NextIntlClientProvider locale={locale}>
<ClientProviders>
{children} {children}
<ToastProvider /> </ClientProviders>
</Providers>
</NextIntlClientProvider> </NextIntlClientProvider>
</Suspense>
</body> </body>
</html> </html>
); );
+5
View File
@@ -1,9 +1,14 @@
'use client'
import { PdfProvider } from '@/app/contexts/PdfContext'; import { PdfProvider } from '@/app/contexts/PdfContext';
import { WatchDocProviders } from '@/app/providers/ClientProviders';
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<WatchDocProviders>
<PdfProvider> <PdfProvider>
{children} {children}
</PdfProvider> </PdfProvider>
</WatchDocProviders>
); );
} }
+1 -1
View File
@@ -119,7 +119,7 @@ export default async function Page({ searchParams }: PageProps) {
<div className="watch-doc-actions"> <div className="watch-doc-actions">
{response?.permissions?.DOWNLOAD && ( {response?.permissions?.DOWNLOAD && (
<DownloadButton /> <DownloadButton fileId={docid} fileName={response?.fileName}/>
)} )}
<CopyLinkButton /> <CopyLinkButton />
</div> </div>
+23
View File
@@ -239,3 +239,26 @@ export async function fetchTrackingStats() {
return null; return null;
} }
} }
export async function downloadTrackingFile(fileId: string, fileName?: string) {
try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/public-download/${fileId}`, {
method: 'GET'
});
if (!response.ok) {
throw 'failed-to-download-file';
}
const arrayBuffer = await response.arrayBuffer()
const base64 = Buffer.from(arrayBuffer).toString('base64')
return {
data: base64,
contentType: response.headers.get('content-type') || 'application/octet-stream',
fileName: fileName ? fileName : 'fileName'
}
} catch (error) {
return null;
}
}
@@ -1,12 +1,49 @@
'use client'; 'use client';
import { usePdf } from '@/app/contexts/PdfContext'; import { downloadTrackingFile } from '@/app/actions/trackingActions';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
interface DownloadButtonProps {
fileId: string;
fileName?: string;
}
export function DownloadButton() { export function DownloadButton({ fileId, fileName }: DownloadButtonProps) {
const { downloadPdf } = usePdf(); const t = useTranslations('Global');
const handleDownload = async () => {
try {
const result = await downloadTrackingFile(fileId, fileName);
if (!result) {
throw 'failed-to-download-file'
}
const byteCharacters = atob(result.data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: result.contentType });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = result.fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - `);
} catch (error) {
toast.error(t('failed-to-download-file'));
}
};
return ( return (
<button className="btn btn-out" onClick={downloadPdf}> <button className="btn btn-out" onClick={handleDownload}>
Скачать документ Скачать документ
</button> </button>
); );
+25
View File
@@ -0,0 +1,25 @@
'use client';
import Providers from './getQueryServer';
import { ToastProvider } from './ToastProvider';
export default function ClientProviders({
children
}: {
children: React.ReactNode;
}) {
return (
<Providers>
{children}
<ToastProvider />
</Providers>
);
}
export function WatchDocProviders({ children }: { children: React.ReactNode }) {
return (
<Providers>
{children}
</Providers>
);
}
-23
View File
@@ -1,34 +1,11 @@
'use client' 'use client'
import { import {
isServer,
QueryClient,
QueryClientProvider, QueryClientProvider,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import { getQueryClient } from '@/app/providers/getQueryClient'; import { getQueryClient } from '@/app/providers/getQueryClient';
/* export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
return makeQueryClient()
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
} */
export default function Providers({ children }: { children: React.ReactNode }) { export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient(); const queryClient = getQueryClient();