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:
committed by
GitHub
parent
180e0e0c5e
commit
c1fda420e6
@@ -19,8 +19,8 @@ export async function loadTranslations() {
|
|||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
translations[locale] = {};
|
translations[locale] = {};
|
||||||
|
|
||||||
const namespaces = readdirSync(join(localesPath, locale)).filter(
|
const namespaces = readdirSync(join(localesPath, locale)).filter((file) =>
|
||||||
(file) => file.endsWith('.json'),
|
file.endsWith('.json'),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const namespace of namespaces) {
|
for (const namespace of namespaces) {
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^1.3.22",
|
"@ai-sdk/openai": "^1.3.22",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"ai": "4.3.16",
|
"ai": "4.3.16",
|
||||||
"lucide-react": "^0.516.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.4",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"react-hook-form": "^7.58.0",
|
"react-hook-form": "^7.58.1",
|
||||||
"tailwindcss": "4.1.10",
|
"tailwindcss": "4.1.10",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.53.0",
|
"@playwright/test": "^1.53.1",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.4",
|
||||||
"dotenv": "16.5.0",
|
"dotenv": "16.5.0",
|
||||||
"node-html-parser": "^7.0.1",
|
"node-html-parser": "^7.0.1",
|
||||||
"totp-generator": "^1.0.0"
|
"totp-generator": "^1.0.0"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
|
|||||||
if (auth.error instanceof MultiFactorAuthError) {
|
if (auth.error instanceof MultiFactorAuthError) {
|
||||||
const urlParams = new URLSearchParams({
|
const urlParams = new URLSearchParams({
|
||||||
next: `${pathsConfig.app.joinTeam}?invite_token=${token}&email=${searchParams.email ?? ''}`,
|
next: `${pathsConfig.app.joinTeam}?invite_token=${token}&email=${searchParams.email ?? ''}`,
|
||||||
})
|
});
|
||||||
|
|
||||||
const verifyMfaUrl = `${pathsConfig.auth.verifyMfa}?${urlParams.toString()}`;
|
const verifyMfaUrl = `${pathsConfig.auth.verifyMfa}?${urlParams.toString()}`;
|
||||||
|
|
||||||
|
|||||||
@@ -56,17 +56,17 @@
|
|||||||
"@marsidev/react-turnstile": "^1.1.0",
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
"@nosecone/next": "1.0.0-beta.8",
|
"@nosecone/next": "1.0.0-beta.8",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.516.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"next-sitemap": "^4.2.3",
|
"next-sitemap": "^4.2.3",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "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",
|
"react-i18next": "^15.5.3",
|
||||||
"recharts": "2.15.3",
|
"recharts": "2.15.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@@ -76,15 +76,15 @@
|
|||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@next/bundle-analyzer": "15.3.3",
|
"@next/bundle-analyzer": "15.3.4",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.4",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"cssnano": "^7.0.7",
|
"cssnano": "^7.0.7",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.1",
|
||||||
"supabase": "^2.26.9",
|
"supabase": "^2.26.9",
|
||||||
"tailwindcss": "4.1.10",
|
"tailwindcss": "4.1.10",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
@@ -92,5 +92,21 @@
|
|||||||
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"reject": "Reject",
|
"reject": "Reject",
|
||||||
"accept": "Accept"
|
"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
@@ -48,7 +48,7 @@
|
|||||||
"@manypkg/cli": "^0.24.0",
|
"@manypkg/cli": "^0.24.0",
|
||||||
"@turbo/gen": "^2.5.4",
|
"@turbo/gen": "^2.5.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.1",
|
||||||
"turbo": "2.5.4",
|
"turbo": "2.5.4",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@types/node": "^24.0.1"
|
"@types/node": "^24.0.4"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -26,13 +26,13 @@
|
|||||||
"@kit/supabase": "workspace:*",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.516.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-hook-form": "^7.58.0",
|
"react-hook-form": "^7.58.1",
|
||||||
"react-i18next": "^15.5.3",
|
"react-i18next": "^15.5.3",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stripe/react-stripe-js": "^3.7.0",
|
"@stripe/react-stripe-js": "^3.7.0",
|
||||||
"@stripe/stripe-js": "^7.3.1",
|
"@stripe/stripe-js": "^7.4.0",
|
||||||
"stripe": "^18.2.1"
|
"stripe": "^18.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@kit/shared": "workspace:*",
|
"@kit/shared": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/wordpress": "workspace:*",
|
"@kit/wordpress": "workspace:*",
|
||||||
"@types/node": "^24.0.1"
|
"@types/node": "^24.0.4"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.4",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.4",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"wp-types": "^4.68.0"
|
"wp-types": "^4.68.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"@kit/supabase": "workspace:*",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/team-accounts": "workspace:*",
|
"@kit/team-accounts": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-email/components": "0.1.0"
|
"@react-email/components": "0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
|
|||||||
@@ -34,16 +34,16 @@
|
|||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"lucide-react": "^0.516.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "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",
|
"react-i18next": "^15.5.3",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ export function PersonalAccountDropdown({
|
|||||||
className ?? '',
|
className ?? '',
|
||||||
{
|
{
|
||||||
['active:bg-secondary/50 items-center gap-4 rounded-md' +
|
['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,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,15 +20,15 @@
|
|||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@makerkit/data-loader-supabase-core": "^0.0.10",
|
"@makerkit/data-loader-supabase-core": "^0.0.10",
|
||||||
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
|
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"lucide-react": "^0.516.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.58.0",
|
"react-hook-form": "^7.58.1",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -29,12 +29,12 @@
|
|||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@marsidev/react-turnstile": "^1.1.0",
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"lucide-react": "^0.516.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"react-hook-form": "^7.58.0",
|
"react-hook-form": "^7.58.1",
|
||||||
"react-i18next": "^15.5.3",
|
"react-i18next": "^15.5.3",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import Link from 'next/link';
|
|||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { UserCheck } from 'lucide-react';
|
import { UserCheck } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Alert, AlertDescription } from '@kit/ui/alert';
|
import { Alert, AlertDescription } from '@kit/ui/alert';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
|
import { useLastAuthMethod } from '../hooks/use-last-auth-method';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
interface ExistingAccountHintProps {
|
interface ExistingAccountHintProps {
|
||||||
signInPath?: string;
|
signInPath?: string;
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ function LastAuthMethodHintImpl({ className }: LastAuthMethodHintProps) {
|
|||||||
|
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey="auth:lastUsedMethodPrefix" />{' '}
|
<Trans i18nKey="auth:lastUsedMethodPrefix" />{' '}
|
||||||
|
|
||||||
<If condition={isOAuth && Boolean(providerName)}>
|
<If condition={isOAuth && Boolean(providerName)}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="auth:methodOauthWithProvider"
|
i18nKey="auth:methodOauthWithProvider"
|
||||||
@@ -72,7 +71,6 @@ function LastAuthMethodHintImpl({ className }: LastAuthMethodHintProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={!isOAuth || !providerName}>
|
<If condition={!isOAuth || !providerName}>
|
||||||
<span className="text-muted-foreground font-medium">
|
<span className="text-muted-foreground font-medium">
|
||||||
<Trans i18nKey={methodKey} />
|
<Trans i18nKey={methodKey} />
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
"@kit/supabase": "workspace:*",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"lucide-react": "^0.516.0",
|
"lucide-react": "^0.523.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-i18next": "^15.5.3"
|
"react-i18next": "^15.5.3"
|
||||||
|
|||||||
@@ -32,18 +32,18 @@
|
|||||||
"@kit/supabase": "workspace:*",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.516.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "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",
|
"react-i18next": "^15.5.3",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/shared": "workspace:*",
|
"@kit/shared": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-i18next": "^15.5.3"
|
"react-i18next": "^15.5.3"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@kit/resend": "workspace:*",
|
"@kit/resend": "workspace:*",
|
||||||
"@kit/shared": "workspace:*",
|
"@kit/shared": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.4",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"@kit/mailers-shared": "workspace:*",
|
"@kit/mailers-shared": "workspace:*",
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@types/node": "^24.0.1",
|
"@types/node": "^24.0.4",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
"./config/server": "./src/sentry.client.server.ts"
|
"./config/server": "./src/sentry.client.server.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/nextjs": "^9.29.0",
|
"@sentry/nextjs": "^9.32.0",
|
||||||
"import-in-the-middle": "1.14.0"
|
"import-in-the-middle": "1.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/supabase": "workspace:*",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|||||||
@@ -25,12 +25,12 @@
|
|||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@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": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.58.0",
|
"react-hook-form": "^7.58.1",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|||||||
@@ -25,10 +25,10 @@
|
|||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@supabase/ssr": "^0.6.1",
|
"@supabase/ssr": "^0.6.1",
|
||||||
"@supabase/supabase-js": "2.50.0",
|
"@supabase/supabase-js": "2.50.2",
|
||||||
"@tanstack/react-query": "5.80.7",
|
"@tanstack/react-query": "5.81.2",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"input-otp": "1.4.2",
|
"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",
|
"react-top-loading-bar": "3.0.2",
|
||||||
"recharts": "2.15.3",
|
"recharts": "2.15.3",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
@@ -43,18 +44,19 @@
|
|||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@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",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.4",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.1",
|
||||||
"react-day-picker": "^9.7.0",
|
"react-day-picker": "^9.7.0",
|
||||||
"react-hook-form": "^7.58.0",
|
"react-hook-form": "^7.58.1",
|
||||||
"react-i18next": "^15.5.3",
|
"react-i18next": "^15.5.3",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwindcss": "4.1.10",
|
"tailwindcss": "4.1.10",
|
||||||
@@ -130,7 +132,8 @@
|
|||||||
"./app-breadcrumbs": "./src/makerkit/app-breadcrumbs.tsx",
|
"./app-breadcrumbs": "./src/makerkit/app-breadcrumbs.tsx",
|
||||||
"./empty-state": "./src/makerkit/empty-state.tsx",
|
"./empty-state": "./src/makerkit/empty-state.tsx",
|
||||||
"./marketing": "./src/makerkit/marketing/index.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": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
247
packages/ui/src/hooks/use-supabase-upload.tsx
Normal file
247
packages/ui/src/hooks/use-supabase-upload.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
306
packages/ui/src/makerkit/dropzone.tsx
Normal file
306
packages/ui/src/makerkit/dropzone.tsx
Normal 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 };
|
||||||
28
packages/ui/src/makerkit/file-uploader.tsx
Normal file
28
packages/ui/src/makerkit/file-uploader.tsx
Normal 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
3956
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,11 @@
|
|||||||
"format": "prettier --check \"**/*.{js,json}\""
|
"format": "prettier --check \"**/*.{js,json}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/eslint-plugin-next": "15.3.3",
|
"@next/eslint-plugin-next": "15.3.4",
|
||||||
"@types/eslint": "9.6.1",
|
"@types/eslint": "9.6.1",
|
||||||
"eslint-config-next": "15.3.3",
|
"eslint-config-next": "15.3.4",
|
||||||
"eslint-config-turbo": "^2.5.4",
|
"eslint-config-turbo": "^2.5.4",
|
||||||
"typescript-eslint": "8.34.1"
|
"typescript-eslint": "8.35.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.12"
|
"prettier-plugin-tailwindcss": "^0.6.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
|
|||||||
Reference in New Issue
Block a user