diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index b8378bf499a..9beb4719f55 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 2.19.7 + version: 2.20.3 title: Supabase CLI language: sh source: https://github.com/supabase/cli diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index d8807b64234..21ca6157310 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -45,17 +45,19 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { const isPhoneAuth = user.phone !== null const isBanned = user.banned_until !== null - const providers = (user.raw_app_meta_data?.providers ?? []).map((provider: string) => { - return { - name: provider.startsWith('sso') ? 'SAML' : provider, - icon: - provider === 'email' - ? `${BASE_PATH}/img/icons/email-icon2.svg` - : providerIconMap[provider] - ? `${BASE_PATH}/img/icons/${providerIconMap[provider]}.svg` - : undefined, + const providers = ((user.raw_app_meta_data?.providers as string[]) ?? []).map( + (provider: string) => { + return { + name: provider.startsWith('sso') ? 'SAML' : provider, + icon: + provider === 'email' + ? `${BASE_PATH}/img/icons/email-icon2.svg` + : providerIconMap[provider] + ? `${BASE_PATH}/img/icons/${providerIconMap[provider]}.svg` + : undefined, + } } - }) + ) const canUpdateUser = useCheckPermissions(PermissionAction.AUTH_EXECUTE, '*') const canSendMagicLink = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_magic_link') diff --git a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx index 3c129acfcf6..da827cf30b1 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx +++ b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx @@ -27,7 +27,7 @@ export const isAtBottom = ({ currentTarget }: UIEvent): boolean export const formatUsersData = (users: User[]) => { return users.map((user) => { - const provider: string = user.raw_app_meta_data?.provider ?? '' + const provider: string = (user.raw_app_meta_data?.provider as string) ?? '' const providers: string[] = user.providers.map((x: string) => { if (x.startsWith('sso')) return 'SAML' return x @@ -168,7 +168,7 @@ export function getDisplayName(user: User, fallback = '-'): string { last_name: cc_last_name, firstName: ccFirstName, first_name: cc_first_name, - } = custom_claims ?? {} + } = (custom_claims ?? {}) as any const last = toPrettyJsonString( familyName || diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx index 10f81c41458..917cab4c768 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx @@ -43,7 +43,7 @@ export const Restriction = () => { shownAlert = 'restricted' } - if (shownAlert === null) { + if (shownAlert === null || !org?.restriction_data) { return null } diff --git a/apps/studio/components/interfaces/Organization/Documents/DPA.tsx b/apps/studio/components/interfaces/Organization/Documents/DPA.tsx index 4a5267dafa5..9409bf237f8 100644 --- a/apps/studio/components/interfaces/Organization/Documents/DPA.tsx +++ b/apps/studio/components/interfaces/Organization/Documents/DPA.tsx @@ -38,7 +38,7 @@ const DPA = () => { download={true} target="_blank" rel="noreferrer noopener" - href="https://supabase.com/downloads/docs/Supabase+DPA+231211.pdf" + href="https://supabase.com/downloads/docs/Supabase+DPA+250314.pdf" > View DPA diff --git a/apps/studio/components/interfaces/Organization/Documents/TIA.tsx b/apps/studio/components/interfaces/Organization/Documents/TIA.tsx index c5ba341a8c2..f2589584876 100644 --- a/apps/studio/components/interfaces/Organization/Documents/TIA.tsx +++ b/apps/studio/components/interfaces/Organization/Documents/TIA.tsx @@ -20,7 +20,7 @@ const TIA = () => { { - const defaultValue = availableVersions[0] ? formatValue(availableVersions[0]) : undefined + const gaVersion = availableVersions.find((x) => x.release_channel === 'ga') + const defaultValue = gaVersion ? formatValue(gaVersion) : undefined form.setValue('postgresVersionSelection', defaultValue) }, [isSuccess, form]) diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx index 01c2d0a3d9e..9951c1138d5 100644 --- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -122,8 +122,8 @@ const Addons = () => { You are currently on a preview branch of your project - Updating addons are not available while you're on a preview branch. To manage your - addons, you may return to your{' '} + Updating addons here will only apply to this preview branch. To manage your addons, + for your main branch, please visit the{' '} main branch @@ -245,10 +245,7 @@ const Addons = () => { className="mt-2 pointer-events-auto" onClick={() => setPanel('computeInstance')} disabled={ - isBranch || - !isProjectActive || - projectUpdateDisabled || - computeSizeChangesDisabled + !isProjectActive || projectUpdateDisabled || computeSizeChangesDisabled } > Change compute size @@ -447,10 +444,7 @@ const Addons = () => { className="mt-2 pointer-events-auto" onClick={() => setPanel('ipv4')} disabled={ - isBranch || - !isProjectActive || - projectUpdateDisabled || - !(canUpdateIPv4 || ipv4) + !isProjectActive || projectUpdateDisabled || !(canUpdateIPv4 || ipv4) } tooltip={{ content: { @@ -576,7 +570,6 @@ const Addons = () => { className="mt-2 pointer-events-auto" onClick={() => setPanel('pitr')} disabled={ - isBranch || !isProjectActive || projectUpdateDisabled || !sufficientPgVersion || @@ -652,7 +645,7 @@ const Addons = () => { type="default" className="mt-2 pointer-events-auto" onClick={() => setPanel('customDomain')} - disabled={isBranch || !isProjectActive || projectUpdateDisabled} + disabled={!isProjectActive || projectUpdateDisabled} > Change custom domain diff --git a/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx b/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx index 78c18f5148a..4a0a7368802 100644 --- a/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx +++ b/apps/studio/components/layouts/AppLayout/RestrictionBanner.tsx @@ -27,7 +27,7 @@ export const RestrictionBanner = () => { {currentOrg.restriction_status === 'restricted' ? : } {currentOrg.restriction_status === 'grace_period' && - `Your organization has exceeded its quota. You are given a grace period until ${dayjs(currentOrg.restriction_data['grace_period_end']).format('DD MMM, YYYY')}`} + `Your organization has exceeded its quota. You are given a grace period until ${dayjs(currentOrg.restriction_data!['grace_period_end']).format('DD MMM, YYYY')}`} {currentOrg.restriction_status === 'grace_period_over' && `Your grace period is over and your project will not be able to serve requests when you used up your quota.`} {currentOrg.restriction_status === 'restricted' && diff --git a/apps/studio/data/organization-members/organization-roles-query.ts b/apps/studio/data/organization-members/organization-roles-query.ts index 1a40af2acb0..9a0be12bee7 100644 --- a/apps/studio/data/organization-members/organization-roles-query.ts +++ b/apps/studio/data/organization-members/organization-roles-query.ts @@ -8,7 +8,8 @@ import { organizationKeys } from './keys' export const FIXED_ROLE_ORDER = ['Owner', 'Administrator', 'Developer', 'Read-only'] export type OrganizationRolesVariables = { slug?: string } export type OrganizationRolesResponse = components['schemas']['OrganizationRoleResponse'] -export type OrganizationRole = components['schemas']['OrganizationRoleV2'] +export type OrganizationRole = + components['schemas']['OrganizationRoleResponse']['org_scoped_roles'][0] export async function getOrganizationRoles( { slug }: OrganizationRolesVariables, diff --git a/apps/studio/data/organizations/organization-audit-logs-query.ts b/apps/studio/data/organizations/organization-audit-logs-query.ts index 67df5e1fa9f..4480816c4a1 100644 --- a/apps/studio/data/organizations/organization-audit-logs-query.ts +++ b/apps/studio/data/organizations/organization-audit-logs-query.ts @@ -37,8 +37,8 @@ export type OrganizationAuditLogsResponse = { export type OrganizationAuditLogsVariables = { slug?: string - iso_timestamp_start?: string - iso_timestamp_end?: string + iso_timestamp_start: string + iso_timestamp_end: string } export async function getOrganizationAuditLogs( diff --git a/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts b/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts index 945ae9c7f14..ca7af88b5d8 100644 --- a/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts +++ b/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts @@ -8,7 +8,7 @@ import { organizationKeys } from './keys' export type OrganizationCustomerProfileUpdateVariables = { slug: string - address?: components['schemas']['CustomerBillingAddress'] + address?: components['schemas']['CustomerResponse']['address'] } export async function updateOrganizationCustomerProfile({ diff --git a/apps/studio/data/projects/project-restart-mutation.ts b/apps/studio/data/projects/project-restart-mutation.ts index 9a226237ac4..f8cda402e68 100644 --- a/apps/studio/data/projects/project-restart-mutation.ts +++ b/apps/studio/data/projects/project-restart-mutation.ts @@ -10,7 +10,7 @@ export type ProjectRestartVariables = { identifier?: string } -type RestartProjectBody = components['schemas']['RestartProjectBodyDto'] +type RestartProjectBody = components['schemas']['RestartProjectBody'] export async function restartProject({ ref, identifier }: ProjectRestartVariables) { const payload: RestartProjectBody = {} diff --git a/apps/studio/data/storage/bucket-object-download-mutation.ts b/apps/studio/data/storage/bucket-object-download-mutation.ts index 0c790f11353..dca56be8d93 100644 --- a/apps/studio/data/storage/bucket-object-download-mutation.ts +++ b/apps/studio/data/storage/bucket-object-download-mutation.ts @@ -10,7 +10,7 @@ type DownloadBucketObjectParams = { projectRef: string bucketId?: string path: string - options?: components['schemas']['DownloadObjectOptions'] + options?: components['schemas']['DownloadObjectBody']['options'] } export const downloadBucketObject = async ( { projectRef, bucketId, path, options }: DownloadBucketObjectParams, diff --git a/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts b/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts index a34a9ae108f..810b724ee70 100644 --- a/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts +++ b/apps/studio/data/storage/bucket-object-get-public-url-mutation.ts @@ -9,7 +9,7 @@ type BucketObjectPublicUrlParams = { projectRef: string bucketId?: string path: string - options?: components['schemas']['PublicUrlOptions'] + options?: components['schemas']['GetSignedUrlBody']['options'] } export const getPublicUrlForBucketObject = async ( { projectRef, bucketId, path, options }: BucketObjectPublicUrlParams, diff --git a/apps/studio/data/storage/bucket-object-sign-mutation.ts b/apps/studio/data/storage/bucket-object-sign-mutation.ts index 0fd305a3879..34600f8a459 100644 --- a/apps/studio/data/storage/bucket-object-sign-mutation.ts +++ b/apps/studio/data/storage/bucket-object-sign-mutation.ts @@ -10,7 +10,7 @@ type SignBucketObjectParams = { bucketId?: string path: string expiresIn: number - options?: components['schemas']['SignedUrlOptions'] + options?: components['schemas']['GetSignedUrlBody']['options'] } export const signBucketObject = async ( { projectRef, bucketId, path, expiresIn, options }: SignBucketObjectParams, diff --git a/apps/studio/data/storage/bucket-objects-list-mutation.ts b/apps/studio/data/storage/bucket-objects-list-mutation.ts index 9870e9b0bc2..da97ab1718f 100644 --- a/apps/studio/data/storage/bucket-objects-list-mutation.ts +++ b/apps/studio/data/storage/bucket-objects-list-mutation.ts @@ -9,7 +9,7 @@ type ListBucketObjectsParams = { projectRef: string bucketId?: string path: string - options: components['schemas']['StorageObjectSearchOptions'] + options: components['schemas']['GetObjectsBody']['options'] } export type StorageObject = components['schemas']['StorageObject'] diff --git a/apps/studio/data/subscriptions/types.ts b/apps/studio/data/subscriptions/types.ts index 43c81ecb94b..9491f5589d5 100644 --- a/apps/studio/data/subscriptions/types.ts +++ b/apps/studio/data/subscriptions/types.ts @@ -15,7 +15,7 @@ export type ProjectAddon = components['schemas']['BillingProjectAddonResponse'] export type PlanId = components['schemas']['BillingPlanId'] -export type OrgPlan = components['schemas']['PlanResponse'] +export type OrgPlan = components['schemas']['PlansResponse']['plans'][0] export type ProjectAddonType = components['schemas']['ProjectAddonType'] diff --git a/apps/studio/data/usage/org-usage-query.ts b/apps/studio/data/usage/org-usage-query.ts index 79124b869b6..9b6f4ae4344 100644 --- a/apps/studio/data/usage/org-usage-query.ts +++ b/apps/studio/data/usage/org-usage-query.ts @@ -14,7 +14,7 @@ export type OrgUsageVariables = { } export type OrgUsageResponse = components['schemas']['OrgUsageResponse'] -export type OrgMetricsUsage = components['schemas']['OrgMetricUsage'] +export type OrgMetricsUsage = components['schemas']['OrgUsageResponse']['usages'][0] export async function getOrgUsage( { orgSlug, projectRef, start, end }: OrgUsageVariables, diff --git a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx index c8f321de714..69ba342545c 100644 --- a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx +++ b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx @@ -940,7 +940,7 @@ class StorageExplorerStore { ) const promises = files.map((file) => { - const fileMimeType = file.metadata?.mimetype ?? null + const fileMimeType = (file.metadata?.mimetype as string) ?? null return () => { return new Promise< | { diff --git a/apps/studio/pages/project/[ref]/settings/general.tsx b/apps/studio/pages/project/[ref]/settings/general.tsx index c6e4981befa..ce096e96ced 100644 --- a/apps/studio/pages/project/[ref]/settings/general.tsx +++ b/apps/studio/pages/project/[ref]/settings/general.tsx @@ -34,15 +34,12 @@ const ProjectSettings: NextPageWithLayout = () => { - {!isBranch ? ( - <> - {/* this is only setable on compliance orgs, currently that means HIPAA orgs */} - {hasHipaaAddon && } - - {projectTransferEnabled && } - - - ) : null} + + {/* this is only settable on compliance orgs, currently that means HIPAA orgs */} + {!isBranch && hasHipaaAddon && } + + {!isBranch && projectTransferEnabled && } + {!isBranch && } ) diff --git a/apps/studio/types/base.ts b/apps/studio/types/base.ts index f10ead99444..0543089ff20 100644 --- a/apps/studio/types/base.ts +++ b/apps/studio/types/base.ts @@ -10,7 +10,7 @@ export interface Organization { opt_in_tags: string[] subscription_id?: string | null restriction_status: 'grace_period' | 'grace_period_over' | 'restricted' | null - restriction_data: Record + restriction_data: Record | null managed_by: 'supabase' | 'vercel-marketplace' | 'aws-marketplace' partner_id?: string } diff --git a/apps/ui-library/components/block-item-code.tsx b/apps/ui-library/components/block-item-code.tsx index 3d0ba4f0823..af6960be537 100644 --- a/apps/ui-library/components/block-item-code.tsx +++ b/apps/ui-library/components/block-item-code.tsx @@ -73,7 +73,7 @@ export function BlockItemCode({ files }: BlockItemCodeProps) { : null return ( -
+
{/* File browser sidebar */}
diff --git a/apps/ui-library/config/docs.ts b/apps/ui-library/config/docs.ts index 722620c80f6..aa5c375e313 100644 --- a/apps/ui-library/config/docs.ts +++ b/apps/ui-library/config/docs.ts @@ -74,6 +74,12 @@ export const frameworkPages: Record = { 'react-router': { title: 'React Router', items: [ + { + title: 'Client', + href: '/docs/react-router/client', + items: [], + commandItemLabel: 'Supabase Client for React Router', + }, { title: 'Dropzone', href: '/docs/react-router/dropzone', @@ -118,8 +124,14 @@ export const frameworkPages: Record = { ], }, react: { - title: 'React', + title: 'React SPA', items: [ + { + title: 'Client', + href: '/docs/react/client', + items: [], + commandItemLabel: 'Supabase Client for React SPA', + }, { title: 'Password-Based Auth', href: '/docs/react/password-based-auth', diff --git a/apps/ui-library/content/docs/getting-started/faq.mdx b/apps/ui-library/content/docs/getting-started/faq.mdx index c30faa146c1..b4c7ade3b97 100644 --- a/apps/ui-library/content/docs/getting-started/faq.mdx +++ b/apps/ui-library/content/docs/getting-started/faq.mdx @@ -3,32 +3,59 @@ title: FAQ description: Frequently asked questions --- - - - Will it overwrite what I already have? - Answer + + + How does it work? + + You run a command in a new or existing project that looks something like this: + ```bash + npx shadcn@latest add http://supabase.com/ui/r/password-based-auth-nextjs.json + ``` + This will add all of the files to your project and install the dependencies. + After you've run the command, you can start using the components. In the case of Auth, you should be up and running with user sign in/sign up with just this one command. + + + Will it overwrite my existing files? + + No. If you run a command in an existing project, it will not overwrite your existing + files. If there conflicting files, you will be asked what you want to do (leave them or + overwrite them). If you run it in a new project, it will just add the new files to your project. + + + + I've already started my a project, what should I do? + + You can still use these components, even if you've already started your project! The commands + won't overwrite your existing files, but you should still be careful with the changes to makes + sure you understand what's been added. You should commit your changes into Git before running + these commands so you'll be able to see a clean diff of the changes. + + + + I've already installed shadcn/ui, what should I do? + + You're off to a great start! You can now run an add command to add the desired component to your + project. + + + + + I already started with `npx create-next-app -e with-supabase` what should I do now? + + + Our Next.js quickstart is a great starting point for building a full app with Supabase and + Next.js. If you've already started with this template, you can use add command to add more + components to your project. + + + + I used an AI builder to build my app, what should I do? + + If your app is already built, you can use any of the commands to drop components into your + project. Your app likely already has a Supabase client set up, so you can just continue using + that if it's working well for you. + + - - I've already installed shadcn/ui, what should I do? - Answer - - - - I already started with -e with-supabase what should I do? - - Answer - - - I use pages router - Answer - - - I use app router - Answer - - - I use none of the things mentioned, what can I do? - Answer - diff --git a/apps/ui-library/content/docs/getting-started/introduction.mdx b/apps/ui-library/content/docs/getting-started/introduction.mdx index f71f9b25bcf..69c40b4c2cf 100644 --- a/apps/ui-library/content/docs/getting-started/introduction.mdx +++ b/apps/ui-library/content/docs/getting-started/introduction.mdx @@ -1,10 +1,34 @@ --- title: Introduction -description: Components +description: A flexible, open-source UI component library built on shadcn/ui, designed to simplify Supabase-powered projects with pre-built Auth, Storage, and Realtime features. --- -## Code Block +Our Supabase UI component library is a set of components built on top of the shadcn/ui registry system. It's designed to make building new or existing Supabase-powered projects faster and easier by providing pre-built solutions for common but challenging features—such as authentication, file uploads, and real-time updates. -```js -console.log('Hello World') -``` +Our Supabase UI library aims to solve these issues with: + +- **Extensible components**: Modify and extend simple primitives as needed. + +- **Composable Components**: Modular structure that makes components easy to integrate and combine. + +- **Supabase Clients**: Interact with Supabase however you choose. + +- **Scaffolding for Hard Problems**: Pre-built solutions for challenging features like sign-in/sign-up flows, file uploads, and real-time data sync. + +- **New or existing projects**: Designed to work with brand new projects or to fit easily into existing ones. + +### Extensible components + +Our components are designed to be easily extensible. You can modify the code to fit your needs and use it as a starting point for your own components. You can change any of the code to fit your needs and use it as a starting point for your own components. The important part is that the code is designed to make the hard parts easy, and leave the easy parts for you. + +### Composable components + +Our components are designed to be easily composable. You can combine them to create new components that are tailored to your needs. If you want to change the design or behavior of a component, you can easily do so in your own codebase. + +### Supabase Clients + +The Supabase Clients are the backbone of your apps. They are designed to be easily integrated with your existing Supabase project. We provide both client-side and server-side clients for several major frameworks, and the code is designed to be easily integrated with your existing codebase. + +### Scaffolding for Hard Problems + +Setting up something like Auth should be easy, so we've done the hard work for you. Our components are designed to be easily integrated with your new or existing Supabase projects. Installing them drops the files into place so you can get the hard parts out of the way quickly. diff --git a/apps/ui-library/content/docs/nextjs/client.mdx b/apps/ui-library/content/docs/nextjs/client.mdx index 48d55d8c801..0d5b914818f 100644 --- a/apps/ui-library/content/docs/nextjs/client.mdx +++ b/apps/ui-library/content/docs/nextjs/client.mdx @@ -11,14 +11,22 @@ description: Supabase client for Next.js +## Usage + This block installs a Supabase client to be used by Next.js. It's meant to be used with app router and it's fully SSR compatible. -If you started a project through `npx create-next-app -e with-supabase` command, this block is already included in your project. +If you already have your Supabase client(s) set up and working well (through the through `npx create-next-app -e with-supabase` template or in another way) you can just continue using them. -You need to add an .env file to your project: +To get started, you need to add an `.env` file to your project with the following entries: + +```env NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= +``` If you're using a local instance of supabase, you can find these values by running `supabase start` or `supabase status` (if already running). -If you're using supabase.com project, you can find them on this page https://supabase.com/dashboard/project/_/settings/api or in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks. +If you're using supabase.com project, you can find them in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). + +This block provides Supabase clients designed for use with SSR by storing JWT tokens in cookies. If you're +developing a React SPA, use the [React SPA client](/ui/docs/react-router/client) instead. diff --git a/apps/ui-library/content/docs/nextjs/dropzone.mdx b/apps/ui-library/content/docs/nextjs/dropzone.mdx index c8a9c7b8479..44e009a2f5f 100644 --- a/apps/ui-library/content/docs/nextjs/dropzone.mdx +++ b/apps/ui-library/content/docs/nextjs/dropzone.mdx @@ -27,18 +27,14 @@ The File Upload component is designed to allow users to upload files with specif 1. **Drag-and-Drop**. Support drag-and-drop functionality for adding files. +1. **Multiple Files**. Allow users to upload multiple files at once. + 1. **MIME Type Restriction**. Only allow specific MIME types. 1. **Invalid File Handling**. Allow users to remove invalid files from the list. Display a tooltip with error information for invalid files. 1. **Image Previews**. Display previews for image files. -1. **Error Handling**. Show error messages if the upload fails due to network or server errors. - -1. **Success Handling**. Display a success message and clear the file list upon successful upload. +1. **Success and Error Handling**. Show error messages if the upload fails due to network or server errors, show a success message and clear the file list upon successful upload. 1. **Configurable Limits**. Allow configuration of file size limits and the maximum number of files. - -## Usage - -## Examples diff --git a/apps/ui-library/content/docs/nextjs/password-based-auth.mdx b/apps/ui-library/content/docs/nextjs/password-based-auth.mdx index 092f84e6434..4284410e099 100644 --- a/apps/ui-library/content/docs/nextjs/password-based-auth.mdx +++ b/apps/ui-library/content/docs/nextjs/password-based-auth.mdx @@ -20,13 +20,10 @@ This block includes the [Supabase client](/ui/docs/nextjs/client). When installi After installing the block in your Next.js project, you'll have all needed pages and components to build a password-based authentication flow. -There few more steps to finish the installation: +To finish the installation: -1. Set the `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` environment variables in your `.env` file in your project. -1. Add an [email template for sign-up](https://supabase.com/dashboard/project/uznvnxvfhkixgqetwekc/auth/templates) to the Supabase project. For detailed instructions on how to configure your email - templates, including the use of variables like `{{ .SiteURL }}`,`{{ .TokenHash }}`, and `{{ .RedirectTo }}`, refer to our [Email Templates guide](/docs/email-templates). - - Your signup email template should contain the following HTML: +1. Set the `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` environment variables in your `.env` file in your project. Find them in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true) under App Frameworks. +1. Add an [email template for sign-up](https://supabase.com/dashboard/project/_/auth/templates) to the Supabase project. Your signup email template should contain at least the following HTML: ```html

Confirm your signup

@@ -40,6 +37,10 @@ There few more steps to finish the installation:

``` -1. Set the site URL in the [Supabase dashboard](https://supabase.com/dashboard/project/_/auth/url-configuration). -1. Set the forgot-password route in `forgot-password-form.tsx` component. The route needs to be configured as a redirect URL in the [Supabase dashboard](https://supabase.com/dashboard/project/_/auth/url-configuration). + For detailed instructions on how to configure your email templates, including the use of variables like `{{ .SiteURL }}`,`{{ .TokenHash }}`, and `{{ .RedirectTo }}`, refer to our [Email Templates guide](/docs/email-templates). + +1. Set the site URL in the [Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/url-configuration). +1. Set the `forgot-password` route in `forgot-password-form.tsx` component. The same route also needs to be configured as a redirect URL in the [Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/url-configuration). It should look something like this: `http://example.com/auth/forgot-password`. 1. Update the redirect paths in the `login-form.tsx` and `update-password-form.tsx` components to point to the logged-in routes in your app. + +You can use this block with pages router by simply moving the routes from the `app` folder into the `pages` folder and renaming them. diff --git a/apps/ui-library/content/docs/nextjs/realtime-cursor.mdx b/apps/ui-library/content/docs/nextjs/realtime-cursor.mdx index eefb0b57f15..71a06756f55 100644 --- a/apps/ui-library/content/docs/nextjs/realtime-cursor.mdx +++ b/apps/ui-library/content/docs/nextjs/realtime-cursor.mdx @@ -12,7 +12,7 @@ description: Real-time cursor sharing for collaborative applications ## Folder structure @@ -20,3 +20,9 @@ description: Real-time cursor sharing for collaborative applications ## Introduction + +The Realtime Cursor component is designed to allow users to share their cursor positions with other users in a room. It can be used to build real-time collaborative applications. + +## Usage + +The Realtime Cursor component is designed to be used in a room. It can be used to build real-time collaborative applications. Add the `` component to your page and it will render realtime cursors from other users in the room. diff --git a/apps/ui-library/content/docs/react-router/client.mdx b/apps/ui-library/content/docs/react-router/client.mdx index e7c94c44c14..214297dd5e2 100644 --- a/apps/ui-library/content/docs/react-router/client.mdx +++ b/apps/ui-library/content/docs/react-router/client.mdx @@ -10,3 +10,6 @@ description: Supabase client for React Router ## Folder structure + +This block provides Supabase clients designed for use with SSR by storing JWT tokens in cookies. If you're +developing a React SPA, use the [React SPA client](/ui/docs/react-router/client) instead. diff --git a/apps/ui-library/content/docs/react/client.mdx b/apps/ui-library/content/docs/react/client.mdx index 411abab3e2c..2aa3af05b15 100644 --- a/apps/ui-library/content/docs/react/client.mdx +++ b/apps/ui-library/content/docs/react/client.mdx @@ -1,11 +1,11 @@ --- -title: React -description: Supabase client for React +title: React Single Page Applications +description: Supabase client for React Single Page Applications --- ## Installation - + ## Folder structure diff --git a/apps/ui-library/content/docs/react/password-based-auth.mdx b/apps/ui-library/content/docs/react/password-based-auth.mdx index f4abcebf05d..d39f7a45e17 100644 --- a/apps/ui-library/content/docs/react/password-based-auth.mdx +++ b/apps/ui-library/content/docs/react/password-based-auth.mdx @@ -1,6 +1,6 @@ --- -title: Password-based Auth (React) -description: Password-based Auth block for React +title: Password-based Authentication +description: Password-based authentication block for React Single Page Applications --- ## Installation diff --git a/apps/ui-library/content/docs/tanstack/client.mdx b/apps/ui-library/content/docs/tanstack/client.mdx index 15c5d31b544..7ab51bba1fb 100644 --- a/apps/ui-library/content/docs/tanstack/client.mdx +++ b/apps/ui-library/content/docs/tanstack/client.mdx @@ -16,3 +16,6 @@ The easiest way to use Tanstack Start with Supabase is to follow the following s ## Folder structure + +This block provides Supabase clients designed for use with SSR by storing JWT tokens in cookies. If you're +developing a React SPA, use the [React SPA client](/ui/docs/react-router/client) instead. diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index e726f214a3a..aa11ebc5ef9 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -18,7 +18,7 @@ "dependencies": { "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-progress": "^1.0.3", - "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.0.7", "class-variance-authority": "^0.6.0", "contentlayer2": "0.4.6", diff --git a/apps/ui-library/public/r/dropzone-nextjs.json b/apps/ui-library/public/r/dropzone-nextjs.json index 3cc30ca5191..f6eca7ec8e3 100644 --- a/apps/ui-library/public/r/dropzone-nextjs.json +++ b/apps/ui-library/public/r/dropzone-nextjs.json @@ -17,12 +17,12 @@ "files": [ { "path": "registry/default/blocks/dropzone/components/dropzone.tsx", - "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { CheckCircle, CircleAlert, File, Loader2, Trash, Upload } from 'lucide-react'\nimport { createContext, PropsWithChildren, useCallback, useContext } from 'react'\n\ntype DropzoneContextType = Omit\n\nconst DropzoneContext = createContext(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren) => {\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n restProps.error ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n \n \n \n {children}\n
\n \n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const { error, files, setFiles, onUpload, loading, success } = useDropzoneContext()\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (success) {\n return (\n
\n \n

Files uploaded successfully

\n
\n )\n }\n\n return (\n
\n {files.map((file) => {\n return (\n \n {file.type.startsWith('image/') ? (\n
\n {file.name}\n
\n ) : (\n
\n \n
\n )}\n
\n {file.name}\n {file.errors.length !== 0 && (\n \n \n \n \n {file.errors.map((e) => e.message).join(', ')}\n \n )}\n
\n\n handleRemoveFile(file.name)}\n >\n \n \n
\n )\n })}\n {files.length > 0 && (\n
\n file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n \n Uploading...\n \n ) : (\n <>Upload\n )}\n \n
\n )}\n {error &&

{error}

}\n
\n )\n}\n\nconst DropzoneEmptyState = ({ className }: { className?: string }) => {\n const { files, maxFiles, maxFileSize, inputRef, success } = useDropzoneContext()\n\n if ((maxFiles !== 0 && files.length >= maxFiles) || success) {\n return null\n }\n\n return (\n
\n
\n \n
\n

\n Add or{' '}\n inputRef.current?.click()} className=\"underline cursor-pointer\">\n drop {maxFiles === 1 ? `file` : 'files'}\n \n

\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n

Maximum file size: {maxFileSize} MB

\n )}\n
\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", + "content": "'use client'\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'\nimport { CheckCircle, File, Loader2, Upload, X } from 'lucide-react'\nimport { createContext, type PropsWithChildren, useCallback, useContext } from 'react'\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\n\nconst DropzoneContext = createContext(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren) => {\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 \n \n \n {children}\n
\n \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
\n \n

\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n

\n
\n )\n }\n\n return (\n
\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 \n {file.type.startsWith('image/') ? (\n
\n {file.name}\n
\n ) : (\n
\n \n
\n )}\n\n
\n

\n {file.name}\n

\n {file.errors.length > 0 ? (\n

\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

\n ) : loading && !isSuccessfullyUploaded ? (\n

Uploading file...

\n ) : !!fileError ? (\n

Failed to upload: {fileError.message}

\n ) : isSuccessfullyUploaded ? (\n

Successfully uploaded file

\n ) : (\n

{formatBytes(file.size, 2)}

\n )}\n
\n\n {!loading && !isSuccessfullyUploaded && (\n handleRemoveFile(file.name)}\n >\n \n \n )}\n
\n )\n })}\n {exceedMaxFiles && (\n

\n You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file\n {files.length - maxFiles > 1 ? 's' : ''}.\n

\n )}\n {files.length > 0 && !exceedMaxFiles && (\n
\n file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n \n Uploading...\n \n ) : (\n <>Upload files\n )}\n \n
\n )}\n
\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
\n \n

