chore(dependencies): update libraries and added File Uploader component (#292)

- Bumped dependencies: `lucide-react`, `react-hook-form`, `@supabase/supabase-js`, `@tanstack/react-query`, `@sentry/nextjs`, and more.
- Added `react-dropzone` to `@kit/ui` for file upload support.
- Adjusted `reset-password.html` to streamline style usage and HTML structure.
- Added new translation keys for file upload functionality.
- Cleaned up import order in `existing-account-hint.tsx`.
This commit is contained in:
Giancarlo Buomprisco
2025-06-26 12:40:54 +07:00
committed by GitHub
parent 180e0e0c5e
commit c1fda420e6
44 changed files with 2454 additions and 2321 deletions

View File

@@ -19,8 +19,8 @@ export async function loadTranslations() {
for (const locale of locales) {
translations[locale] = {};
const namespaces = readdirSync(join(localesPath, locale)).filter(
(file) => file.endsWith('.json'),
const namespaces = readdirSync(join(localesPath, locale)).filter((file) =>
file.endsWith('.json'),
);
for (const namespace of namespaces) {

View File

@@ -10,10 +10,10 @@
"dependencies": {
"@ai-sdk/openai": "^1.3.22",
"@hookform/resolvers": "^5.1.1",
"@tanstack/react-query": "5.80.7",
"@tanstack/react-query": "5.81.2",
"ai": "4.3.16",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"lucide-react": "^0.523.0",
"next": "15.3.4",
"nodemailer": "^7.0.3",
"react": "19.1.0",
"react-dom": "19.1.0",
@@ -26,13 +26,13 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@tailwindcss/postcss": "^4.1.10",
"@types/node": "^24.0.1",
"@types/node": "^24.0.4",
"@types/nodemailer": "6.4.17",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"pino-pretty": "^13.0.0",
"react-hook-form": "^7.58.0",
"react-hook-form": "^7.58.1",
"tailwindcss": "4.1.10",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.8.3",

View File

@@ -12,8 +12,8 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.53.0",
"@types/node": "^24.0.1",
"@playwright/test": "^1.53.1",
"@types/node": "^24.0.4",
"dotenv": "16.5.0",
"node-html-parser": "^7.0.1",
"totp-generator": "^1.0.0"

View File

@@ -52,7 +52,7 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
if (auth.error instanceof MultiFactorAuthError) {
const urlParams = new URLSearchParams({
next: `${pathsConfig.app.joinTeam}?invite_token=${token}&email=${searchParams.email ?? ''}`,
})
});
const verifyMfaUrl = `${pathsConfig.auth.verifyMfa}?${urlParams.toString()}`;

View File

@@ -56,17 +56,17 @@
"@marsidev/react-turnstile": "^1.1.0",
"@nosecone/next": "1.0.0-beta.8",
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.50.0",
"@tanstack/react-query": "5.80.7",
"@supabase/supabase-js": "2.50.2",
"@tanstack/react-query": "5.81.2",
"@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"lucide-react": "^0.523.0",
"next": "15.3.4",
"next-sitemap": "^4.2.3",
"next-themes": "0.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.58.0",
"react-hook-form": "^7.58.1",
"react-i18next": "^15.5.3",
"recharts": "2.15.3",
"tailwind-merge": "^3.3.1",
@@ -76,15 +76,15 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@next/bundle-analyzer": "15.3.3",
"@next/bundle-analyzer": "15.3.4",
"@tailwindcss/postcss": "^4.1.10",
"@types/node": "^24.0.1",
"@types/node": "^24.0.4",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cssnano": "^7.0.7",
"pino-pretty": "^13.0.0",
"prettier": "^3.5.3",
"prettier": "^3.6.1",
"supabase": "^2.26.9",
"tailwindcss": "4.1.10",
"tailwindcss-animate": "^1.0.7",

View File

@@ -92,5 +92,21 @@
"description": "This website uses cookies to ensure you get the best experience on our website.",
"reject": "Reject",
"accept": "Accept"
},
"dropzone": {
"success": "Successfully uploaded {{count}} file(s)",
"error": "Error uploading {{count}} file(s)",
"errorMessageUnknown": "An unknown error occurred.",
"errorMessageFileUnknown": "Unknown file",
"errorMessageFileSizeUnknown": "Unknown file size",
"errorMessageFileSizeTooSmall": "File size is too small",
"errorMessageFileSizeTooLarge": "File size is too large",
"uploading": "Uploading...",
"uploadFiles": "Upload {{count}} file(s)",
"maxFileSize": "Maximum file size: {{size}}",
"maxFiles": "You may upload only up to {{count}} files, please remove {{files}} files.",
"dragAndDrop": "Drag and drop or",
"select": "select files",
"toUpload": "to upload"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -48,7 +48,7 @@
"@manypkg/cli": "^0.24.0",
"@turbo/gen": "^2.5.4",
"cross-env": "^7.0.3",
"prettier": "^3.5.3",
"prettier": "^3.6.1",
"turbo": "2.5.4",
"typescript": "^5.8.3"
}

View File

@@ -16,7 +16,7 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^24.0.1"
"@types/node": "^24.0.4"
},
"typesVersions": {
"*": {

View File

@@ -26,13 +26,13 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@supabase/supabase-js": "2.50.0",
"@supabase/supabase-js": "2.50.2",
"@types/react": "19.1.8",
"date-fns": "^4.1.0",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"lucide-react": "^0.523.0",
"next": "15.3.4",
"react": "19.1.0",
"react-hook-form": "^7.58.0",
"react-hook-form": "^7.58.1",
"react-i18next": "^15.5.3",
"zod": "^3.25.67"
},

View File

@@ -25,7 +25,7 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/react": "19.1.8",
"next": "15.3.3",
"next": "15.3.4",
"react": "19.1.0",
"zod": "^3.25.67"
},

View File

@@ -16,7 +16,7 @@
},
"dependencies": {
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
"@stripe/stripe-js": "^7.4.0",
"stripe": "^18.2.1"
},
"devDependencies": {
@@ -29,7 +29,7 @@
"@kit/ui": "workspace:*",
"@types/react": "19.1.8",
"date-fns": "^4.1.0",
"next": "15.3.3",
"next": "15.3.4",
"react": "19.1.0",
"zod": "^3.25.67"
},

View File

@@ -20,7 +20,7 @@
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/wordpress": "workspace:*",
"@types/node": "^24.0.1"
"@types/node": "^24.0.4"
},
"typesVersions": {
"*": {

View File

@@ -26,7 +26,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/node": "^24.0.1",
"@types/node": "^24.0.4",
"@types/react": "19.1.8",
"react": "19.1.0",
"zod": "^3.25.67"

View File

@@ -20,7 +20,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@types/node": "^24.0.1",
"@types/node": "^24.0.4",
"@types/react": "19.1.8",
"wp-types": "^4.68.0"
},

View File

@@ -22,7 +22,7 @@
"@kit/supabase": "workspace:*",
"@kit/team-accounts": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/supabase-js": "2.50.0",
"@supabase/supabase-js": "2.50.2",
"zod": "^3.25.67"
},
"typesVersions": {

View File

@@ -13,7 +13,7 @@
".": "./src/index.ts"
},
"dependencies": {
"@react-email/components": "0.1.0"
"@react-email/components": "0.1.1"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",

View File

@@ -34,16 +34,16 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.50.0",
"@tanstack/react-query": "5.80.7",
"@supabase/supabase-js": "2.50.2",
"@tanstack/react-query": "5.81.2",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"lucide-react": "^0.523.0",
"next": "15.3.4",
"next-themes": "0.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.58.0",
"react-hook-form": "^7.58.1",
"react-i18next": "^15.5.3",
"zod": "^3.25.67"
},

View File

@@ -95,7 +95,8 @@ export function PersonalAccountDropdown({
className ?? '',
{
['active:bg-secondary/50 items-center gap-4 rounded-md' +
' hover:bg-secondary p-2 transition-colors border border-dashed']: showProfileName,
' hover:bg-secondary border border-dashed p-2 transition-colors']:
showProfileName,
},
)}
>

