Files
supabase/apps/ui-library/public/r/dropzone-tanstack.json
Gildas Garcia 743d665dfe chore: migrate from next-mdx-remote to next-mdx-remote-client (#45149)
## Problem

We want to upgrade to react 19. However some libraries aren't compatible
with it. Besides, `next-mdx-remote` is now archived and not maintained
anymore.

## Solution

The [NextJS
documentation)[https://nextjs.org/docs/15/app/guides/mdx#remote-mdx]
suggest using
[`next-mdx-remote-client`](https://github.com/ipikuka/next-mdx-remote-client)
which was a fork of `next-mdx-remote`.

- [x] migrate `apps/www` from `next-mdx-remote` to
`next-mdx-remote-client`
- [x] migrate `apps/www` from `next-mdx-remote` to
`next-mdx-remote-client`

I haven't noticed any change in the pages.
When upgrading to react 19, we'll have to use v2 of
`next-mdx-remote-client`.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Switched MDX rendering/serialization to a newer client-focused
implementation across docs and site for improved compatibility.

* **Bug Fixes**
* Improved handling of serialization errors so MDX failures render clear
fallback messages instead of breaking pages.

* **Chores**
* Updated local environment template value for the public anonymous key.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-06 16:02:49 +02:00

43 lines
16 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "dropzone-tanstack",
"type": "registry:component",
"title": "Dropzone (File Upload)",
"description": "Displays a control for easier uploading of files directly to Supabase Storage.",
"dependencies": [
"react-dropzone",
"lucide-react",
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button"
],
"files": [
{
"path": "registry/default/blocks/dropzone/components/dropzone.tsx",
"content": "'use client'\n\nimport { CheckCircle, File, Loader2, Upload, X } from 'lucide-react'\nimport { createContext, useCallback, useContext, type PropsWithChildren } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { type UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'\nimport { Button } from '@/registry/default/components/ui/button'\n\nexport const formatBytes = (\n bytes: number,\n decimals = 2,\n size?: 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB' | 'EB' | 'ZB' | 'YB'\n) => {\n const k = 1000\n const dm = decimals < 0 ? 0 : decimals\n const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n\n if (bytes === 0 || bytes === undefined) return size !== undefined ? `0 ${size}` : '0 bytes'\n const i = size !== undefined ? sizes.indexOf(size) : Math.floor(Math.log(bytes) / Math.log(k))\n return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]\n}\n\ntype DropzoneContextType = Omit<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const isSuccess = restProps.isSuccess\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n (restProps.errors.length > 0 && !restProps.isSuccess) ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground',\n className,\n isSuccess ? 'border-solid' : 'border-dashed',\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const {\n files,\n setFiles,\n onUpload,\n loading,\n successes,\n errors,\n maxFileSize,\n maxFiles,\n isSuccess,\n } = useDropzoneContext()\n\n const exceedMaxFiles = files.length > maxFiles\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (isSuccess) {\n return (\n <div className={cn('flex flex-row items-center gap-x-2 justify-center', className)}>\n <CheckCircle size={16} className=\"text-primary\" />\n <p className=\"text-primary text-sm\">\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n </p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file, idx) => {\n const fileError = errors.find((e) => e.name === file.name)\n const isSuccessfullyUploaded = !!successes.find((e) => e === file.name)\n\n return (\n <div\n key={`${file.name}-${idx}`}\n className=\"flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-10 w-10 rounded-sm border overflow-hidden shrink-0 bg-muted flex items-center justify-center\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-10 w-10 rounded-sm border bg-muted flex items-center justify-center\">\n <File size={18} />\n </div>\n )}\n\n <div className=\"shrink grow flex flex-col items-start truncate\">\n <p title={file.name} className=\"text-sm truncate max-w-full\">\n {file.name}\n </p>\n {file.errors.length > 0 ? (\n <p className=\"text-xs text-destructive\">\n {file.errors\n .map((e) =>\n e.message.startsWith('File is larger than')\n ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})`\n : e.message\n )\n .join(', ')}\n </p>\n ) : loading && !isSuccessfullyUploaded ? (\n <p className=\"text-xs text-muted-foreground\">Uploading file...</p>\n ) : !!fileError ? (\n <p className=\"text-xs text-destructive\">Failed to upload: {fileError.message}</p>\n ) : isSuccessfullyUploaded ? (\n <p className=\"text-xs text-primary\">Successfully uploaded file</p>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{formatBytes(file.size, 2)}</p>\n )}\n </div>\n\n {!loading && !isSuccessfullyUploaded && (\n <Button\n size=\"icon\"\n variant=\"link\"\n className=\"shrink-0 justify-self-end text-muted-foreground hover:text-foreground\"\n onClick={() => handleRemoveFile(file.name)}\n >\n <X />\n </Button>\n )}\n </div>\n )\n })}\n {exceedMaxFiles && (\n <p className=\"text-sm text-left mt-2 text-destructive\">\n You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file\n {files.length - maxFiles > 1 ? 's' : ''}.\n </p>\n )}\n {files.length > 0 && !exceedMaxFiles && (\n <div className=\"mt-2\">\n <Button\n variant=\"outline\"\n onClick={onUpload}\n disabled={files.some((file) => file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n Uploading...\n </>\n ) : (\n <>Upload files</>\n )}\n </Button>\n </div>\n )}\n </div>\n )\n}\n\nconst DropzoneEmptyState = ({ className }: { className?: string }) => {\n const { maxFiles, maxFileSize, inputRef, isSuccess } = useDropzoneContext()\n\n if (isSuccess) {\n return null\n }\n\n return (\n <div className={cn('flex flex-col items-center gap-y-2', className)}>\n <Upload size={20} className=\"text-muted-foreground\" />\n <p className=\"text-sm\">\n Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file\n {!maxFiles || maxFiles > 1 ? 's' : ''}\n </p>\n <div className=\"flex flex-col items-center gap-y-1\">\n <p className=\"text-xs text-muted-foreground\">\n Drag and drop or{' '}\n <a\n onClick={() => inputRef.current?.click()}\n className=\"underline cursor-pointer transition hover:text-foreground\"\n >\n select {maxFiles === 1 ? `file` : 'files'}\n </a>{' '}\n to upload\n </p>\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n <p className=\"text-xs text-muted-foreground\">\n Maximum file size: {formatBytes(maxFileSize, 2)}\n </p>\n )}\n </div>\n </div>\n )\n}\n\nconst useDropzoneContext = () => {\n const context = useContext(DropzoneContext)\n\n if (!context) {\n throw new Error('useDropzoneContext must be used within a Dropzone')\n }\n\n return context\n}\n\nexport { Dropzone, DropzoneContent, DropzoneEmptyState, useDropzoneContext }\n",
"type": "registry:component"
},
{
"path": "registry/default/blocks/dropzone/hooks/use-supabase-upload.ts",
"content": "import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useDropzone, type FileError, type FileRejection } from 'react-dropzone'\n\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\n\nconst supabase = createClient()\n\ninterface FileWithPreview extends File {\n preview?: string\n errors: readonly FileError[]\n}\n\ntype UseSupabaseUploadOptions = {\n /**\n * Name of bucket to upload files to in your Supabase project\n */\n bucketName: string\n /**\n * Folder to upload files to in the specified bucket within your Supabase project.\n *\n * Defaults to uploading files to the root of the bucket\n *\n * e.g If specified path is `test`, your file will be uploaded as `test/file_name`\n */\n path?: string\n /**\n * Allowed MIME types for each file upload (e.g `image/png`, `text/html`, etc). Wildcards are also supported (e.g `image/*`).\n *\n * Defaults to allowing uploading of all MIME types.\n */\n allowedMimeTypes?: string[]\n /**\n * Maximum upload size of each file allowed in bytes. (e.g 1000 bytes = 1 KB)\n */\n maxFileSize?: number\n /**\n * Maximum number of files allowed per upload.\n */\n maxFiles?: number\n /**\n * The number of seconds the asset is cached in the browser and in the Supabase CDN.\n *\n * This is set in the Cache-Control: max-age=<seconds> header. Defaults to 3600 seconds.\n */\n cacheControl?: number\n /**\n * When set to true, the file is overwritten if it exists.\n *\n * When set to false, an error is thrown if the object already exists. Defaults to `false`\n */\n upsert?: boolean\n}\n\ntype UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const [files, setFiles] = useState<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState<string[]>([])\n\n const isSuccess = useMemo(() => {\n if (errors.length === 0 && successes.length === 0) {\n return false\n }\n if (errors.length === 0 && successes.length === files.length) {\n return true\n }\n return false\n }, [errors.length, successes.length, files.length])\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles\n .filter((file) => !files.find((x) => x.name === file.name))\n .map((file) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = []\n return file as FileWithPreview\n })\n\n const invalidFiles = fileRejections.map(({ file, errors }) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = errors\n return file as FileWithPreview\n })\n\n const newFiles = [...files, ...validFiles, ...invalidFiles]\n\n setFiles(newFiles)\n },\n [files, setFiles]\n )\n\n const dropzoneProps = useDropzone({\n onDrop,\n noClick: true,\n accept: allowedMimeTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),\n maxSize: maxFileSize,\n maxFiles: maxFiles,\n multiple: maxFiles !== 1,\n })\n\n const onUpload = useCallback(async () => {\n setLoading(true)\n\n // [Joshen] This is to support handling partial successes\n // If any files didn't upload for any reason, hitting \"Upload\" again will only upload the files that had errors\n const filesWithErrors = errors.map((x) => x.name)\n const filesToUpload =\n filesWithErrors.length > 0\n ? [\n ...files.filter((f) => filesWithErrors.includes(f.name)),\n ...files.filter((f) => !successes.includes(f.name)),\n ]\n : files\n\n const responses = await Promise.all(\n filesToUpload.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(!!path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n if (error) {\n return { name: file.name, message: error.message }\n } else {\n return { name: file.name, message: undefined }\n }\n })\n )\n\n const responseErrors = responses.filter((x) => x.message !== undefined)\n // if there were errors previously, this function tried to upload the files again so we should clear/overwrite the existing errors.\n setErrors(responseErrors)\n\n const responseSuccesses = responses.filter((x) => x.message === undefined)\n const newSuccesses = Array.from(\n new Set([...successes, ...responseSuccesses.map((x) => x.name)])\n )\n setSuccesses(newSuccesses)\n\n setLoading(false)\n }, [files, path, bucketName, errors, successes])\n\n useEffect(() => {\n if (files.length === 0) {\n setErrors([])\n }\n\n // If the number of files doesn't exceed the maxFiles parameter, remove the error 'Too many files' from each file\n if (files.length <= maxFiles) {\n let changed = false\n const newFiles = files.map((file) => {\n if (file.errors.some((e) => e.code === 'too-many-files')) {\n file.errors = file.errors.filter((e) => e.code !== 'too-many-files')\n changed = true\n }\n return file\n })\n if (changed) {\n setFiles(newFiles)\n }\n }\n }, [files.length, setFiles, maxFiles])\n\n return {\n files,\n setFiles,\n successes,\n isSuccess,\n loading,\n errors,\n setErrors,\n onUpload,\n maxFileSize: maxFileSize,\n maxFiles: maxFiles,\n allowedMimeTypes,\n ...dropzoneProps,\n }\n}\n\nexport { useSupabaseUpload, type UseSupabaseUploadOptions, type UseSupabaseUploadReturn }\n",
"type": "registry:hook"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
"content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n",
"type": "registry:lib"
}
],
"envVars": {
"VITE_SUPABASE_URL": "",
"VITE_SUPABASE_PUBLISHABLE_KEY": ""
},
"docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`."
}