\n Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file\n {!maxFiles || maxFiles > 1 ? 's' : ''}\n

\n
\n

\n Drag and drop or{' '}\n inputRef.current?.click()}\n className=\"underline cursor-pointer transition hover:text-foreground\"\n >\n select {maxFiles === 1 ? `file` : 'files'}\n {' '}\n to upload\n

\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n

\n Maximum file size: {formatBytes(maxFileSize, 2)}\n

\n )}\n
\n
\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 { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useState } from 'react'\nimport { FileError, FileRejection, useDropzone } from 'react-dropzone'\n\nconst supabase = createClient()\n\ninterface FileWithPreview extends File {\n preview?: string\n errors: readonly FileError[]\n}\n\ntype UseSupabaseUploadOptions = {\n bucketName: string\n path: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n}\n\ntype UseSupabaseUploadReturn = ReturnType\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 0,\n } = options\n\n const [files, setFiles] = useState([])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState(null)\n const [success, setSuccess] = useState(false)\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles.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 setError(null)\n setLoading(true)\n await Promise.all(\n files.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(`${path}/${file.name}`, file, {\n cacheControl: '3600',\n upsert: false,\n })\n if (error) {\n setError(error.message)\n } else {\n setSuccess(true)\n }\n })\n )\n setLoading(false)\n }, [files, path, bucketName, setError, setLoading])\n\n useEffect(() => {\n if (files.length === 0) {\n setError(null)\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 success,\n loading,\n error,\n setError,\n onUpload,\n maxFileSize: maxFileSize,\n maxFiles: maxFiles,\n ...dropzoneProps,\n }\n}\n\nexport { useSupabaseUpload, type UseSupabaseUploadOptions, type UseSupabaseUploadReturn }\n", + "content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { type FileError, type FileRejection, useDropzone } from 'react-dropzone'\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= 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\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([])\n const [loading, setLoading] = useState(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState([])\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" }, { diff --git a/apps/ui-library/public/r/dropzone-react-router.json b/apps/ui-library/public/r/dropzone-react-router.json index 6f8e1aa3446..6b4e6dc66f9 100644 --- a/apps/ui-library/public/r/dropzone-react-router.json +++ b/apps/ui-library/public/r/dropzone-react-router.json @@ -17,12 +17,12 @@ "files": [ { "path": "registry/default/blocks/dropzone/components/dropzone.tsx", - "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { CheckCircle, CircleAlert, File, Loader2, Trash, Upload } from 'lucide-react'\nimport { createContext, PropsWithChildren, useCallback, useContext } from 'react'\n\ntype DropzoneContextType = Omit\n\nconst DropzoneContext = createContext(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren) => {\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n restProps.error ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n \n \n \n {children}\n \n \n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const { error, files, setFiles, onUpload, loading, success } = useDropzoneContext()\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (success) {\n return (\n
\n \n

Files uploaded successfully

\n
\n )\n }\n\n return (\n
\n {files.map((file) => {\n return (\n \n {file.type.startsWith('image/') ? (\n
\n {file.name}\n
\n ) : (\n
\n \n
\n )}\n
\n {file.name}\n {file.errors.length !== 0 && (\n \n \n \n \n {file.errors.map((e) => e.message).join(', ')}\n \n )}\n
\n\n handleRemoveFile(file.name)}\n >\n \n \n
\n )\n })}\n {files.length > 0 && (\n
\n file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n \n Uploading...\n \n ) : (\n <>Upload\n )}\n \n
\n )}\n {error &&

{error}

}\n \n )\n}\n\nconst DropzoneEmptyState = ({ className }: { className?: string }) => {\n const { files, maxFiles, maxFileSize, inputRef, success } = useDropzoneContext()\n\n if ((maxFiles !== 0 && files.length >= maxFiles) || success) {\n return null\n }\n\n return (\n
\n
\n \n
\n

\n Add or{' '}\n inputRef.current?.click()} className=\"underline cursor-pointer\">\n drop {maxFiles === 1 ? `file` : 'files'}\n \n

\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n

Maximum file size: {maxFileSize} MB

\n )}\n
\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", + "content": "'use client'\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'\nimport { CheckCircle, File, Loader2, Upload, X } from 'lucide-react'\nimport { createContext, type PropsWithChildren, useCallback, useContext } from 'react'\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\n\nconst DropzoneContext = createContext(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren) => {\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 \n \n \n {children}\n \n \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
\n \n