View File

@@ -20,15 +20,15 @@
"@kit/ui": "workspace:*",
"@makerkit/data-loader-supabase-core": "^0.0.10",
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
"@supabase/supabase-js": "2.50.0",
"@tanstack/react-query": "5.80.7",
"@supabase/supabase-js": "2.50.2",
"@tanstack/react-query": "5.81.2",
"@tanstack/react-table": "^8.21.3",
"@types/react": "19.1.8",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"lucide-react": "^0.523.0",
"next": "15.3.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.58.0",
"react-hook-form": "^7.58.1",
"zod": "^3.25.67"
},
"exports": {

View File

@@ -29,12 +29,12 @@
"@kit/ui": "workspace:*",
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.50.0",
"@tanstack/react-query": "5.80.7",
"@supabase/supabase-js": "2.50.2",
"@tanstack/react-query": "5.81.2",
"@types/react": "19.1.8",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"react-hook-form": "^7.58.0",
"lucide-react": "^0.523.0",
"next": "15.3.4",
"react-hook-form": "^7.58.1",
"react-i18next": "^15.5.3",
"sonner": "^2.0.5",
"zod": "^3.25.67"

View File

@@ -7,13 +7,13 @@ import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { UserCheck } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Alert, AlertDescription } from '@kit/ui/alert';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
import { useTranslation } from 'react-i18next';
interface ExistingAccountHintProps {
signInPath?: string;

View File

@@ -62,7 +62,6 @@ function LastAuthMethodHintImpl({ className }: LastAuthMethodHintProps) {
<span>
<Trans i18nKey="auth:lastUsedMethodPrefix" />{' '}
<If condition={isOAuth && Boolean(providerName)}>
<Trans
i18nKey="auth:methodOauthWithProvider"
@@ -72,7 +71,6 @@ function LastAuthMethodHintImpl({ className }: LastAuthMethodHintProps) {
}}
/>
</If>
<If condition={!isOAuth || !providerName}>
<span className="text-muted-foreground font-medium">
<Trans i18nKey={methodKey} />

View File

@@ -19,10 +19,10 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@supabase/supabase-js": "2.50.0",
"@tanstack/react-query": "5.80.7",
"@supabase/supabase-js": "2.50.2",
"@tanstack/react-query": "5.81.2",
"@types/react": "19.1.8",
"lucide-react": "^0.516.0",
"lucide-react": "^0.523.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^15.5.3"

View File

@@ -32,18 +32,18 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@supabase/supabase-js": "2.50.0",
"@tanstack/react-query": "5.80.7",
"@supabase/supabase-js": "2.50.2",
"@tanstack/react-query": "5.81.2",
"@tanstack/react-table": "^8.21.3",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"class-variance-authority": "^0.7.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"lucide-react": "^0.523.0",
"next": "15.3.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.58.0",
"react-hook-form": "^7.58.1",
"react-i18next": "^15.5.3",
"zod": "^3.25.67"
},