\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n

\n
\n )\n }\n\n return (\n
\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 \n {file.type.startsWith('image/') ? (\n
\n {file.name}\n
\n ) : (\n
\n \n
\n )}\n\n
\n

\n {file.name}\n

\n {file.errors.length > 0 ? (\n

\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

\n ) : loading && !isSuccessfullyUploaded ? (\n

Uploading file...

\n ) : !!fileError ? (\n

Failed to upload: {fileError.message}

\n ) : isSuccessfullyUploaded ? (\n

Successfully uploaded file

\n ) : (\n

{formatBytes(file.size, 2)}

\n )}\n
\n\n {!loading && !isSuccessfullyUploaded && (\n handleRemoveFile(file.name)}\n >\n \n \n )}\n
\n )\n })}\n {exceedMaxFiles && (\n

\n You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file\n {files.length - maxFiles > 1 ? 's' : ''}.\n

\n )}\n {files.length > 0 && !exceedMaxFiles && (\n
\n file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n \n Uploading...\n \n ) : (\n <>Upload files\n )}\n \n
\n )}\n \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
\n \n

\n Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file\n {!maxFiles || maxFiles > 1 ? 's' : ''}\n

\n
\n

\n Drag and drop or{' '}\n inputRef.current?.click()}\n className=\"underline cursor-pointer transition hover:text-foreground\"\n >\n select {maxFiles === 1 ? `file` : 'files'}\n {' '}\n to upload\n

\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n

\n Maximum file size: {formatBytes(maxFileSize, 2)}\n

\n )}\n
\n
\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 { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useState } from 'react'\nimport { FileError, FileRejection, useDropzone } from 'react-dropzone'\n\nconst supabase = createClient()\n\ninterface FileWithPreview extends File {\n preview?: string\n errors: readonly FileError[]\n}\n\ntype UseSupabaseUploadOptions = {\n bucketName: string\n path: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n}\n\ntype UseSupabaseUploadReturn = ReturnType\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 0,\n } = options\n\n const [files, setFiles] = useState([])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState(null)\n const [success, setSuccess] = useState(false)\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles.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 setError(null)\n setLoading(true)\n await Promise.all(\n files.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(`${path}/${file.name}`, file, {\n cacheControl: '3600',\n upsert: false,\n })\n if (error) {\n setError(error.message)\n } else {\n setSuccess(true)\n }\n })\n )\n setLoading(false)\n }, [files, path, bucketName, setError, setLoading])\n\n useEffect(() => {\n if (files.length === 0) {\n setError(null)\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 success,\n loading,\n error,\n setError,\n onUpload,\n maxFileSize: maxFileSize,\n maxFiles: maxFiles,\n ...dropzoneProps,\n }\n}\n\nexport { useSupabaseUpload, type UseSupabaseUploadOptions, type UseSupabaseUploadReturn }\n", + "content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { type FileError, type FileRejection, useDropzone } from 'react-dropzone'\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= 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\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([])\n const [loading, setLoading] = useState(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState([])\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" }, { diff --git a/apps/ui-library/public/r/dropzone-react.json b/apps/ui-library/public/r/dropzone-react.json index 496fb4bc8ea..e43f655c85e 100644 --- a/apps/ui-library/public/r/dropzone-react.json +++ b/apps/ui-library/public/r/dropzone-react.json @@ -17,12 +17,12 @@ "files": [ { "path": "registry/default/blocks/dropzone/components/dropzone.tsx", - "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { CheckCircle, CircleAlert, File, Loader2, Trash, Upload } from 'lucide-react'\nimport { createContext, PropsWithChildren, useCallback, useContext } from 'react'\n\ntype DropzoneContextType = Omit\n\nconst DropzoneContext = createContext(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren) => {\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n restProps.error ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n \n \n \n {children}\n \n \n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const { error, files, setFiles, onUpload, loading, success } = useDropzoneContext()\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (success) {\n return (\n
\n \n

Files uploaded successfully

\n
\n )\n }\n\n return (\n
\n {files.map((file) => {\n return (\n \n {file.type.startsWith('image/') ? (\n
\n {file.name}\n
\n ) : (\n
\n \n
\n )}\n
\n {file.name}\n {file.errors.length !== 0 && (\n \n \n \n \n {file.errors.map((e) => e.message).join(', ')}\n \n )}\n
\n\n handleRemoveFile(file.name)}\n >\n \n \n
\n )\n })}\n {files.length > 0 && (\n
\n file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n \n Uploading...\n \n ) : (\n <>Upload\n )}\n \n
\n )}\n {error &&

{error}

}\n \n )\n}\n\nconst DropzoneEmptyState = ({ className }: { className?: string }) => {\n const { files, maxFiles, maxFileSize, inputRef, success } = useDropzoneContext()\n\n if ((maxFiles !== 0 && files.length >= maxFiles) || success) {\n return null\n }\n\n return (\n
\n
\n \n
\n

\n Add or{' '}\n inputRef.current?.click()} className=\"underline cursor-pointer\">\n drop {maxFiles === 1 ? `file` : 'files'}\n \n

\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n

Maximum file size: {maxFileSize} MB

\n )}\n
\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", + "content": "'use client'\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'\nimport { CheckCircle, File, Loader2, Upload, X } from 'lucide-react'\nimport { createContext, type PropsWithChildren, useCallback, useContext } from 'react'\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\n\nconst DropzoneContext = createContext(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren) => {\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 \n \n \n {children}\n \n \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
\n \n

\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n

\n
\n )\n }\n\n return (\n
\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 \n {file.type.startsWith('image/') ? (\n
\n {file.name}\n
\n ) : (\n
\n \n
\n )}\n\n
\n

\n {file.name}\n

\n {file.errors.length > 0 ? (\n

\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

\n ) : loading && !isSuccessfullyUploaded ? (\n

Uploading file...

\n ) : !!fileError ? (\n

Failed to upload: {fileError.message}

\n ) : isSuccessfullyUploaded ? (\n

Successfully uploaded file

\n ) : (\n

{formatBytes(file.size, 2)}

\n )}\n
\n\n {!loading && !isSuccessfullyUploaded && (\n handleRemoveFile(file.name)}\n >\n \n \n )}\n
\n )\n })}\n {exceedMaxFiles && (\n

\n You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file\n {files.length - maxFiles > 1 ? 's' : ''}.\n

\n )}\n {files.length > 0 && !exceedMaxFiles && (\n
\n file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n \n Uploading...\n \n ) : (\n <>Upload files\n )}\n \n
\n )}\n \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
\n \n

\n Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file\n {!maxFiles || maxFiles > 1 ? 's' : ''}\n

\n
\n

\n Drag and drop or{' '}\n inputRef.current?.click()}\n className=\"underline cursor-pointer transition hover:text-foreground\"\n >\n select {maxFiles === 1 ? `file` : 'files'}\n {' '}\n to upload\n

\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n

\n Maximum file size: {formatBytes(maxFileSize, 2)}\n

\n )}\n
\n
\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 { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useState } from 'react'\nimport { FileError, FileRejection, useDropzone } from 'react-dropzone'\n\nconst supabase = createClient()\n\ninterface FileWithPreview extends File {\n preview?: string\n errors: readonly FileError[]\n}\n\ntype UseSupabaseUploadOptions = {\n bucketName: string\n path: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n}\n\ntype UseSupabaseUploadReturn = ReturnType\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 0,\n } = options\n\n const [files, setFiles] = useState([])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState(null)\n const [success, setSuccess] = useState(false)\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles.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 setError(null)\n setLoading(true)\n await Promise.all(\n files.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(`${path}/${file.name}`, file, {\n cacheControl: '3600',\n upsert: false,\n })\n if (error) {\n setError(error.message)\n } else {\n setSuccess(true)\n }\n })\n )\n setLoading(false)\n }, [files, path, bucketName, setError, setLoading])\n\n useEffect(() => {\n if (files.length === 0) {\n setError(null)\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 success,\n loading,\n error,\n setError,\n onUpload,\n maxFileSize: maxFileSize,\n maxFiles: maxFiles,\n ...dropzoneProps,\n }\n}\n\nexport { useSupabaseUpload, type UseSupabaseUploadOptions, type UseSupabaseUploadReturn }\n", + "content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { type FileError, type FileRejection, useDropzone } from 'react-dropzone'\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= 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\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([])\n const [loading, setLoading] = useState(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState([])\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" }, { diff --git a/apps/ui-library/public/r/dropzone-tanstack.json b/apps/ui-library/public/r/dropzone-tanstack.json index da0de6cb718..76898f4264b 100644 --- a/apps/ui-library/public/r/dropzone-tanstack.json +++ b/apps/ui-library/public/r/dropzone-tanstack.json @@ -18,12 +18,12 @@ "files": [ { "path": "registry/default/blocks/dropzone/components/dropzone.tsx", - "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\nimport { CheckCircle, CircleAlert, File, Loader2, Trash, Upload } from 'lucide-react'\nimport { createContext, PropsWithChildren, useCallback, useContext } from 'react'\n\ntype DropzoneContextType = Omit\n\nconst DropzoneContext = createContext(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren) => {\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n restProps.error ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n \n \n \n {children}\n \n \n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const { error, files, setFiles, onUpload, loading, success } = useDropzoneContext()\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (success) {\n return (\n
\n \n

Files uploaded successfully

\n
\n )\n }\n\n return (\n
\n {files.map((file) => {\n return (\n \n {file.type.startsWith('image/') ? (\n
\n {file.name}\n
\n ) : (\n
\n \n
\n )}\n
\n {file.name}\n {file.errors.length !== 0 && (\n \n \n \n \n {file.errors.map((e) => e.message).join(', ')}\n \n )}\n
\n\n handleRemoveFile(file.name)}\n >\n \n \n
\n )\n })}\n {files.length > 0 && (\n
\n file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n \n Uploading...\n \n ) : (\n <>Upload\n )}\n \n
\n )}\n {error &&

{error}

}\n \n )\n}\n\nconst DropzoneEmptyState = ({ className }: { className?: string }) => {\n const { files, maxFiles, maxFileSize, inputRef, success } = useDropzoneContext()\n\n if ((maxFiles !== 0 && files.length >= maxFiles) || success) {\n return null\n }\n\n return (\n
\n
\n \n
\n

\n Add or{' '}\n inputRef.current?.click()} className=\"underline cursor-pointer\">\n drop {maxFiles === 1 ? `file` : 'files'}\n \n

\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n

Maximum file size: {maxFileSize} MB

\n )}\n
\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", + "content": "'use client'\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'\nimport { CheckCircle, File, Loader2, Upload, X } from 'lucide-react'\nimport { createContext, type PropsWithChildren, useCallback, useContext } from 'react'\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\n\nconst DropzoneContext = createContext(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren) => {\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 \n \n \n {children}\n \n \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
\n \n

\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n

\n
\n )\n }\n\n return (\n
\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 \n {file.type.startsWith('image/') ? (\n
\n {file.name}\n
\n ) : (\n
\n \n
\n )}\n\n
\n

\n {file.name}\n

\n {file.errors.length > 0 ? (\n

\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

\n ) : loading && !isSuccessfullyUploaded ? (\n

Uploading file...

\n ) : !!fileError ? (\n

Failed to upload: {fileError.message}

\n ) : isSuccessfullyUploaded ? (\n

Successfully uploaded file

\n ) : (\n

{formatBytes(file.size, 2)}

\n )}\n
\n\n {!loading && !isSuccessfullyUploaded && (\n handleRemoveFile(file.name)}\n >\n \n \n )}\n
\n )\n })}\n {exceedMaxFiles && (\n

\n You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file\n {files.length - maxFiles > 1 ? 's' : ''}.\n

\n )}\n {files.length > 0 && !exceedMaxFiles && (\n
\n file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n \n Uploading...\n \n ) : (\n <>Upload files\n )}\n \n
\n )}\n \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
\n \n

\n Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file\n {!maxFiles || maxFiles > 1 ? 's' : ''}\n

\n
\n

\n Drag and drop or{' '}\n inputRef.current?.click()}\n className=\"underline cursor-pointer transition hover:text-foreground\"\n >\n select {maxFiles === 1 ? `file` : 'files'}\n {' '}\n to upload\n