View File

@@ -20,8 +20,8 @@
"@kit/prettier-config": "workspace:*",
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@tanstack/react-query": "5.80.7",
"next": "15.3.3",
"@tanstack/react-query": "5.81.2",
"next": "15.3.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^15.5.3"

View File

@@ -20,7 +20,7 @@
"@kit/resend": "workspace:*",
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^24.0.1",
"@types/node": "^24.0.4",
"zod": "^3.25.67"
},
"typesVersions": {

View File

@@ -17,7 +17,7 @@
"@kit/mailers-shared": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@types/node": "^24.0.1",
"@types/node": "^24.0.4",
"zod": "^3.25.67"
},
"typesVersions": {

View File

@@ -16,8 +16,8 @@
"./config/server": "./src/sentry.client.server.ts"
},
"dependencies": {
"@sentry/nextjs": "^9.29.0",
"import-in-the-middle": "1.14.0"
"@sentry/nextjs": "^9.32.0",
"import-in-the-middle": "1.14.2"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",

View File

@@ -20,8 +20,8 @@
"@kit/prettier-config": "workspace:*",
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/supabase-js": "2.50.0",
"next": "15.3.3",
"@supabase/supabase-js": "2.50.2",
"next": "15.3.4",
"zod": "^3.25.67"
},
"typesVersions": {

View File

@@ -25,12 +25,12 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.50.0",
"@supabase/supabase-js": "2.50.2",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.58.0",
"react-hook-form": "^7.58.1",
"zod": "^3.25.67"
},
"typesVersions": {

View File

@@ -25,10 +25,10 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "2.50.0",
"@tanstack/react-query": "5.80.7",
"@supabase/supabase-js": "2.50.2",
"@tanstack/react-query": "5.81.2",
"@types/react": "19.1.8",
"next": "15.3.3",
"next": "15.3.4",
"react": "19.1.0",
"server-only": "^0.0.1",
"zod": "^3.25.67"

View File

@@ -33,7 +33,8 @@
"clsx": "^2.1.1",
"cmdk": "1.1.1",
"input-otp": "1.4.2",
"lucide-react": "^0.516.0",
"lucide-react": "^0.523.0",
"react-dropzone": "^14.3.8",
"react-top-loading-bar": "3.0.2",
"recharts": "2.15.3",
"tailwind-merge": "^3.3.1"
@@ -43,18 +44,19 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@radix-ui/react-icons": "^1.3.2",
"@tanstack/react-query": "5.80.7",
"@supabase/supabase-js": "2.50.2",
"@tanstack/react-query": "5.81.2",
"@tanstack/react-table": "^8.21.3",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"class-variance-authority": "^0.7.1",
"date-fns": "^4.1.0",
"eslint": "^9.29.0",
"next": "15.3.3",
"next": "15.3.4",
"next-themes": "0.4.6",
"prettier": "^3.5.3",
"prettier": "^3.6.1",
"react-day-picker": "^9.7.0",
"react-hook-form": "^7.58.0",
"react-hook-form": "^7.58.1",
"react-i18next": "^15.5.3",
"sonner": "^2.0.5",
"tailwindcss": "4.1.10",
@@ -130,7 +132,8 @@
"./app-breadcrumbs": "./src/makerkit/app-breadcrumbs.tsx",
"./empty-state": "./src/makerkit/empty-state.tsx",
"./marketing": "./src/makerkit/marketing/index.tsx",
"./oauth-provider-logo-image": "./src/makerkit/oauth-provider-logo-image.tsx"
"./oauth-provider-logo-image": "./src/makerkit/oauth-provider-logo-image.tsx",
"./file-uploader": "./src/makerkit/file-uploader.tsx"
},
"typesVersions": {
"*": {

View File

@@ -0,0 +1,247 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { SupabaseClient } from '@supabase/supabase-js';
import {
type FileError,
type FileRejection,
useDropzone,
} from 'react-dropzone';
interface FileWithPreview extends File {
preview?: string;
errors: readonly FileError[];
}
type UseSupabaseUploadOptions = {
/**
* Name of bucket to upload files to in your Supabase project
*/
bucketName: string;
/**
* Folder to upload files to in the specified bucket within your Supabase project.
*
* Defaults to uploading files to the root of the bucket
*
* e.g If specified path is `test`, your file will be uploaded as `test/file_name`
*/
path?: string;
/**
* Allowed MIME types for each file upload (e.g `image/png`, `text/html`, etc). Wildcards are also supported (e.g `image/*`).
*
* Defaults to allowing uploading of all MIME types.
*/
allowedMimeTypes?: string[];
/**
* Maximum upload size of each file allowed in bytes. (e.g 1000 bytes = 1 KB)
*/
maxFileSize?: number;
/**
* Maximum number of files allowed per upload.
*/
maxFiles?: number;
/**
* The number of seconds the asset is cached in the browser and in the Supabase CDN.
*
* This is set in the Cache-Control: max-age=<seconds> header. Defaults to 3600 seconds.
*/
cacheControl?: number;
/**
* When set to true, the file is overwritten if it exists.
*
* When set to false, an error is thrown if the object already exists. Defaults to `false`
*/
upsert?: boolean;
/**
* Supabase client to use for the upload.
*/
client: SupabaseClient;
/**
* Callback to call when the upload is successful.
*/
onUploadSuccess?: (files: string[]) => void;
};
export type UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>;
/**
* Hook to upload files to a Supabase bucket.
*
* @param options - Options for the upload.
* @returns The upload state.
*/
export const useSupabaseUpload = (options: UseSupabaseUploadOptions) => {
const {
bucketName,
path,
allowedMimeTypes = [],
maxFileSize = Number.POSITIVE_INFINITY,
maxFiles = 1,
cacheControl = 3600,
upsert = false,
client,
onUploadSuccess,
} = options;
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [errors, setErrors] = useState<{ name: string; message: string }[]>([]);
const [successes, setSuccesses] = useState<string[]>([]);
const isSuccess = useMemo(() => {
if (errors.length === 0 && successes.length === 0) {
return false;
}
if (errors.length === 0 && successes.length === files.length) {
return true;
}
return false;
}, [errors.length, successes.length, files.length]);
const onDrop = useCallback(
(acceptedFiles: File[], fileRejections: FileRejection[]) => {
const validFiles = acceptedFiles
.filter((file) => !files.find((x) => x.name === file.name))
.map((file) => {
(file as FileWithPreview).preview = URL.createObjectURL(file);
(file as FileWithPreview).errors = [];
return file as FileWithPreview;
});
const invalidFiles = fileRejections.map(({ file, errors }) => {
(file as FileWithPreview).preview = URL.createObjectURL(file);
(file as FileWithPreview).errors = errors;
return file as FileWithPreview;
});
const newFiles = [...files, ...validFiles, ...invalidFiles];
setFiles(newFiles);
},
[files, setFiles],
);
const dropzoneProps = useDropzone({
onDrop,
noClick: true,
accept: allowedMimeTypes.reduce(
(acc, type) => ({ ...acc, [type]: [] }),
{},
),
maxSize: maxFileSize,
maxFiles: maxFiles,
multiple: maxFiles !== 1,
});
const onUpload = useCallback(async () => {
setLoading(true);
// [Joshen] This is to support handling partial successes
// If any files didn't upload for any reason, hitting "Upload" again will only upload the files that had errors
const filesWithErrors = errors.map((x) => x.name);
const filesToUpload =
filesWithErrors.length > 0
? [
...files.filter((f) => filesWithErrors.includes(f.name)),
...files.filter((f) => !successes.includes(f.name)),
]
: files;
const responses = await Promise.all(
filesToUpload.map(async (file) => {
const filePath = path ? `${path}/${file.name}` : file.name;
const { error } = await client.storage
.from(bucketName)
.upload(filePath, file, {
cacheControl: cacheControl.toString(),
upsert,
});
const fullFilePath = [bucketName, filePath].join('/');
if (error) {
return { name: file.name, message: error.message, fullFilePath };
} else {
return { name: file.name, message: undefined, fullFilePath };
}
}),
);
const responseErrors = responses.filter((x) => x.message !== undefined);
// if there were errors previously, this function tried to upload the files again so we should clear/overwrite the existing errors.
setErrors(responseErrors);
const responseSuccesses = responses.filter((x) => x.message === undefined);
const newSuccesses = Array.from(
new Set([...successes, ...responseSuccesses.map((x) => x.name)]),
);
setSuccesses(newSuccesses);
if (responseSuccesses.length > 0) {
const files = responseSuccesses.map((item) => {
return item.fullFilePath;
});
onUploadSuccess?.(files);
}
setLoading(false);
}, [
files,
path,
bucketName,
errors,
successes,
onUploadSuccess,
client,
cacheControl,
upsert,
]);
useEffect(() => {
if (files.length === 0) {
setErrors([]);
}
// If the number of files doesn't exceed the maxFiles parameter, remove the error 'Too many files' from each file
if (files.length <= maxFiles) {
let changed = false;
const newFiles = files.map((file) => {
if (file.errors.some((e) => e.code === 'too-many-files')) {
file.errors = file.errors.filter((e) => e.code !== 'too-many-files');
changed = true;
}
return file;
});
if (changed) {
setFiles(newFiles);
}
}
}, [files.length, setFiles, maxFiles, files]);
return {
files,
setFiles,
successes,
isSuccess,
loading,
errors,
setErrors,
onUpload,
maxFileSize: maxFileSize,
maxFiles: maxFiles,
allowedMimeTypes,
...dropzoneProps,
};
};

View File

@@ -0,0 +1,306 @@
'use client';
import {
type PropsWithChildren,
createContext,
useCallback,
useContext,
} from 'react';
import { CheckCircle, File, Loader2, Upload, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { type UseSupabaseUploadReturn } from '../hooks/use-supabase-upload';
import { cn } from '../lib/utils';
import { Button } from '../shadcn/button';
import { Trans } from './trans';
export const formatBytes = (
bytes: number,
decimals = 2,
size?: 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB' | 'EB' | 'ZB' | 'YB',
) => {
const k = 1000;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
if (bytes === 0 || bytes === undefined) {
return size !== undefined ? `0 ${size}` : '0 bytes';
}
const i =
size !== undefined
? sizes.indexOf(size)
: Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
type DropzoneContextType = Omit<
UseSupabaseUploadReturn,
'getRootProps' | 'getInputProps'
>;
const DropzoneContext = createContext<DropzoneContextType | undefined>(
undefined,
);
type DropzoneProps = UseSupabaseUploadReturn & {
className?: string;
};
const Dropzone = ({
className,
children,
getRootProps,
getInputProps,
...restProps
}: PropsWithChildren<DropzoneProps>) => {
const isSuccess = restProps.isSuccess;
const isActive = restProps.isDragActive;
const isInvalid =
(restProps.isDragActive && restProps.isDragReject) ||
(restProps.errors.length > 0 && !restProps.isSuccess) ||
restProps.files.some((file) => file.errors.length !== 0);
return (
<DropzoneContext.Provider value={{ ...restProps }}>
<div
{...getRootProps({
className: cn(
'bg-card text-foreground rounded-lg border p-6 text-center transition-colors duration-300',
className,
isSuccess ? 'border-solid' : 'border-dashed',
isActive && 'border-primary',
isInvalid && 'border-destructive bg-destructive/10',
),
})}
>
<input {...getInputProps()} />
{children}
</div>
</DropzoneContext.Provider>
);
};
const DropzoneContent = ({ className }: { className?: string }) => {
const {
files,
setFiles,
onUpload,
loading,
successes,
errors,
maxFileSize,
maxFiles,
isSuccess,
} = useDropzoneContext();
const { t } = useTranslation();
const exceedMaxFiles = files.length > maxFiles;
const handleRemoveFile = useCallback(
(fileName: string) => {
setFiles(files.filter((file) => file.name !== fileName));
},
[files, setFiles],
);
if (isSuccess) {
return (
<div
className={cn(
'flex flex-row items-center justify-center gap-x-2',
className,
)}
>
<CheckCircle size={16} className="text-primary" />
<p className="text-primary text-sm">
<Trans
i18nKey="common:dropzone.success"
values={{ count: files.length }}
/>
</p>
</div>
);
}
return (
<div className={cn('flex flex-col', className)}>
{files.map((file, idx) => {
const fileError = errors.find((e) => e.name === file.name);
const isSuccessfullyUploaded = !!successes.find((e) => e === file.name);
return (
<div
key={`${file.name}-${idx}`}
className="flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4"
>
{file.type.startsWith('image/') ? (
<div className="bg-muted flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded border">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
decoding={'async'}
src={file.preview}
alt={file.name}
className="object-cover"
/>
</div>
) : (
<div className="bg-muted flex h-10 w-10 items-center justify-center rounded border">
<File size={18} />
</div>
)}
<div className="flex shrink grow flex-col items-start truncate">
<p title={file.name} className="max-w-full truncate text-sm">
{file.name}
</p>
{file.errors.length > 0 ? (
<p className="text-destructive text-xs">
{file.errors
.map((e) =>
e.message.startsWith('File is larger than')
? t('common:dropzone.errorMessageFileSizeTooLarge', {
size: formatBytes(file.size, 2),
maxSize: formatBytes(maxFileSize, 2),
})
: e.message,
)
.join(', ')}
</p>
) : loading && !isSuccessfullyUploaded ? (
<p className="text-muted-foreground text-xs">
<Trans i18nKey="common:dropzone.uploading" />
</p>
) : fileError ? (
<p className="text-destructive text-xs">
<Trans
i18nKey="common:dropzone.errorMessage"
values={{ message: fileError.message }}
/>
</p>
) : isSuccessfullyUploaded ? (
<p className="text-primary text-xs">
<Trans i18nKey="common:dropzone.success" />
</p>
) : (
<p className="text-muted-foreground text-xs">
{formatBytes(file.size, 2)}
</p>
)}
</div>
{!loading && !isSuccessfullyUploaded && (
<Button
size="icon"
variant="link"
className="text-muted-foreground hover:text-foreground shrink-0 justify-self-end"
onClick={() => handleRemoveFile(file.name)}
>
<X />
</Button>
)}
</div>
);
})}
{exceedMaxFiles && (
<p className="text-destructive mt-2 text-left text-sm">
<Trans
i18nKey="common:dropzone.errorMaxFiles"
values={{ count: maxFiles, files: files.length - maxFiles }}
/>
</p>
)}
{files.length > 0 && !exceedMaxFiles && (
<div className="mt-2">
<Button
variant="outline"
onClick={onUpload}
disabled={files.some((file) => file.errors.length !== 0) || loading}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Trans i18nKey="common:dropzone.uploading" />
</>
) : (
<span className="flex items-center">
<Upload size={20} className="mr-2 h-4 w-4" />
<Trans
i18nKey="common:dropzone.uploadFiles"
values={{
count: files.length,
}}
/>
</span>
)}
</Button>
</div>
)}
</div>
);
};
const DropzoneEmptyState = ({ className }: { className?: string }) => {
const { maxFiles, maxFileSize, inputRef, isSuccess } = useDropzoneContext();
if (isSuccess) {
return null;
}
return (
<div className={cn('flex flex-col items-center gap-y-2', className)}>
<Upload size={20} className="text-muted-foreground" />
<p className="text-sm">
<Trans
i18nKey="common:dropzone.uploadFiles"
values={{ count: maxFiles }}
/>
</p>
<div className="flex flex-col items-center gap-y-1">
<p className="text-muted-foreground text-xs">
<Trans i18nKey="common:dropzone.dragAndDrop" />{' '}
<a
onClick={() => inputRef.current?.click()}
className="hover:text-foreground cursor-pointer underline transition"
>
<Trans
i18nKey="common:dropzone.select"
values={{ count: maxFiles === 1 ? `file` : 'files' }}
/>
</a>{' '}
<Trans i18nKey="common:dropzone.toUpload" />
</p>
{maxFileSize !== Number.POSITIVE_INFINITY && (
<p className="text-muted-foreground text-xs">
<Trans
i18nKey="common:dropzone.maxFileSize"
values={{ size: formatBytes(maxFileSize, 2) }}
/>
</p>
)}
</div>
</div>
);
};
const useDropzoneContext = () => {
const context = useContext(DropzoneContext);
if (!context) {
throw new Error('useDropzoneContext must be used within a Dropzone');
}
return context;
};
export { Dropzone, DropzoneContent, DropzoneEmptyState, useDropzoneContext };

View File

@@ -0,0 +1,28 @@
import type { SupabaseClient } from '@supabase/supabase-js';
import { useSupabaseUpload } from '../hooks/use-supabase-upload';
import { cn } from '../lib/utils/cn';
import { Dropzone, DropzoneContent, DropzoneEmptyState } from './dropzone';
export const FileUploader = (props: {
className?: string;
maxFiles: number;
bucketName: string;
path?: string;
allowedMimeTypes: string[];
maxFileSize: number | undefined;
cacheControl?: number;
client: SupabaseClient;
onUploadSuccess?: (files: string[]) => void;
}) => {
const uploader = useSupabaseUpload(props);
return (
<div className={cn(props.className)}>
<Dropzone {...uploader}>
<DropzoneEmptyState />
<DropzoneContent />
</Dropzone>
</div>
);
};

3956
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,11 +13,11 @@
"format": "prettier --check \"**/*.{js,json}\""
},
"dependencies": {
"@next/eslint-plugin-next": "15.3.3",
"@next/eslint-plugin-next": "15.3.4",
"@types/eslint": "9.6.1",
"eslint-config-next": "15.3.3",
"eslint-config-next": "15.3.4",
"eslint-config-turbo": "^2.5.4",
"typescript-eslint": "8.34.1"
"typescript-eslint": "8.35.0"
},
"devDependencies": {
"@kit/prettier-config": "workspace:*",

View File

@@ -10,8 +10,8 @@
},
"dependencies": {
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.12"
"prettier": "^3.6.1",
"prettier-plugin-tailwindcss": "^0.6.13"
},
"devDependencies": {
"@kit/tsconfig": "workspace:*",