\n {maxFileSize !== Number.POSITIVE_INFINITY && (\n

\n Maximum file size: {formatBytes(maxFileSize, 2)}\n

\n )}\n
\n
\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 { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useState } from 'react'\nimport { FileError, FileRejection, useDropzone } from 'react-dropzone'\n\nconst supabase = createClient()\n\ninterface FileWithPreview extends File {\n preview?: string\n errors: readonly FileError[]\n}\n\ntype UseSupabaseUploadOptions = {\n bucketName: string\n path: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n}\n\ntype UseSupabaseUploadReturn = ReturnType\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 0,\n } = options\n\n const [files, setFiles] = useState([])\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState(null)\n const [success, setSuccess] = useState(false)\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles.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 setError(null)\n setLoading(true)\n await Promise.all(\n files.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(`${path}/${file.name}`, file, {\n cacheControl: '3600',\n upsert: false,\n })\n if (error) {\n setError(error.message)\n } else {\n setSuccess(true)\n }\n })\n )\n setLoading(false)\n }, [files, path, bucketName, setError, setLoading])\n\n useEffect(() => {\n if (files.length === 0) {\n setError(null)\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 success,\n loading,\n error,\n setError,\n onUpload,\n maxFileSize: maxFileSize,\n maxFiles: maxFiles,\n ...dropzoneProps,\n }\n}\n\nexport { useSupabaseUpload, type UseSupabaseUploadOptions, type UseSupabaseUploadReturn }\n", + "content": "import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { type FileError, type FileRejection, useDropzone } from 'react-dropzone'\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= 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\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([])\n const [loading, setLoading] = useState(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState([])\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" }, { diff --git a/apps/ui-library/public/r/realtime-cursor-nextjs.json b/apps/ui-library/public/r/realtime-cursor-nextjs.json index 779bf350660..0922f7689ec 100644 --- a/apps/ui-library/public/r/realtime-cursor-nextjs.json +++ b/apps/ui-library/public/r/realtime-cursor-nextjs.json @@ -17,7 +17,7 @@ }, { "path": "registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx", - "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n const { cursors } = useRealtimeCursors({ roomName, username })\n\n return (\n
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\n )\n}\n", + "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n const { cursors } = useRealtimeCursors({ roomName, username })\n\n return (\n
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/realtime-cursor-react-router.json b/apps/ui-library/public/r/realtime-cursor-react-router.json index 86222b8e45e..db72aa9261a 100644 --- a/apps/ui-library/public/r/realtime-cursor-react-router.json +++ b/apps/ui-library/public/r/realtime-cursor-react-router.json @@ -17,7 +17,7 @@ }, { "path": "registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx", - "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n const { cursors } = useRealtimeCursors({ roomName, username })\n\n return (\n
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\n )\n}\n", + "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n const { cursors } = useRealtimeCursors({ roomName, username })\n\n return (\n
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/realtime-cursor-react.json b/apps/ui-library/public/r/realtime-cursor-react.json index 548fce3bb73..e0a0327a068 100644 --- a/apps/ui-library/public/r/realtime-cursor-react.json +++ b/apps/ui-library/public/r/realtime-cursor-react.json @@ -17,7 +17,7 @@ }, { "path": "registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx", - "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n const { cursors } = useRealtimeCursors({ roomName, username })\n\n return (\n
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\n )\n}\n", + "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n const { cursors } = useRealtimeCursors({ roomName, username })\n\n return (\n
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/realtime-cursor-tanstack.json b/apps/ui-library/public/r/realtime-cursor-tanstack.json index aeef652a424..3417beca751 100644 --- a/apps/ui-library/public/r/realtime-cursor-tanstack.json +++ b/apps/ui-library/public/r/realtime-cursor-tanstack.json @@ -18,7 +18,7 @@ }, { "path": "registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx", - "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n const { cursors } = useRealtimeCursors({ roomName, username })\n\n return (\n
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\n )\n}\n", + "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n const { cursors } = useRealtimeCursors({ roomName, username })\n\n return (\n
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/registry/default/blocks/dropzone/components/dropzone.tsx b/apps/ui-library/registry/default/blocks/dropzone/components/dropzone.tsx index 7004b3889ad..f60521afd9c 100644 --- a/apps/ui-library/registry/default/blocks/dropzone/components/dropzone.tsx +++ b/apps/ui-library/registry/default/blocks/dropzone/components/dropzone.tsx @@ -1,11 +1,24 @@ 'use client' import { cn } from '@/lib/utils' -import { UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload' +import { type UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload' import { Button } from '@/registry/default/components/ui/button' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip' -import { CheckCircle, CircleAlert, File, Loader2, Trash, Upload } from 'lucide-react' -import { createContext, PropsWithChildren, useCallback, useContext } from 'react' +import { CheckCircle, File, Loader2, Upload, X } from 'lucide-react' +import { createContext, type PropsWithChildren, useCallback, useContext } from 'react' + +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 @@ -22,10 +35,11 @@ const Dropzone = ({ getInputProps, ...restProps }: PropsWithChildren) => { + const isSuccess = restProps.isSuccess const isActive = restProps.isDragActive const isInvalid = (restProps.isDragActive && restProps.isDragReject) || - restProps.error || + (restProps.errors.length > 0 && !restProps.isSuccess) || restProps.files.some((file) => file.errors.length !== 0) return ( @@ -33,8 +47,9 @@ const Dropzone = ({
{ - const { error, files, setFiles, onUpload, loading, success } = useDropzoneContext() + const { + files, + setFiles, + onUpload, + loading, + successes, + errors, + maxFileSize, + maxFiles, + isSuccess, + } = useDropzoneContext() + + const exceedMaxFiles = files.length > maxFiles const handleRemoveFile = useCallback( (fileName: string) => { @@ -56,59 +83,86 @@ const DropzoneContent = ({ className }: { className?: string }) => { [files, setFiles] ) - if (success) { + if (isSuccess) { return ( -
- -

Files uploaded successfully

+
+ +

+ Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''} +

) } return (
- {files.map((file) => { + {files.map((file, idx) => { + const fileError = errors.find((e) => e.name === file.name) + const isSuccessfullyUploaded = !!successes.find((e) => e === file.name) + return (
{file.type.startsWith('image/') ? ( -
+
{file.name}
) : ( -
- +
+
)} -
- {file.name} - {file.errors.length !== 0 && ( - - - - - {file.errors.map((e) => e.message).join(', ')} - + +
+

+ {file.name} +

+ {file.errors.length > 0 ? ( +

+ {file.errors + .map((e) => + e.message.startsWith('File is larger than') + ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})` + : e.message + ) + .join(', ')} +

+ ) : loading && !isSuccessfullyUploaded ? ( +

Uploading file...

+ ) : !!fileError ? ( +

Failed to upload: {fileError.message}

+ ) : isSuccessfullyUploaded ? ( +

Successfully uploaded file

+ ) : ( +

{formatBytes(file.size, 2)}

)}
- + {!loading && !isSuccessfullyUploaded && ( + + )}
) })} - {files.length > 0 && ( + {exceedMaxFiles && ( +

+ You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file + {files.length - maxFiles > 1 ? 's' : ''}. +

+ )} + {files.length > 0 && !exceedMaxFiles && (
)} - {error &&

{error}

}
) } const DropzoneEmptyState = ({ className }: { className?: string }) => { - const { files, maxFiles, maxFileSize, inputRef, success } = useDropzoneContext() + const { maxFiles, maxFileSize, inputRef, isSuccess } = useDropzoneContext() - if ((maxFiles !== 0 && files.length >= maxFiles) || success) { + if (isSuccess) { return null } return ( -
-
- -
-

- Add or{' '} - inputRef.current?.click()} className="underline cursor-pointer"> - drop {maxFiles === 1 ? `file` : 'files'} - +

+ +

+ Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file + {!maxFiles || maxFiles > 1 ? 's' : ''}

- {maxFileSize !== Number.POSITIVE_INFINITY && ( -

Maximum file size: {maxFileSize} MB

- )} +
+

+ Drag and drop or{' '} + inputRef.current?.click()} + className="underline cursor-pointer transition hover:text-foreground" + > + select {maxFiles === 1 ? `file` : 'files'} + {' '} + to upload +

+ {maxFileSize !== Number.POSITIVE_INFINITY && ( +

+ Maximum file size: {formatBytes(maxFileSize, 2)} +

+ )} +
) } diff --git a/apps/ui-library/registry/default/blocks/dropzone/hooks/use-supabase-upload.ts b/apps/ui-library/registry/default/blocks/dropzone/hooks/use-supabase-upload.ts index 437218d495c..113f6f75653 100644 --- a/apps/ui-library/registry/default/blocks/dropzone/hooks/use-supabase-upload.ts +++ b/apps/ui-library/registry/default/blocks/dropzone/hooks/use-supabase-upload.ts @@ -1,6 +1,6 @@ import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client' -import { useCallback, useEffect, useState } from 'react' -import { FileError, FileRejection, useDropzone } from 'react-dropzone' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { type FileError, type FileRejection, useDropzone } from 'react-dropzone' const supabase = createClient() @@ -10,11 +10,44 @@ interface FileWithPreview extends File { } type UseSupabaseUploadOptions = { + /** + * Name of bucket to upload files to in your Supabase project + */ bucketName: string - path: 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= 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 } type UseSupabaseUploadReturn = ReturnType @@ -25,21 +58,35 @@ const useSupabaseUpload = (options: UseSupabaseUploadOptions) => { path, allowedMimeTypes = [], maxFileSize = Number.POSITIVE_INFINITY, - maxFiles = 0, + maxFiles = 1, + cacheControl = 3600, + upsert = false, } = options const [files, setFiles] = useState([]) const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [success, setSuccess] = useState(false) + const [errors, setErrors] = useState<{ name: string; message: string }[]>([]) + const [successes, setSuccesses] = useState([]) + + 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.map((file) => { - ;(file as FileWithPreview).preview = URL.createObjectURL(file) - ;(file as FileWithPreview).errors = [] - return file as FileWithPreview - }) + 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) @@ -64,29 +111,51 @@ const useSupabaseUpload = (options: UseSupabaseUploadOptions) => { }) const onUpload = useCallback(async () => { - setError(null) setLoading(true) - await Promise.all( - files.map(async (file) => { + + // [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 { error } = await supabase.storage .from(bucketName) - .upload(`${path}/${file.name}`, file, { - cacheControl: '3600', - upsert: false, + .upload(!!path ? `${path}/${file.name}` : file.name, file, { + cacheControl: cacheControl.toString(), + upsert, }) if (error) { - setError(error.message) + return { name: file.name, message: error.message } } else { - setSuccess(true) + return { name: file.name, message: undefined } } }) ) + + 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) + setLoading(false) - }, [files, path, bucketName, setError, setLoading]) + }, [files, path, bucketName, errors, successes]) useEffect(() => { if (files.length === 0) { - setError(null) + setErrors([]) } // If the number of files doesn't exceed the maxFiles parameter, remove the error 'Too many files' from each file @@ -108,13 +177,15 @@ const useSupabaseUpload = (options: UseSupabaseUploadOptions) => { return { files, setFiles, - success, + successes, + isSuccess, loading, - error, - setError, + errors, + setErrors, onUpload, maxFileSize: maxFileSize, maxFiles: maxFiles, + allowedMimeTypes, ...dropzoneProps, } } diff --git a/apps/ui-library/registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx b/apps/ui-library/registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx index a0db3751b45..70e5b576bce 100644 --- a/apps/ui-library/registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx +++ b/apps/ui-library/registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx @@ -11,7 +11,7 @@ export const RealtimeCursors = ({ roomName, username }: { roomName: string; user {Object.keys(cursors).map((id) => ( { path: 'test', allowedMimeTypes: ['image/*'], maxFiles: 2, + maxFileSize: 1000 * 1000 * 10, // 10MB, }) return ( -
+
diff --git a/apps/www/pages/legal/dpa.tsx b/apps/www/pages/legal/dpa.tsx index 156d0276f51..e8f7c7d5009 100644 --- a/apps/www/pages/legal/dpa.tsx +++ b/apps/www/pages/legal/dpa.tsx @@ -30,7 +30,7 @@ const DPA = () => { resetForm() setFormSubmitted(true) - window.open('https://supabase.com/downloads/docs/Supabase+DPA+231211.pdf', '_blank') + window.open('https://supabase.com/downloads/docs/Supabase+DPA+250314.pdf', '_blank') } catch (error: any) { setError(error.message) } diff --git a/apps/www/public/downloads/docs/Supabase+DPA+231211.pdf b/apps/www/public/downloads/docs/Supabase+DPA+231211.pdf deleted file mode 100644 index f54f33f1dce..00000000000 Binary files a/apps/www/public/downloads/docs/Supabase+DPA+231211.pdf and /dev/null differ diff --git a/apps/www/public/downloads/docs/Supabase+DPA+250314.pdf b/apps/www/public/downloads/docs/Supabase+DPA+250314.pdf new file mode 100644 index 00000000000..a746a27ff81 Binary files /dev/null and b/apps/www/public/downloads/docs/Supabase+DPA+250314.pdf differ diff --git a/apps/www/public/downloads/docs/Supabase+TIA+241030.pdf b/apps/www/public/downloads/docs/Supabase+TIA+241030.pdf deleted file mode 100644 index 755dea7bb5d..00000000000 Binary files a/apps/www/public/downloads/docs/Supabase+TIA+241030.pdf and /dev/null differ diff --git a/apps/www/public/downloads/docs/Supabase+TIA+250314.pdf b/apps/www/public/downloads/docs/Supabase+TIA+250314.pdf new file mode 100644 index 00000000000..c065f68a356 Binary files /dev/null and b/apps/www/public/downloads/docs/Supabase+TIA+250314.pdf differ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6bf9eae7598..8fea15b886c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,7 +11,7 @@ services: studio: container_name: supabase-studio - image: supabase/studio:20250224-d10db0f + image: supabase/studio:20250317-6955350 restart: unless-stopped healthcheck: test: @@ -186,7 +186,7 @@ services: realtime: # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain container_name: realtime-dev.supabase-realtime - image: supabase/realtime:v2.34.40 + image: supabase/realtime:v2.34.43 restart: unless-stopped depends_on: db: @@ -296,7 +296,7 @@ services: meta: container_name: supabase-meta - image: supabase/postgres-meta:v0.86.1 + image: supabase/postgres-meta:v0.87.1 restart: unless-stopped depends_on: db: @@ -314,7 +314,7 @@ services: functions: container_name: supabase-edge-functions - image: supabase/edge-runtime:v1.67.2 + image: supabase/edge-runtime:v1.67.4 restart: unless-stopped volumes: - ./volumes/functions:/home/deno/functions:Z @@ -338,7 +338,7 @@ services: analytics: container_name: supabase-analytics - image: supabase/logflare:1.12.5 + image: supabase/logflare:1.12.0 restart: unless-stopped ports: - 4000:4000 @@ -386,7 +386,7 @@ services: # Comment out everything below this point if you are using an external Postgres database db: container_name: supabase-db - image: supabase/postgres:15.8.1.049 + image: supabase/postgres:15.8.1.060 restart: unless-stopped volumes: - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z @@ -474,7 +474,7 @@ services: # Update the DATABASE_URL if you are using an external Postgres database supavisor: container_name: supabase-pooler - image: supabase/supavisor:2.4.12 + image: supabase/supavisor:2.4.14 restart: unless-stopped ports: - ${POSTGRES_PORT}:5432 diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 31f60b30510..d2f9189100f 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -1643,6 +1643,9 @@ export interface components { * @enum {string} */ release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview' + secrets?: { + [key: string]: string + } } CreateOrganizationV1Dto: { name: string diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 43ac6c943a1..22cf13f82ea 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -3965,7 +3965,17 @@ export interface components { schemas: { AccessControlPermission: { actions: string[] | null - condition: Record + condition: + | ( + | string + | number + | boolean + | unknown[] + | { + [key: string]: unknown + } + ) + | null organization_id: number | null organization_slug: string project_ids: number[] | null @@ -4032,7 +4042,7 @@ export interface components { role_scoped_projects?: string[] } AuditLogsResponse: { - result: Record[] + result: unknown[] retention_period: number } AuthBackupAdminLintResponseDto: { @@ -4134,13 +4144,16 @@ export interface components { } BillingCustomerUpdateBody: { additional_emails?: string[] - address?: components['schemas']['CustomerBillingAddress'] + address?: { + city?: string + country: string + line1: string + line2?: string + postal_code?: string + state?: string + } } /** @enum {string} */ - BillingPlanChangeType: 'upgrade' | 'downgrade' | 'none' - /** @enum {string} */ - BillingPlanEffectiveAt: 'now' | 'end_of_billing_period' | 'none' - /** @enum {string} */ BillingPlanId: 'free' | 'pro' | 'team' | 'enterprise' BillingProjectAddonResponse: { addons: components['schemas']['SelectedAddonResponse'][] @@ -4379,7 +4392,7 @@ export interface components { payment_method?: string size?: string /** @enum {string} */ - tier: 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise' + tier: 'tier_free' | 'tier_pro' | 'tier_payg' | 'tier_team' | 'tier_enterprise' } CreatePipelineResponse: { id: number @@ -4494,8 +4507,8 @@ export interface components { id: number } CreateStorageBucketBody: { - allowed_mime_types: string[] - file_size_limit: number + allowed_mime_types?: string[] + file_size_limit?: number id: string public: boolean } @@ -4562,7 +4575,9 @@ export interface components { email_change_token_new?: string email_confirmed_at?: string encrypted_password?: string + /** Format: uuid */ id?: string + /** Format: uuid */ instance_id?: string invited_at?: string is_anonymous?: boolean @@ -4574,8 +4589,12 @@ export interface components { phone_change_sent_at?: string phone_change_token?: string phone_confirmed_at?: string - raw_app_meta_data?: Record - raw_user_meta_data?: Record + raw_app_meta_data?: { + [key: string]: unknown + } + raw_user_meta_data?: { + [key: string]: unknown + } reauthentication_sent_at?: string reauthentication_token?: string recovery_sent_at?: string @@ -4622,17 +4641,16 @@ export interface components { | 'requires_payment_method' | 'succeeded' } - CustomerBillingAddress: { - city?: string - country: string - line1: string - line2?: string - postal_code?: string - state?: string - } CustomerResponse: { additional_emails: string[] | null - address: components['schemas']['CustomerBillingAddress'] | null + address?: { + city?: string + country: string + line1: string + line2?: string + postal_code?: string + state?: string + } balance: number billing_via_partner: boolean email: string @@ -4823,14 +4841,21 @@ export interface components { fileUrl: string } DownloadObjectBody: { - options?: components['schemas']['DownloadObjectOptions'] + options?: { + download?: boolean + downloadName?: string + transform?: { + /** @enum {string} */ + format?: 'origin' + height?: number + quality?: number + /** @enum {string} */ + resize?: 'cover' | 'contain' | 'fill' + width?: number + } + } path: string } - DownloadObjectOptions: { - download?: boolean - downloadName?: string - transform?: components['schemas']['StorageObjectTransformOptions'] - } FormatQueryBody: { query: string } @@ -4934,7 +4959,15 @@ export interface components { website: string } GetObjectsBody: { - options: components['schemas']['StorageObjectSearchOptions'] + options?: { + limit?: number + offset?: number + search?: string + sortBy?: { + column?: string + order?: string + } + } path: string } GetOrganizationByFlyOrganizationIdResponse: { @@ -5020,26 +5053,52 @@ export interface components { title: string }[] GetPublicUrlBody: { - options?: components['schemas']['PublicUrlOptions'] + options?: { + download?: boolean + downloadName?: string + transform?: { + /** @enum {string} */ + format?: 'origin' + height?: number + quality?: number + /** @enum {string} */ + resize?: 'cover' | 'contain' | 'fill' + width?: number + } + } path: string } GetSignedUrlBody: { expiresIn: number - options?: components['schemas']['SignedUrlOptions'] + options?: { + download?: boolean + downloadName?: string + transform?: { + /** @enum {string} */ + format?: 'origin' + height?: number + quality?: number + /** @enum {string} */ + resize?: 'cover' | 'contain' | 'fill' + width?: number + } + } path: string } GetSignedUrlsBody: { expiresIn: number - options?: components['schemas']['SignedUrlsOptions'] + options?: { + download?: boolean + downloadName?: string + } path: string[] } - GetStorageCredential: { - created_at: string - description: string - id: string - } GetStorageCredentialsResponse: { - data: components['schemas']['GetStorageCredential'][] + data: { + created_at: string + description: string + id: string + }[] } GetSubscriptionResponse: { addons: components['schemas']['BillingSubscriptionAddon'][] @@ -5143,7 +5202,7 @@ export interface components { } projects: components['schemas']['IntegrationVercelProject'][] } - GitHubAuthorization: { + GitHubAuthorizationResponse: { id: number sender_id: number user_id: number @@ -5341,12 +5400,6 @@ export interface components { link?: components['schemas']['VercelProjectLink'] name: string } - Invitation: { - id: number - invited_at: string - invited_email: string - role_id: number - } InvitationByTokenResponse: { authorized_user: boolean email_match: boolean @@ -5357,7 +5410,12 @@ export interface components { token_does_not_exist: boolean } InvitationResponse: { - invitations: components['schemas']['Invitation'][] + invitations: { + id: number + invited_at: string + invited_email: string + role_id: number + }[] } Invoice: { amount_due: number @@ -5490,14 +5548,13 @@ export interface components { project_ref: string }[] } - LoadBalancerDatabase: { - identifier: string - status: string - /** @enum {string} */ - type: 'PRIMARY' | 'READ_REPLICA' - } LoadBalancerDetailResponse: { - databases: components['schemas']['LoadBalancerDatabase'][] + databases: { + identifier: string + status: string + /** @enum {string} */ + type: 'PRIMARY' | 'READ_REPLICA' + }[] endpoint: string } MarkDefaultPaymentMethodBody: { @@ -5509,7 +5566,9 @@ export interface components { Member: { gotrue_id: string is_sso_user: boolean | null - metadata: Record | null + metadata: { + [key: string]: unknown + } mfa_enabled: boolean primary_email: string | null role_ids: number[] @@ -5611,7 +5670,9 @@ export interface components { is_owner: boolean name: string opt_in_tags: string[] - restriction_data: Record + restriction_data: { + [key: string]: string + } | null /** @enum {string|null} */ restriction_status: 'grace_period' | 'grace_period_over' | 'restricted' | null slug: string @@ -5619,15 +5680,20 @@ export interface components { subscription_id: string | null } OrganizationRoleResponse: { - org_scoped_roles: components['schemas']['OrganizationRoleV2'][] - project_scoped_roles: components['schemas']['OrganizationRoleV2'][] - } - OrganizationRoleV2: { - base_role_id: number - description: string | null - id: number - name: string - project_ids: number[] | null + org_scoped_roles: { + base_role_id: number + description: string | null + id: number + name: string + project_ids: number[] | null + }[] + project_scoped_roles: { + base_role_id: number + description: string | null + id: number + name: string + project_ids: number[] | null + }[] } OrganizationSlugAvailableVersionsBodyDto: { provider: string @@ -5681,62 +5747,66 @@ export interface components { OrgDocumentUrlResponse: { fileUrl: string } - OrgMetricUsage: { - available_in_plan: boolean - capped: boolean - cost: number - /** @enum {string} */ - metric: - | 'EGRESS' - | 'DATABASE_SIZE' - | 'STORAGE_SIZE' - | 'MONTHLY_ACTIVE_USERS' - | 'MONTHLY_ACTIVE_SSO_USERS' - | 'FUNCTION_INVOCATIONS' - | 'STORAGE_IMAGES_TRANSFORMED' - | 'REALTIME_MESSAGE_COUNT' - | 'REALTIME_PEAK_CONNECTIONS' - | 'DISK_SIZE_GB_HOURS_GP3' - | 'DISK_SIZE_GB_HOURS_IO2' - | 'AUTH_MFA_PHONE' - | 'AUTH_MFA_WEB_AUTHN' - | 'LOG_DRAIN_EVENTS' - | 'MONTHLY_ACTIVE_THIRD_PARTY_USERS' - | 'DISK_THROUGHPUT_GP3' - | 'DISK_IOPS_GP3' - | 'DISK_IOPS_IO2' - | 'COMPUTE_HOURS_BRANCH' - | 'COMPUTE_HOURS_XS' - | 'COMPUTE_HOURS_SM' - | 'COMPUTE_HOURS_MD' - | 'COMPUTE_HOURS_L' - | 'COMPUTE_HOURS_XL' - | 'COMPUTE_HOURS_2XL' - | 'COMPUTE_HOURS_4XL' - | 'COMPUTE_HOURS_8XL' - | 'COMPUTE_HOURS_12XL' - | 'COMPUTE_HOURS_16XL' - | 'CUSTOM_DOMAIN' - | 'PITR_7' - | 'PITR_14' - | 'PITR_28' - | 'IPV4' - | 'LOG_DRAIN' - pricing_free_units?: number - pricing_package_price?: number - pricing_package_size?: number - pricing_per_unit_price?: number - /** @enum {string} */ - pricing_strategy: 'UNIT' | 'PACKAGE' | 'TIERED' | 'NONE' - project_allocations: components['schemas']['ProjectAllocation'][] - unit_price_desc: string - unlimited: boolean - usage: number - usage_original: number - } OrgUsageResponse: { usage_billing_enabled: boolean - usages: components['schemas']['OrgMetricUsage'][] + usages: { + available_in_plan: boolean + capped: boolean + cost: number + /** @enum {string} */ + metric: + | 'EGRESS' + | 'DATABASE_SIZE' + | 'STORAGE_SIZE' + | 'MONTHLY_ACTIVE_USERS' + | 'MONTHLY_ACTIVE_SSO_USERS' + | 'FUNCTION_INVOCATIONS' + | 'STORAGE_IMAGES_TRANSFORMED' + | 'REALTIME_MESSAGE_COUNT' + | 'REALTIME_PEAK_CONNECTIONS' + | 'DISK_SIZE_GB_HOURS_GP3' + | 'DISK_SIZE_GB_HOURS_IO2' + | 'AUTH_MFA_PHONE' + | 'AUTH_MFA_WEB_AUTHN' + | 'LOG_DRAIN_EVENTS' + | 'MONTHLY_ACTIVE_THIRD_PARTY_USERS' + | 'DISK_THROUGHPUT_GP3' + | 'DISK_IOPS_GP3' + | 'DISK_IOPS_IO2' + | 'COMPUTE_HOURS_BRANCH' + | 'COMPUTE_HOURS_XS' + | 'COMPUTE_HOURS_SM' + | 'COMPUTE_HOURS_MD' + | 'COMPUTE_HOURS_L' + | 'COMPUTE_HOURS_XL' + | 'COMPUTE_HOURS_2XL' + | 'COMPUTE_HOURS_4XL' + | 'COMPUTE_HOURS_8XL' + | 'COMPUTE_HOURS_12XL' + | 'COMPUTE_HOURS_16XL' + | 'CUSTOM_DOMAIN' + | 'PITR_7' + | 'PITR_14' + | 'PITR_28' + | 'IPV4' + | 'LOG_DRAIN' + pricing_free_units?: number + pricing_package_price?: number + pricing_package_size?: number + pricing_per_unit_price?: number + /** @enum {string} */ + pricing_strategy: 'UNIT' | 'PACKAGE' | 'TIERED' | 'NONE' + project_allocations: { + hours?: number + name: string + ref: string + usage: number + }[] + unit_price_desc: string + unlimited: boolean + usage: number + usage_original: number + }[] } OverdueInvoiceCount: { organization_id: number @@ -5747,11 +5817,11 @@ export interface components { } PasswordCheckResponse: { result: { - feedback?: { - suggestions?: string[] - warning?: string + feedback: { + suggestions: string[] + warning: string } - score?: number + score: number } } PauseStatusResponse: { @@ -5798,16 +5868,18 @@ export interface components { PgbouncerStatusResponse: { active: boolean } - PlanResponse: { - change_type: components['schemas']['BillingPlanChangeType'] - effective_at: components['schemas']['BillingPlanEffectiveAt'] - id: components['schemas']['BillingPlanId'] - is_current: boolean - name: string - price: number - } PlansResponse: { - plans: components['schemas']['PlanResponse'][] + plans: { + /** @enum {string} */ + change_type: 'upgrade' | 'downgrade' | 'none' + /** @enum {string} */ + effective_at?: 'now' | 'end_of_billing_period' | 'none' + /** @enum {string} */ + id: 'free' | 'pro' | 'team' | 'enterprise' + is_current: boolean + name: string + price: number + }[] } PointInTimeRestoreBody: { recovery_time_target_unix: number @@ -5878,7 +5950,27 @@ export interface components { schema: string | null } PostgresForeignTable: { - columns?: components['schemas']['PostgresColumn'][] + columns?: { + check: string | null + comment: string | null + data_type: string + default_value: (string | number) | null + enums: string[] + format: string + id: string + /** @enum {string|null} */ + identity_generation: 'BY DEFAULT' | 'ALWAYS' | null + is_generated: boolean + is_identity: boolean + is_nullable: boolean + is_unique: boolean + is_updatable: boolean + name: string + ordinal_position: number + schema: string + table: string + table_id: number + }[] comment: string | null id: number name: string @@ -5900,7 +5992,27 @@ export interface components { security_definer: boolean } PostgresMaterializedView: { - columns?: components['schemas']['PostgresColumn'][] + columns?: { + check: string | null + comment: string | null + data_type: string + default_value: (string | number) | null + enums: string[] + format: string + id: string + /** @enum {string|null} */ + identity_generation: 'BY DEFAULT' | 'ALWAYS' | null + is_generated: boolean + is_identity: boolean + is_nullable: boolean + is_unique: boolean + is_updatable: boolean + name: string + ordinal_position: number + schema: string + table: string + table_id: number + }[] comment: string | null id: number is_populated: boolean @@ -5980,7 +6092,27 @@ export interface components { schema: string } PostgresView: { - columns?: components['schemas']['PostgresColumn'][] + columns?: { + check: string | null + comment: string | null + data_type: string + default_value: (string | number) | null + enums: string[] + format: string + id: string + /** @enum {string|null} */ + identity_generation: 'BY DEFAULT' | 'ALWAYS' | null + is_generated: boolean + is_identity: boolean + is_nullable: boolean + is_unique: boolean + is_updatable: boolean + name: string + ordinal_position: number + schema: string + table: string + table_id: number + }[] comment: string | null id: number is_updatable: boolean @@ -6090,12 +6222,6 @@ export interface components { price_interval: components['schemas']['ProjectAddonPricingInterval'] price_type: components['schemas']['ProjectAddonVariantPricingType'] } - ProjectAllocation: { - hours?: number - name: string - ref: string - usage: number - } ProjectAppConfigResponse: { db_schema: string endpoint: string @@ -6280,11 +6406,6 @@ export interface components { region: string status: components['schemas']['project_status'] } - PublicUrlOptions: { - download?: boolean - downloadName?: string - transform?: components['schemas']['StorageObjectTransformOptions'] - } PublicUrlResponse: { publicUrl: string } @@ -6427,13 +6548,15 @@ export interface components { }[] } ResetPasswordBody: { + /** Format: email */ email: string + /** Format: uri */ redirectTo?: string } ResizeBody: { volume_size_gb: number } - RestartProjectBodyDto: { + RestartProjectBody: { database_identifier?: string } RestartServiceRequest: { @@ -6571,31 +6694,28 @@ export interface components { } SetupIntentResponse: { client_secret: string - payment_method: Record - } - SignedUrlOptions: { - download?: boolean - downloadName?: string - transform?: components['schemas']['StorageObjectTransformOptions'] + payment_method: + | string + | { + [key: string]: unknown + } } SignedUrlResponse: { signedUrl: string } - SignedUrlsOptions: { - download?: boolean - downloadName?: string - } SignedUrlsResponse: { error: string | null path: string | null signedUrl: string } SignUpBody: { + /** Format: email */ email: string password: string + /** Format: uri */ redirectTo?: string } - StorageBucket: { + StorageBucketResponse: { created_at: string id: string name: string @@ -6620,34 +6740,24 @@ export interface components { } StorageObject: { bucket_id: string - buckets: components['schemas']['StorageBucket'] + buckets: { + created_at: string + id: string + name: string + owner: string + public: boolean + updated_at: string + } created_at: string id: string last_accessed_at: string - metadata: Record + metadata: { + [key: string]: unknown + } name: string owner: string updated_at: string } - StorageObjectSearchOptions: { - limit?: number - offset?: number - search?: string - sortBy?: components['schemas']['StorageObjectSortBy'] - } - StorageObjectSortBy: { - column?: string - order?: string - } - StorageObjectTransformOptions: { - /** @enum {string} */ - format?: 'origin' - height?: number - quality?: number - /** @enum {string} */ - resize?: 'cover' | 'contain' | 'fill' - width?: number - } SupavisorConfigResponse: { connection_string: string /** @@ -6763,8 +6873,9 @@ export interface components { pathname: string } TransferOrganizationBody: { + /** Format: uuid */ member_gotrue_id: string - member_id: number + member_id?: number org_id: number } TransferProjectBody: { @@ -7159,8 +7270,8 @@ export interface components { max_rows: number } UpdateProfileBody: { - first_name: string - last_name: string + first_name?: string + last_name?: string } UpdateProjectBody: { name: string @@ -7230,8 +7341,8 @@ export interface components { message: string } UpdateStorageBucketBody: { - allowed_mime_types: string[] - file_size_limit: number + allowed_mime_types?: string[] + file_size_limit?: number public: boolean } UpdateStorageConfigBody: { @@ -7304,7 +7415,9 @@ export interface components { email_change_token_new?: string email_confirmed_at?: string encrypted_password?: string + /** Format: uuid */ id?: string + /** Format: uuid */ instance_id?: string invited_at?: string is_anonymous?: boolean @@ -7316,8 +7429,12 @@ export interface components { phone_change_sent_at?: string phone_change_token?: string phone_confirmed_at?: string - raw_app_meta_data?: Record - raw_user_meta_data?: Record + raw_app_meta_data?: { + [key: string]: unknown + } + raw_user_meta_data?: { + [key: string]: unknown + } reauthentication_sent_at?: string reauthentication_token?: string recovery_sent_at?: string @@ -7346,6 +7463,10 @@ export interface components { /** @enum {string} */ visibility: 'user' | 'project' | 'org' | 'public' } + UserAuditLogsResponse: { + result: unknown[] + retention_period: number + } UserBody: { aud?: string banned_until?: string @@ -7362,7 +7483,9 @@ export interface components { email_change_token_new?: string email_confirmed_at?: string encrypted_password?: string + /** Format: uuid */ id?: string + /** Format: uuid */ instance_id?: string invited_at?: string is_anonymous?: boolean @@ -7374,8 +7497,12 @@ export interface components { phone_change_sent_at?: string phone_change_token?: string phone_confirmed_at?: string - raw_app_meta_data?: Record - raw_user_meta_data?: Record + raw_app_meta_data?: { + [key: string]: unknown + } + raw_user_meta_data?: { + [key: string]: unknown + } reauthentication_sent_at?: string reauthentication_token?: string recovery_sent_at?: string @@ -7434,12 +7561,11 @@ export interface components { subject: string } ValidateSpamResponse: { - rules: components['schemas']['ValidateSpamRule'][] - } - ValidateSpamRule: { - desc: string - name: string - score: number + rules: { + desc: string + name: string + score: number + }[] } VercelProjectDeployHooks: { createdAt?: number @@ -7461,7 +7587,7 @@ export interface components { branch_id: string check_run_id: number | null created_at: string - git_config: Record | null + git_config?: unknown id: string /** @enum {string} */ status: @@ -7780,7 +7906,6 @@ export interface operations { query?: never header?: never path: { - /** @description Project ref */ ref: string } cookie?: never @@ -7820,7 +7945,6 @@ export interface operations { header?: never path: { id: string - /** @description Project ref */ ref: string } cookie?: never @@ -7854,7 +7978,6 @@ export interface operations { header?: never path: { id: string - /** @description Project ref */ ref: string } cookie?: never @@ -8355,8 +8478,8 @@ export interface operations { parameters: { query: { id: number - limit?: string - offset?: string + limit: number + offset: number } header: { 'x-connection-encrypted': string @@ -8583,7 +8706,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['GitHubAuthorization'] + 'application/json': components['schemas']['GitHubAuthorizationResponse'] } } /** @description Failed to get GitHub authorization */ @@ -9188,7 +9311,6 @@ export interface operations { query?: never header?: never path: { - /** @description Oauth authorization id */ id: string } cookie?: never @@ -9348,11 +9470,9 @@ export interface operations { } OrgAuditLogsController_getAuditLogs: { parameters: { - query?: { - /** @description End timestamp */ - iso_timestamp_end?: string - /** @description Start timestamp */ - iso_timestamp_start?: string + query: { + iso_timestamp_end: string + iso_timestamp_start: string } header?: never path: { @@ -9787,7 +9907,6 @@ export interface operations { query?: never header?: never path: { - /** @description Organization slug */ slug: string } cookie?: never @@ -9816,7 +9935,6 @@ export interface operations { query?: never header?: never path: { - /** @description Organization slug */ slug: string } cookie?: never @@ -9998,7 +10116,6 @@ export interface operations { query?: never header?: never path: { - /** @description Organization slug */ slug: string } cookie?: never @@ -10028,7 +10145,6 @@ export interface operations { header?: never path: { gotrue_id: string - /** @description Organization slug */ slug: string } cookie?: never @@ -10056,7 +10172,6 @@ export interface operations { header?: never path: { gotrue_id: string - /** @description Organization slug */ slug: string } cookie?: never @@ -10089,7 +10204,6 @@ export interface operations { path: { gotrue_id: string role_id: string - /** @description Organization slug */ slug: string } cookie?: never @@ -10122,7 +10236,6 @@ export interface operations { path: { gotrue_id: string role_id: string - /** @description Organization slug */ slug: string } cookie?: never @@ -10149,7 +10262,6 @@ export interface operations { query?: never header?: never path: { - /** @description Organization slug */ slug: string } cookie?: never @@ -10178,7 +10290,6 @@ export interface operations { query?: never header?: never path: { - /** @description Organization slug */ slug: string } cookie?: never @@ -10210,7 +10321,6 @@ export interface operations { header?: never path: { id: number - /** @description Organization slug */ slug: string } cookie?: never @@ -10237,7 +10347,6 @@ export interface operations { query?: never header?: never path: { - /** @description Organization slug */ slug: string token: string } @@ -10267,7 +10376,6 @@ export interface operations { query?: never header?: never path: { - /** @description Organization slug */ slug: string token: string } @@ -10326,7 +10434,6 @@ export interface operations { } header?: never path: { - /** @description Organization slug */ slug: string } cookie?: never @@ -10354,7 +10461,6 @@ export interface operations { query?: never header?: never path: { - /** @description Organization slug */ slug: string } cookie?: never @@ -10474,9 +10580,7 @@ export interface operations { query?: never header?: never path: { - /** @description Oauth authorization id */ id: string - /** @description Organization slug */ slug: string } cookie?: never @@ -10504,9 +10608,7 @@ export interface operations { query?: never header?: never path: { - /** @description Oauth authorization id */ id: string - /** @description Organization slug */ slug: string } cookie?: never @@ -10885,7 +10987,6 @@ export interface operations { } header?: never path: { - /** @description Organization slug */ slug: string } cookie?: never @@ -12831,11 +12932,9 @@ export interface operations { } UserAuditLogsController_getAuditLogs: { parameters: { - query?: { - /** @description End timestamp */ - iso_timestamp_end?: string - /** @description Start timestamp */ - iso_timestamp_start?: string + query: { + iso_timestamp_end: string + iso_timestamp_start: string } header?: never path?: never @@ -12848,7 +12947,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['AuditLogsResponse'] + 'application/json': components['schemas']['UserAuditLogsResponse'] } } /** @description Failed to get a user's audit logs */ @@ -15672,7 +15771,7 @@ export interface operations { } requestBody: { content: { - 'application/json': components['schemas']['RestartProjectBodyDto'] + 'application/json': components['schemas']['RestartProjectBody'] } } responses: { @@ -17012,7 +17111,6 @@ export interface operations { query?: never header?: never path: { - /** @description Project ref */ ref: string } cookie?: never @@ -17024,7 +17122,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['StorageBucket'][] + 'application/json': components['schemas']['StorageBucketResponse'][] } } 403: { @@ -17047,7 +17145,6 @@ export interface operations { query?: never header?: never path: { - /** @description Project ref */ ref: string } cookie?: never @@ -17086,9 +17183,7 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17100,7 +17195,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['StorageBucket'] + 'application/json': components['schemas']['StorageBucketResponse'] } } 403: { @@ -17123,9 +17218,7 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17158,9 +17251,7 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17197,9 +17288,7 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17232,9 +17321,8 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ + /** @description Bucket ID */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17271,9 +17359,8 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ + /** @description Bucket ID */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17312,9 +17399,8 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ + /** @description Bucket ID */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17351,9 +17437,8 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ + /** @description Bucket ID */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17392,9 +17477,8 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ + /** @description Bucket ID */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17431,9 +17515,8 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ + /** @description Bucket ID */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17472,9 +17555,8 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ + /** @description Bucket ID */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17513,9 +17595,8 @@ export interface operations { query?: never header?: never path: { - /** @description Storage bucket id */ + /** @description Bucket ID */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -17554,7 +17635,6 @@ export interface operations { query?: never header?: never path: { - /** @description Project ref */ ref: string } cookie?: never @@ -17589,7 +17669,6 @@ export interface operations { query?: never header?: never path: { - /** @description Project ref */ ref: string } cookie?: never @@ -17628,9 +17707,7 @@ export interface operations { query?: never header?: never path: { - /** @description Storage credential id */ id: string - /** @description Project ref */ ref: string } cookie?: never @@ -18005,7 +18082,6 @@ export interface operations { VercelProjectsController_getVercelProjects: { parameters: { query: { - id: string teamId: string } header: { @@ -18036,7 +18112,6 @@ export interface operations { VercelProjectsController_getVercelProject: { parameters: { query: { - id: string teamId: string } header: { @@ -18044,7 +18119,6 @@ export interface operations { } path: { id: string - teamId: string } cookie?: never } @@ -18071,7 +18145,7 @@ export interface operations { parameters: { query: { projectId: string - teamId?: string + teamId: string } header: { vercel_authorization: string @@ -18102,7 +18176,7 @@ export interface operations { parameters: { query: { projectId: string - teamId?: string + teamId: string } header: { vercel_authorization: string @@ -18190,11 +18264,9 @@ export interface operations { WorkflowRunController_listWorkflowRuns: { parameters: { query?: { - /** @description Branch ID */ branch_id?: string limit?: number offset?: number - /** @description Project ref */ project_ref?: string } header?: never @@ -18223,9 +18295,7 @@ export interface operations { WorkflowRunController_countWorkflowRuns: { parameters: { query?: { - /** @description Branch ID */ branch_id?: string - /** @description Project ref */ project_ref?: string } header?: never @@ -18256,7 +18326,6 @@ export interface operations { query?: never header?: never path: { - /** @description Workflow run ID */ workflow_run_id: string } cookie?: never diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2242731b9c0..e7176f03423 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,10 +88,10 @@ importers: version: 1.2.0 next: specifier: 'catalog:' - version: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) + version: 14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.1)(markdown-wasm@1.2.0)(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.1)(markdown-wasm@1.2.0)(next@14.2.21(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -868,7 +868,7 @@ importers: version: 9.3.4 '@testing-library/jest-dom': specifier: ^6.4.6 - version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)))(vitest@3.0.4) + version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4) '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1011,7 +1011,7 @@ importers: specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-slot': - specifier: ^1.0.2 + specifier: ^1.1.2 version: 1.1.2(@types/react@18.3.3)(react@18.2.0) '@radix-ui/react-tooltip': specifier: ^1.0.7 @@ -1036,10 +1036,10 @@ importers: version: 0.436.0(react@18.2.0) next: specifier: 'catalog:' - version: 14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) + version: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.1)(markdown-wasm@1.2.0)(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.1)(markdown-wasm@1.2.0)(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1124,7 +1124,7 @@ importers: version: link:../../packages/config fumadocs-ui: specifier: ^15.1.0 - version: 15.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(fumadocs-core@15.1.1(@types/react@18.3.3)(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1))(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.10)(typescript@5.5.2))) + version: 15.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(fumadocs-core@15.1.1(@types/react@18.3.3)(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1))(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.10)(typescript@5.5.2))) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -1930,7 +1930,7 @@ importers: version: 3.6.1 '@testing-library/jest-dom': specifier: ^6.1.3 - version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)))(vitest@3.0.4) + version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4) '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -2147,7 +2147,7 @@ importers: version: link:../api-types next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@14.2.21(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0) + version: 0.9.13(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0) typescript: specifier: ~5.5.0 version: 5.5.2 @@ -9026,7 +9026,6 @@ packages: resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==} peerDependencies: eslint: '>= 5' - bundledDependencies: [] eslint-plugin-import@2.29.1: resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} @@ -21071,22 +21070,6 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)))(vitest@3.0.4)': - dependencies: - '@adobe/css-tools': 4.4.0 - '@babel/runtime': 7.24.7 - aria-query: 5.3.0 - chalk: 3.0.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - lodash: 4.17.21 - redent: 3.0.0 - optionalDependencies: - '@jest/globals': 29.7.0(supports-color@8.1.1) - '@types/jest': 29.5.5 - jest: 29.7.0(@types/node@20.12.11)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)) - vitest: 3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5) - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)': dependencies: '@adobe/css-tools': 4.4.0 @@ -21918,7 +21901,7 @@ snapshots: sirv: 3.0.0 tinyglobby: 0.2.10 tinyrainbow: 2.0.0 - vitest: 3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@20.12.11)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5) + vitest: 3.0.4(@types/node@22.13.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@22.13.10)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5) '@vitest/utils@3.0.4': dependencies: @@ -24655,7 +24638,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@15.1.1(@types/react@18.3.3)(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1): + fumadocs-core@15.1.1(@types/react@18.3.3)(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1): dependencies: '@formatjs/intl-localematcher': 0.6.0 '@orama/orama': 3.1.3 @@ -24673,14 +24656,14 @@ snapshots: shiki: 3.2.1 unist-util-visit: 5.0.0 optionalDependencies: - next: 14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) + next: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: - '@types/react' - supports-color - fumadocs-ui@15.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(fumadocs-core@15.1.1(@types/react@18.3.3)(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1))(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.10)(typescript@5.5.2))): + fumadocs-ui@15.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(fumadocs-core@15.1.1(@types/react@18.3.3)(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1))(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.10)(typescript@5.5.2))): dependencies: '@radix-ui/react-accordion': 1.2.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-collapsible': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -24692,10 +24675,10 @@ snapshots: '@radix-ui/react-slot': 1.1.2(@types/react@18.3.3)(react@18.2.0) '@radix-ui/react-tabs': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) class-variance-authority: 0.7.1 - fumadocs-core: 15.1.1(@types/react@18.3.3)(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1) + fumadocs-core: 15.1.1(@types/react@18.3.3)(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1) lodash.merge: 4.6.2 lucide-react: 0.482.0(react@18.2.0) - next: 14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) + next: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) next-themes: 0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) postcss-selector-parser: 7.1.0 react: 18.2.0 @@ -27999,7 +27982,7 @@ snapshots: - markdown-wasm - supports-color - next-contentlayer2@0.4.6(contentlayer2@0.4.6(esbuild@0.25.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.1)(markdown-wasm@1.2.0)(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1): + next-contentlayer2@0.4.6(contentlayer2@0.4.6(esbuild@0.25.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.1)(markdown-wasm@1.2.0)(next@14.2.21(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(supports-color@8.1.1): dependencies: '@contentlayer2/core': 0.4.3(esbuild@0.25.1)(markdown-wasm@1.2.0)(supports-color@8.1.1) '@contentlayer2/utils': 0.4.3 @@ -28047,7 +28030,7 @@ snapshots: next: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) react: 18.2.0 - next-router-mock@0.9.13(next@14.2.21(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0): + next-router-mock@0.9.13(next@14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0): dependencies: next: 14.2.21(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) react: 18.2.0