Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2025-03-25 12:52:43 -02:30
commit 5cb0f327e7
57 files changed
+780 -516

No files matched your search

+1 -1
View File
@@ -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
@@ -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')
@@ -27,7 +27,7 @@ export const isAtBottom = ({ currentTarget }: UIEvent<HTMLDivElement>): 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 ||
@@ -43,7 +43,7 @@ export const Restriction = () => {
shownAlert = 'restricted'
}
if (shownAlert === null) {
if (shownAlert === null || !org?.restriction_data) {
return null
}
@@ -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
</a>
@@ -20,7 +20,7 @@ const TIA = () => {
</ScaffoldSectionDetail>
<ScaffoldSectionContent className="flex items-center justify-center h-full">
<a
href="https://supabase.com/downloads/docs/Supabase+TIA+241030.pdf"
href="https://supabase.com/downloads/docs/Supabase+TIA+250314.pdf"
target="_blank"
rel="noreferrer noopener"
download={true}
@@ -70,7 +70,8 @@ export const PostgresVersionSelector = ({
)
useEffect(() => {
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])
@@ -122,8 +122,8 @@ const Addons = () => {
You are currently on a preview branch of your project
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
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{' '}
<Link href={`/project/${parentProject.ref}/settings/general`} className="text-brand">
main branch
</Link>
@@ -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
</Button>
@@ -27,7 +27,7 @@ export const RestrictionBanner = () => {
{currentOrg.restriction_status === 'restricted' ? <CriticalIcon /> : <WarningIcon />}
<span>
{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' &&
@@ -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,
@@ -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(
@@ -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({
@@ -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 = {}
@@ -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,
@@ -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,
@@ -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,
@@ -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']
+1 -1
View File
@@ -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']
+1 -1
View File
@@ -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,
@@ -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<
| {
@@ -34,15 +34,12 @@ const ProjectSettings: NextPageWithLayout = () => {
</ScaffoldContainer>
<ScaffoldContainer className="flex flex-col gap-10" bottomPadding>
<General />
{!isBranch ? (
<>
{/* this is only setable on compliance orgs, currently that means HIPAA orgs */}
{hasHipaaAddon && <ComplianceConfig />}
<CustomDomainConfig />
{projectTransferEnabled && <TransferProjectPanel />}
<DeleteProjectPanel />
</>
) : null}
{/* this is only settable on compliance orgs, currently that means HIPAA orgs */}
{!isBranch && hasHipaaAddon && <ComplianceConfig />}
<CustomDomainConfig />
{!isBranch && projectTransferEnabled && <TransferProjectPanel />}
{!isBranch && <DeleteProjectPanel />}
</ScaffoldContainer>
</>
)
+1 -1
View File
@@ -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<string, never>
restriction_data: Record<string, string> | null
managed_by: 'supabase' | 'vercel-marketplace' | 'aws-marketplace'
partner_id?: string
}
@@ -73,7 +73,7 @@ export function BlockItemCode({ files }: BlockItemCodeProps) {
: null
return (
<div className="flex border rounded-lg overflow-hidden h-[652px] not-prose">
<div className="flex border rounded-lg overflow-hidden h-[652px] not-prose mt-4">
{/* File browser sidebar */}
<div className="w-64 border-r bg-muted/30 overflow-y-auto">
<div className="border-b bg-muted/50 h-12 items-center flex px-2">
+13 -1
View File
@@ -74,6 +74,12 @@ export const frameworkPages: Record<string, SidebarNavGroup> = {
'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<string, SidebarNavGroup> = {
],
},
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',
@@ -3,32 +3,59 @@ title: FAQ
description: Frequently asked questions
---
<Accordion openBehaviour="single">
<AccordionItem value="overwrite">
<AccordionTrigger>Will it overwrite what I already have?</AccordionTrigger>
<AccordionContent>Answer</AccordionContent>
<Accordion openBehaviour="single" collapsible>
<AccordionItem value="how">
<AccordionTrigger>How does it work? </AccordionTrigger>
<AccordionContent>
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.
</AccordionContent>
</AccordionItem>
<AccordionItem value="overwrite">
<AccordionTrigger>Will it overwrite my existing files?</AccordionTrigger>
<AccordionContent>
No. If you run a command in an existing project, it will <u>not</u> 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.
</AccordionContent>
</AccordionItem>
<AccordionItem value="existing-project">
<AccordionTrigger>I've already started my a project, what should I do?</AccordionTrigger>
<AccordionContent>
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.
</AccordionContent>
</AccordionItem>
<AccordionItem value="shadcn">
<AccordionTrigger>I've already installed shadcn/ui, what should I do?</AccordionTrigger>
<AccordionContent>
You're off to a great start! You can now run an add command to add the desired component to your
project.
</AccordionContent>
</AccordionItem>
<AccordionItem value="quickstart">
<AccordionTrigger>
I already started with `npx create-next-app -e with-supabase` what should I do now?
</AccordionTrigger>
<AccordionContent>
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.
</AccordionContent>
</AccordionItem>
<AccordionItem value="ai-builder">
<AccordionTrigger>I used an AI builder to build my app, what should I do?</AccordionTrigger>
<AccordionContent>
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.
</AccordionContent>
</AccordionItem>
<AccordionItem value="shadcn">
<AccordionTrigger>I've already installed shadcn/ui, what should I do?</AccordionTrigger>
<AccordionContent>Answer</AccordionContent>
</AccordionItem>
<AccordionItem value="supabase">
<AccordionTrigger>
I already started with -e with-supabase what should I do?
</AccordionTrigger>
<AccordionContent>Answer</AccordionContent>
</AccordionItem>
<AccordionItem value="pages">
<AccordionTrigger>I use pages router</AccordionTrigger>
<AccordionContent>Answer</AccordionContent>
</AccordionItem>
<AccordionItem value="app">
<AccordionTrigger>I use app router</AccordionTrigger>
<AccordionContent>Answer</AccordionContent>
</AccordionItem>
<AccordionItem value="none">
<AccordionTrigger>I use none of the things mentioned, what can I do?</AccordionTrigger>
<AccordionContent>Answer</AccordionContent>
</AccordionItem>
</Accordion>
@@ -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.
+11 -3
View File
@@ -11,14 +11,22 @@ description: Supabase client for Next.js
<RegistryBlock itemName="supabase-client-nextjs" />
## 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.
@@ -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
@@ -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
<h2>Confirm your signup</h2>
@@ -40,6 +37,10 @@ There few more steps to finish the installation:
</p>
```
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.
@@ -12,7 +12,7 @@ description: Real-time cursor sharing for collaborative applications
<BlockItem
name="realtime-cursor-nextjs"
description="Component which renders realtime cursors from other users in a room."
description="Renders realtime cursors from other users in a room."
/>
## Folder structure
@@ -20,3 +20,9 @@ description: Real-time cursor sharing for collaborative applications
<RegistryBlock itemName="realtime-cursor-nextjs" />
## 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 `<RealtimeCursors />` component to your page and it will render realtime cursors from other users in the room.
@@ -10,3 +10,6 @@ description: Supabase client for React Router
## Folder structure
<RegistryBlock itemName="supabase-client-react-router" />
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.
@@ -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
<BlockItem name="supabase-client-react" description="Supabase Client for React" />
<BlockItem name="supabase-client-react" description="Supabase Client for React SPA" />
## Folder structure
@@ -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
@@ -16,3 +16,6 @@ The easiest way to use Tanstack Start with Supabase is to follow the following s
## Folder structure
<RegistryBlock itemName="supabase-client-tanstack" />
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.
+1 -1
View File
@@ -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",
@@ -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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const 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 <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300',\n className,\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const { 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 <div className={cn('flex flex-row items-center gap-1 justify-center', className)}>\n <CheckCircle className=\"text-primary text-lg font-medium\" />\n <p className=\"text-primary text-lg font-medium\">Files uploaded successfully</p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file) => {\n return (\n <div\n key={file.name}\n className=\"flex items-center gap-2 border-b p-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-12 w-12 rounded border overflow-hidden flex-shrink-0 bg-muted\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-12 w-12 rounded border bg-muted flex items-center justify-center\">\n <File />\n </div>\n )}\n <div className=\"grow flex items-center gap-2 justify-start\">\n <span className=\"text-sm\">{file.name}</span>\n {file.errors.length !== 0 && (\n <Tooltip>\n <TooltipTrigger asChild>\n <CircleAlert className=\"text-destructive\" />\n </TooltipTrigger>\n <TooltipContent>{file.errors.map((e) => e.message).join(', ')}</TooltipContent>\n </Tooltip>\n )}\n </div>\n\n <Button\n size=\"icon\"\n variant=\"destructive\"\n className=\"justify-self-end\"\n onClick={() => handleRemoveFile(file.name)}\n >\n <Trash />\n </Button>\n </div>\n )\n })}\n {files.length > 0 && (\n <div className=\"mt-2\">\n <Button\n variant=\"default\"\n onClick={onUpload}\n disabled={files.some((file) => file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n Uploading...\n </>\n ) : (\n <>Upload</>\n )}\n </Button>\n </div>\n )}\n {error && <p className=\"text-destructive text-lg font-medium\">{error}</p>}\n </div>\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 <div className={cn('flex flex-col items-center', className)}>\n <div className=\"rounded-full bg-primary text-primary-foreground p-2\">\n <Upload />\n </div>\n <p className=\"text-lg font-medium\">\n Add or{' '}\n <a onClick={() => inputRLine truncated
"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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const isSuccess = restProps.isSuccess\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n (restProps.errors.length > 0 && !restProps.isSuccess) ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground',\n className,\n isSuccess ? 'border-solid' : 'border-dashed',\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const {\n files,\n setFiles,\n onUpload,\n loading,\n successes,\n errors,\n maxFileSize,\n maxFiles,\n isSuccess,\n } = useDropzoneContext()\n\n const exceedMaxFiles = files.length > maxFiles\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (isSuccess) {\n return (\n <div className={cn('flex flex-row items-center gap-x-2 justify-center', className)}>\n <CheckCircle size={16} className=\"text-primary\" />\n <p className=\"text-primary text-sm\">\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n </p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file, idx) => {\n const fileError = errors.find((e) => e.name === file.name)\n const isSuccessfullyUploaded = !!successes.find((e) => e === file.name)\n\n return (\n <div\n key={`${file.name}-${idx}`}\n className=\"flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-10 w-10 rounded border overflow-hidden shrink-0 bg-muted flex items-center justify-center\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-10 w-10 rounded border bg-muted flex items-center justify-center\">\n <File size={18} />\n </div>\n )}\n\n <div className=\"shrink grow flex flex-col items-start truncate\">\n <p title={file.name} className=\"text-sm truncate max-w-full\">\n {file.name}\n </p>\n {file.errors.length > 0 ? (\n <p className=\"text-xs text-destructive\">\n {file.errors\n .map((e) =>\n e.message.startsWith('File is larger than')\n ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})`\n : e.message\n )\n .join(', ')}\n </p>\n ) : loading && !isSuccessfullyUploaded ? (\n <p className=\"text-xs text-muted-foreground\">Uploading file...</p>\n ) : !!fileError ? (\n <p className=\"text-xs text-destructive\">Failed to upload: {fileError.message}</p>\n ) : isSuccessfullyUploadedLine truncated
"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<typeof useSupabaseUpload>\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<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [error, setError] = useState<string | null>(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=<seconds> header. Defaults to 3600 seconds.\n */\n cacheControl?: number\n /**\n * When set to true, the file is overwritten if it exists.\n *\n * When set to false, an error is thrown if the object already exists. Defaults to `false`\n */\n upsert?: boolean\n}\n\ntype UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const [files, setFiles] = useState<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState<string[]>([])\n\n const isSuccess = useMemo(() => {\n if (errors.length === 0 && successes.length === 0) {\n return false\n }\n if (errors.length === 0 && successes.length === files.length) {\n return true\n }\n return false\n }, [errors.length, successes.length, files.length])\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles\n .filter((file) => !files.find((x) => x.name === file.name))\n .map((file) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = []\n return file as FileWithPreview\n })\n\n const invalidFiles = fileRejections.map(({ file, errors }) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = errors\n return file as FileWithPreview\n })\n\n const newFiles = [...files, ...validFiles, ...invalidFiles]\n\n setFiles(newFiles)\n },\n [files, setFiles]\n )\n\n const dropzoneProps = useDropzone({\n onDrop,\n noClick: true,\n accept: allowedMimeTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),\n maxSize: maxFileSize,\n maxFiles: maxFiles,\n multiple: maxFiles !== 1,\n })\n\n const onUpload = useCallback(async () => {\n setLoading(true)\n\n // [Joshen] This is to support handling partial successes\n // If any files didn't upload for any reason, hitting \"Upload\" again will only upload the files that had errors\n const filesWithErrors = errors.map((x) => x.name)\n const filesToUpload =\n filesWithErrors.length > 0\n ? [\n ...files.filter((f) => filesWithErrors.includes(f.name)),\n ...files.filter((f) => !successes.includes(f.name)),\n ]\n : files\n\n const responses = await Promise.all(\n filesToUpload.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(!!path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n if (error) {\n return { name: file.name, message: error.message }\n } else {\n return { name: file.name, message: undefined }\n }\n })\n )\n\n const responseErrors = responses.filter((x) => x.message !== undefined)\n // if there were errors previously, this function tried to upload the files again so we should clear/overwrite the existing errors.\n setErrors(responseErrors)\n\n const responseSuccesses = responses.filter((x) => x.message === undefined)\n const newSucLine truncated
"type": "registry:hook"
},
{
@@ -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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const 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 <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300',\n className,\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const { 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 <div className={cn('flex flex-row items-center gap-1 justify-center', className)}>\n <CheckCircle className=\"text-primary text-lg font-medium\" />\n <p className=\"text-primary text-lg font-medium\">Files uploaded successfully</p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file) => {\n return (\n <div\n key={file.name}\n className=\"flex items-center gap-2 border-b p-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-12 w-12 rounded border overflow-hidden flex-shrink-0 bg-muted\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-12 w-12 rounded border bg-muted flex items-center justify-center\">\n <File />\n </div>\n )}\n <div className=\"grow flex items-center gap-2 justify-start\">\n <span className=\"text-sm\">{file.name}</span>\n {file.errors.length !== 0 && (\n <Tooltip>\n <TooltipTrigger asChild>\n <CircleAlert className=\"text-destructive\" />\n </TooltipTrigger>\n <TooltipContent>{file.errors.map((e) => e.message).join(', ')}</TooltipContent>\n </Tooltip>\n )}\n </div>\n\n <Button\n size=\"icon\"\n variant=\"destructive\"\n className=\"justify-self-end\"\n onClick={() => handleRemoveFile(file.name)}\n >\n <Trash />\n </Button>\n </div>\n )\n })}\n {files.length > 0 && (\n <div className=\"mt-2\">\n <Button\n variant=\"default\"\n onClick={onUpload}\n disabled={files.some((file) => file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n Uploading...\n </>\n ) : (\n <>Upload</>\n )}\n </Button>\n </div>\n )}\n {error && <p className=\"text-destructive text-lg font-medium\">{error}</p>}\n </div>\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 <div className={cn('flex flex-col items-center', className)}>\n <div className=\"rounded-full bg-primary text-primary-foreground p-2\">\n <Upload />\n </div>\n <p className=\"text-lg font-medium\">\n Add or{' '}\n <a onClick={() => inputRLine truncated
"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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const isSuccess = restProps.isSuccess\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n (restProps.errors.length > 0 && !restProps.isSuccess) ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground',\n className,\n isSuccess ? 'border-solid' : 'border-dashed',\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const {\n files,\n setFiles,\n onUpload,\n loading,\n successes,\n errors,\n maxFileSize,\n maxFiles,\n isSuccess,\n } = useDropzoneContext()\n\n const exceedMaxFiles = files.length > maxFiles\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (isSuccess) {\n return (\n <div className={cn('flex flex-row items-center gap-x-2 justify-center', className)}>\n <CheckCircle size={16} className=\"text-primary\" />\n <p className=\"text-primary text-sm\">\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n </p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file, idx) => {\n const fileError = errors.find((e) => e.name === file.name)\n const isSuccessfullyUploaded = !!successes.find((e) => e === file.name)\n\n return (\n <div\n key={`${file.name}-${idx}`}\n className=\"flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-10 w-10 rounded border overflow-hidden shrink-0 bg-muted flex items-center justify-center\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-10 w-10 rounded border bg-muted flex items-center justify-center\">\n <File size={18} />\n </div>\n )}\n\n <div className=\"shrink grow flex flex-col items-start truncate\">\n <p title={file.name} className=\"text-sm truncate max-w-full\">\n {file.name}\n </p>\n {file.errors.length > 0 ? (\n <p className=\"text-xs text-destructive\">\n {file.errors\n .map((e) =>\n e.message.startsWith('File is larger than')\n ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})`\n : e.message\n )\n .join(', ')}\n </p>\n ) : loading && !isSuccessfullyUploaded ? (\n <p className=\"text-xs text-muted-foreground\">Uploading file...</p>\n ) : !!fileError ? (\n <p className=\"text-xs text-destructive\">Failed to upload: {fileError.message}</p>\n ) : isSuccessfullyUploadedLine truncated
"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<typeof useSupabaseUpload>\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<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [error, setError] = useState<string | null>(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=<seconds> header. Defaults to 3600 seconds.\n */\n cacheControl?: number\n /**\n * When set to true, the file is overwritten if it exists.\n *\n * When set to false, an error is thrown if the object already exists. Defaults to `false`\n */\n upsert?: boolean\n}\n\ntype UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const [files, setFiles] = useState<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState<string[]>([])\n\n const isSuccess = useMemo(() => {\n if (errors.length === 0 && successes.length === 0) {\n return false\n }\n if (errors.length === 0 && successes.length === files.length) {\n return true\n }\n return false\n }, [errors.length, successes.length, files.length])\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles\n .filter((file) => !files.find((x) => x.name === file.name))\n .map((file) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = []\n return file as FileWithPreview\n })\n\n const invalidFiles = fileRejections.map(({ file, errors }) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = errors\n return file as FileWithPreview\n })\n\n const newFiles = [...files, ...validFiles, ...invalidFiles]\n\n setFiles(newFiles)\n },\n [files, setFiles]\n )\n\n const dropzoneProps = useDropzone({\n onDrop,\n noClick: true,\n accept: allowedMimeTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),\n maxSize: maxFileSize,\n maxFiles: maxFiles,\n multiple: maxFiles !== 1,\n })\n\n const onUpload = useCallback(async () => {\n setLoading(true)\n\n // [Joshen] This is to support handling partial successes\n // If any files didn't upload for any reason, hitting \"Upload\" again will only upload the files that had errors\n const filesWithErrors = errors.map((x) => x.name)\n const filesToUpload =\n filesWithErrors.length > 0\n ? [\n ...files.filter((f) => filesWithErrors.includes(f.name)),\n ...files.filter((f) => !successes.includes(f.name)),\n ]\n : files\n\n const responses = await Promise.all(\n filesToUpload.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(!!path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n if (error) {\n return { name: file.name, message: error.message }\n } else {\n return { name: file.name, message: undefined }\n }\n })\n )\n\n const responseErrors = responses.filter((x) => x.message !== undefined)\n // if there were errors previously, this function tried to upload the files again so we should clear/overwrite the existing errors.\n setErrors(responseErrors)\n\n const responseSuccesses = responses.filter((x) => x.message === undefined)\n const newSucLine truncated
"type": "registry:hook"
},
{
+2 -2
View File
@@ -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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const 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 <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300',\n className,\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const { 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 <div className={cn('flex flex-row items-center gap-1 justify-center', className)}>\n <CheckCircle className=\"text-primary text-lg font-medium\" />\n <p className=\"text-primary text-lg font-medium\">Files uploaded successfully</p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file) => {\n return (\n <div\n key={file.name}\n className=\"flex items-center gap-2 border-b p-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-12 w-12 rounded border overflow-hidden flex-shrink-0 bg-muted\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-12 w-12 rounded border bg-muted flex items-center justify-center\">\n <File />\n </div>\n )}\n <div className=\"grow flex items-center gap-2 justify-start\">\n <span className=\"text-sm\">{file.name}</span>\n {file.errors.length !== 0 && (\n <Tooltip>\n <TooltipTrigger asChild>\n <CircleAlert className=\"text-destructive\" />\n </TooltipTrigger>\n <TooltipContent>{file.errors.map((e) => e.message).join(', ')}</TooltipContent>\n </Tooltip>\n )}\n </div>\n\n <Button\n size=\"icon\"\n variant=\"destructive\"\n className=\"justify-self-end\"\n onClick={() => handleRemoveFile(file.name)}\n >\n <Trash />\n </Button>\n </div>\n )\n })}\n {files.length > 0 && (\n <div className=\"mt-2\">\n <Button\n variant=\"default\"\n onClick={onUpload}\n disabled={files.some((file) => file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n Uploading...\n </>\n ) : (\n <>Upload</>\n )}\n </Button>\n </div>\n )}\n {error && <p className=\"text-destructive text-lg font-medium\">{error}</p>}\n </div>\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 <div className={cn('flex flex-col items-center', className)}>\n <div className=\"rounded-full bg-primary text-primary-foreground p-2\">\n <Upload />\n </div>\n <p className=\"text-lg font-medium\">\n Add or{' '}\n <a onClick={() => inputRLine truncated
"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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const isSuccess = restProps.isSuccess\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n (restProps.errors.length > 0 && !restProps.isSuccess) ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground',\n className,\n isSuccess ? 'border-solid' : 'border-dashed',\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const {\n files,\n setFiles,\n onUpload,\n loading,\n successes,\n errors,\n maxFileSize,\n maxFiles,\n isSuccess,\n } = useDropzoneContext()\n\n const exceedMaxFiles = files.length > maxFiles\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (isSuccess) {\n return (\n <div className={cn('flex flex-row items-center gap-x-2 justify-center', className)}>\n <CheckCircle size={16} className=\"text-primary\" />\n <p className=\"text-primary text-sm\">\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n </p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file, idx) => {\n const fileError = errors.find((e) => e.name === file.name)\n const isSuccessfullyUploaded = !!successes.find((e) => e === file.name)\n\n return (\n <div\n key={`${file.name}-${idx}`}\n className=\"flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-10 w-10 rounded border overflow-hidden shrink-0 bg-muted flex items-center justify-center\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-10 w-10 rounded border bg-muted flex items-center justify-center\">\n <File size={18} />\n </div>\n )}\n\n <div className=\"shrink grow flex flex-col items-start truncate\">\n <p title={file.name} className=\"text-sm truncate max-w-full\">\n {file.name}\n </p>\n {file.errors.length > 0 ? (\n <p className=\"text-xs text-destructive\">\n {file.errors\n .map((e) =>\n e.message.startsWith('File is larger than')\n ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})`\n : e.message\n )\n .join(', ')}\n </p>\n ) : loading && !isSuccessfullyUploaded ? (\n <p className=\"text-xs text-muted-foreground\">Uploading file...</p>\n ) : !!fileError ? (\n <p className=\"text-xs text-destructive\">Failed to upload: {fileError.message}</p>\n ) : isSuccessfullyUploadedLine truncated
"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<typeof useSupabaseUpload>\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<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [error, setError] = useState<string | null>(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=<seconds> header. Defaults to 3600 seconds.\n */\n cacheControl?: number\n /**\n * When set to true, the file is overwritten if it exists.\n *\n * When set to false, an error is thrown if the object already exists. Defaults to `false`\n */\n upsert?: boolean\n}\n\ntype UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const [files, setFiles] = useState<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState<string[]>([])\n\n const isSuccess = useMemo(() => {\n if (errors.length === 0 && successes.length === 0) {\n return false\n }\n if (errors.length === 0 && successes.length === files.length) {\n return true\n }\n return false\n }, [errors.length, successes.length, files.length])\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles\n .filter((file) => !files.find((x) => x.name === file.name))\n .map((file) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = []\n return file as FileWithPreview\n })\n\n const invalidFiles = fileRejections.map(({ file, errors }) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = errors\n return file as FileWithPreview\n })\n\n const newFiles = [...files, ...validFiles, ...invalidFiles]\n\n setFiles(newFiles)\n },\n [files, setFiles]\n )\n\n const dropzoneProps = useDropzone({\n onDrop,\n noClick: true,\n accept: allowedMimeTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),\n maxSize: maxFileSize,\n maxFiles: maxFiles,\n multiple: maxFiles !== 1,\n })\n\n const onUpload = useCallback(async () => {\n setLoading(true)\n\n // [Joshen] This is to support handling partial successes\n // If any files didn't upload for any reason, hitting \"Upload\" again will only upload the files that had errors\n const filesWithErrors = errors.map((x) => x.name)\n const filesToUpload =\n filesWithErrors.length > 0\n ? [\n ...files.filter((f) => filesWithErrors.includes(f.name)),\n ...files.filter((f) => !successes.includes(f.name)),\n ]\n : files\n\n const responses = await Promise.all(\n filesToUpload.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(!!path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n if (error) {\n return { name: file.name, message: error.message }\n } else {\n return { name: file.name, message: undefined }\n }\n })\n )\n\n const responseErrors = responses.filter((x) => x.message !== undefined)\n // if there were errors previously, this function tried to upload the files again so we should clear/overwrite the existing errors.\n setErrors(responseErrors)\n\n const responseSuccesses = responses.filter((x) => x.message === undefined)\n const newSucLine truncated
"type": "registry:hook"
},
{
@@ -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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const 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 <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300',\n className,\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const { 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 <div className={cn('flex flex-row items-center gap-1 justify-center', className)}>\n <CheckCircle className=\"text-primary text-lg font-medium\" />\n <p className=\"text-primary text-lg font-medium\">Files uploaded successfully</p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file) => {\n return (\n <div\n key={file.name}\n className=\"flex items-center gap-2 border-b p-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-12 w-12 rounded border overflow-hidden flex-shrink-0 bg-muted\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-12 w-12 rounded border bg-muted flex items-center justify-center\">\n <File />\n </div>\n )}\n <div className=\"grow flex items-center gap-2 justify-start\">\n <span className=\"text-sm\">{file.name}</span>\n {file.errors.length !== 0 && (\n <Tooltip>\n <TooltipTrigger asChild>\n <CircleAlert className=\"text-destructive\" />\n </TooltipTrigger>\n <TooltipContent>{file.errors.map((e) => e.message).join(', ')}</TooltipContent>\n </Tooltip>\n )}\n </div>\n\n <Button\n size=\"icon\"\n variant=\"destructive\"\n className=\"justify-self-end\"\n onClick={() => handleRemoveFile(file.name)}\n >\n <Trash />\n </Button>\n </div>\n )\n })}\n {files.length > 0 && (\n <div className=\"mt-2\">\n <Button\n variant=\"default\"\n onClick={onUpload}\n disabled={files.some((file) => file.errors.length !== 0) || loading}\n >\n {loading ? (\n <>\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n Uploading...\n </>\n ) : (\n <>Upload</>\n )}\n </Button>\n </div>\n )}\n {error && <p className=\"text-destructive text-lg font-medium\">{error}</p>}\n </div>\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 <div className={cn('flex flex-col items-center', className)}>\n <div className=\"rounded-full bg-primary text-primary-foreground p-2\">\n <Upload />\n </div>\n <p className=\"text-lg font-medium\">\n Add or{' '}\n <a onClick={() => inputRLine truncated
"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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n className?: string\n}\n\nconst Dropzone = ({\n className,\n children,\n getRootProps,\n getInputProps,\n ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n const isSuccess = restProps.isSuccess\n const isActive = restProps.isDragActive\n const isInvalid =\n (restProps.isDragActive && restProps.isDragReject) ||\n (restProps.errors.length > 0 && !restProps.isSuccess) ||\n restProps.files.some((file) => file.errors.length !== 0)\n\n return (\n <DropzoneContext.Provider value={{ ...restProps }}>\n <div\n {...getRootProps({\n className: cn(\n 'border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground',\n className,\n isSuccess ? 'border-solid' : 'border-dashed',\n isActive && 'border-primary bg-primary/10',\n isInvalid && 'border-destructive bg-destructive/10'\n ),\n })}\n >\n <input {...getInputProps()} />\n {children}\n </div>\n </DropzoneContext.Provider>\n )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n const {\n files,\n setFiles,\n onUpload,\n loading,\n successes,\n errors,\n maxFileSize,\n maxFiles,\n isSuccess,\n } = useDropzoneContext()\n\n const exceedMaxFiles = files.length > maxFiles\n\n const handleRemoveFile = useCallback(\n (fileName: string) => {\n setFiles(files.filter((file) => file.name !== fileName))\n },\n [files, setFiles]\n )\n\n if (isSuccess) {\n return (\n <div className={cn('flex flex-row items-center gap-x-2 justify-center', className)}>\n <CheckCircle size={16} className=\"text-primary\" />\n <p className=\"text-primary text-sm\">\n Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n </p>\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n {files.map((file, idx) => {\n const fileError = errors.find((e) => e.name === file.name)\n const isSuccessfullyUploaded = !!successes.find((e) => e === file.name)\n\n return (\n <div\n key={`${file.name}-${idx}`}\n className=\"flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 \"\n >\n {file.type.startsWith('image/') ? (\n <div className=\"h-10 w-10 rounded border overflow-hidden shrink-0 bg-muted flex items-center justify-center\">\n <img src={file.preview} alt={file.name} className=\"object-cover\" />\n </div>\n ) : (\n <div className=\"h-10 w-10 rounded border bg-muted flex items-center justify-center\">\n <File size={18} />\n </div>\n )}\n\n <div className=\"shrink grow flex flex-col items-start truncate\">\n <p title={file.name} className=\"text-sm truncate max-w-full\">\n {file.name}\n </p>\n {file.errors.length > 0 ? (\n <p className=\"text-xs text-destructive\">\n {file.errors\n .map((e) =>\n e.message.startsWith('File is larger than')\n ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})`\n : e.message\n )\n .join(', ')}\n </p>\n ) : loading && !isSuccessfullyUploaded ? (\n <p className=\"text-xs text-muted-foreground\">Uploading file...</p>\n ) : !!fileError ? (\n <p className=\"text-xs text-destructive\">Failed to upload: {fileError.message}</p>\n ) : isSuccessfullyUploadedLine truncated
"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<typeof useSupabaseUpload>\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<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [error, setError] = useState<string | null>(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=<seconds> header. Defaults to 3600 seconds.\n */\n cacheControl?: number\n /**\n * When set to true, the file is overwritten if it exists.\n *\n * When set to false, an error is thrown if the object already exists. Defaults to `false`\n */\n upsert?: boolean\n}\n\ntype UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const [files, setFiles] = useState<FileWithPreview[]>([])\n const [loading, setLoading] = useState<boolean>(false)\n const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n const [successes, setSuccesses] = useState<string[]>([])\n\n const isSuccess = useMemo(() => {\n if (errors.length === 0 && successes.length === 0) {\n return false\n }\n if (errors.length === 0 && successes.length === files.length) {\n return true\n }\n return false\n }, [errors.length, successes.length, files.length])\n\n const onDrop = useCallback(\n (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n const validFiles = acceptedFiles\n .filter((file) => !files.find((x) => x.name === file.name))\n .map((file) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = []\n return file as FileWithPreview\n })\n\n const invalidFiles = fileRejections.map(({ file, errors }) => {\n ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n ;(file as FileWithPreview).errors = errors\n return file as FileWithPreview\n })\n\n const newFiles = [...files, ...validFiles, ...invalidFiles]\n\n setFiles(newFiles)\n },\n [files, setFiles]\n )\n\n const dropzoneProps = useDropzone({\n onDrop,\n noClick: true,\n accept: allowedMimeTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),\n maxSize: maxFileSize,\n maxFiles: maxFiles,\n multiple: maxFiles !== 1,\n })\n\n const onUpload = useCallback(async () => {\n setLoading(true)\n\n // [Joshen] This is to support handling partial successes\n // If any files didn't upload for any reason, hitting \"Upload\" again will only upload the files that had errors\n const filesWithErrors = errors.map((x) => x.name)\n const filesToUpload =\n filesWithErrors.length > 0\n ? [\n ...files.filter((f) => filesWithErrors.includes(f.name)),\n ...files.filter((f) => !successes.includes(f.name)),\n ]\n : files\n\n const responses = await Promise.all(\n filesToUpload.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(!!path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n if (error) {\n return { name: file.name, message: error.message }\n } else {\n return { name: file.name, message: undefined }\n }\n })\n )\n\n const responseErrors = responses.filter((x) => x.message !== undefined)\n // if there were errors previously, this function tried to upload the files again so we should clear/overwrite the existing errors.\n setErrors(responseErrors)\n\n const responseSuccesses = responses.filter((x) => x.message === undefined)\n const newSucLine truncated
"type": "registry:hook"
},
{
@@ -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 <div>\n {Object.keys(cursors).map((id) => (\n <Cursor\n key={id}\n className=\"absolute transform ease-linear duration-70 z-50\"\n style={{\n top: 0,\n left: 0,\n transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n }}\n color={cursors[id].color}\n name={cursors[id].user.name}\n />\n ))}\n </div>\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 <div>\n {Object.keys(cursors).map((id) => (\n <Cursor\n key={id}\n className=\"fixed ease-linear duration-70 z-50\"\n style={{\n top: 0,\n left: 0,\n transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n }}\n color={cursors[id].color}\n name={cursors[id].user.name}\n />\n ))}\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -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 <div>\n {Object.keys(cursors).map((id) => (\n <Cursor\n key={id}\n className=\"absolute transform ease-linear duration-70 z-50\"\n style={{\n top: 0,\n left: 0,\n transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n }}\n color={cursors[id].color}\n name={cursors[id].user.name}\n />\n ))}\n </div>\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 <div>\n {Object.keys(cursors).map((id) => (\n <Cursor\n key={id}\n className=\"fixed ease-linear duration-70 z-50\"\n style={{\n top: 0,\n left: 0,\n transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n }}\n color={cursors[id].color}\n name={cursors[id].user.name}\n />\n ))}\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -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 <div>\n {Object.keys(cursors).map((id) => (\n <Cursor\n key={id}\n className=\"absolute transform ease-linear duration-70 z-50\"\n style={{\n top: 0,\n left: 0,\n transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n }}\n color={cursors[id].color}\n name={cursors[id].user.name}\n />\n ))}\n </div>\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 <div>\n {Object.keys(cursors).map((id) => (\n <Cursor\n key={id}\n className=\"fixed ease-linear duration-70 z-50\"\n style={{\n top: 0,\n left: 0,\n transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n }}\n color={cursors[id].color}\n name={cursors[id].user.name}\n />\n ))}\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -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 <div>\n {Object.keys(cursors).map((id) => (\n <Cursor\n key={id}\n className=\"absolute transform ease-linear duration-70 z-50\"\n style={{\n top: 0,\n left: 0,\n transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n }}\n color={cursors[id].color}\n name={cursors[id].user.name}\n />\n ))}\n </div>\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 <div>\n {Object.keys(cursors).map((id) => (\n <Cursor\n key={id}\n className=\"fixed ease-linear duration-70 z-50\"\n style={{\n top: 0,\n left: 0,\n transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n }}\n color={cursors[id].color}\n name={cursors[id].user.name}\n />\n ))}\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -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<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>
@@ -22,10 +35,11 @@ const Dropzone = ({
getInputProps,
...restProps
}: PropsWithChildren<DropzoneProps>) => {
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 = ({
<div
{...getRootProps({
className: cn(
'border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300',
'border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground',
className,
isSuccess ? 'border-solid' : 'border-dashed',
isActive && 'border-primary bg-primary/10',
isInvalid && 'border-destructive bg-destructive/10'
),
@@ -47,7 +62,19 @@ const Dropzone = ({
)
}
const DropzoneContent = ({ className }: { className?: string }) => {
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 (
<div className={cn('flex flex-row items-center gap-1 justify-center', className)}>
<CheckCircle className="text-primary text-lg font-medium" />
<p className="text-primary text-lg font-medium">Files uploaded successfully</p>
<div className={cn('flex flex-row items-center gap-x-2 justify-center', className)}>
<CheckCircle size={16} className="text-primary" />
<p className="text-primary text-sm">
Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}
</p>
</div>
)
}
return (
<div className={cn('flex flex-col', className)}>
{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 (
<div
key={file.name}
className="flex items-center gap-2 border-b p-2 first:mt-4 last:mb-4 "
key={`${file.name}-${idx}`}
className="flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 "
>
{file.type.startsWith('image/') ? (
<div className="h-12 w-12 rounded border overflow-hidden flex-shrink-0 bg-muted">
<div className="h-10 w-10 rounded border overflow-hidden shrink-0 bg-muted flex items-center justify-center">
<img src={file.preview} alt={file.name} className="object-cover" />
</div>
) : (
<div className="h-12 w-12 rounded border bg-muted flex items-center justify-center">
<File />
<div className="h-10 w-10 rounded border bg-muted flex items-center justify-center">
<File size={18} />
</div>
)}
<div className="grow flex items-center gap-2 justify-start">
<span className="text-sm">{file.name}</span>
{file.errors.length !== 0 && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlert className="text-destructive" />
</TooltipTrigger>
<TooltipContent>{file.errors.map((e) => e.message).join(', ')}</TooltipContent>
</Tooltip>
<div className="shrink grow flex flex-col items-start truncate">
<p title={file.name} className="text-sm truncate max-w-full">
{file.name}
</p>
{file.errors.length > 0 ? (
<p className="text-xs text-destructive">
{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(', ')}
</p>
) : loading && !isSuccessfullyUploaded ? (
<p className="text-xs text-muted-foreground">Uploading file...</p>
) : !!fileError ? (
<p className="text-xs text-destructive">Failed to upload: {fileError.message}</p>
) : isSuccessfullyUploaded ? (
<p className="text-xs text-primary">Successfully uploaded file</p>
) : (
<p className="text-xs text-muted-foreground">{formatBytes(file.size, 2)}</p>
)}
</div>
<Button
size="icon"
variant="destructive"
className="justify-self-end"
onClick={() => handleRemoveFile(file.name)}
>
<Trash />
</Button>
{!loading && !isSuccessfullyUploaded && (
<Button
size="icon"
variant="link"
className="shrink-0 justify-self-end text-muted-foreground hover:text-foreground"
onClick={() => handleRemoveFile(file.name)}
>
<X />
</Button>
)}
</div>
)
})}
{files.length > 0 && (
{exceedMaxFiles && (
<p className="text-sm text-left mt-2 text-destructive">
You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file
{files.length - maxFiles > 1 ? 's' : ''}.
</p>
)}
{files.length > 0 && !exceedMaxFiles && (
<div className="mt-2">
<Button
variant="default"
variant="outline"
onClick={onUpload}
disabled={files.some((file) => file.errors.length !== 0) || loading}
>
@@ -118,37 +172,46 @@ const DropzoneContent = ({ className }: { className?: string }) => {
Uploading...
</>
) : (
<>Upload</>
<>Upload files</>
)}
</Button>
</div>
)}
{error && <p className="text-destructive text-lg font-medium">{error}</p>}
</div>
)
}
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 (
<div className={cn('flex flex-col items-center', className)}>
<div className="rounded-full bg-primary text-primary-foreground p-2">
<Upload />
</div>
<p className="text-lg font-medium">
Add or{' '}
<a onClick={() => inputRef.current?.click()} className="underline cursor-pointer">
drop {maxFiles === 1 ? `file` : 'files'}
</a>
<div className={cn('flex flex-col items-center gap-y-2', className)}>
<Upload size={20} className="text-muted-foreground" />
<p className="text-sm">
Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file
{!maxFiles || maxFiles > 1 ? 's' : ''}
</p>
{maxFileSize !== Number.POSITIVE_INFINITY && (
<p className="text-sm text-muted-foreground mt-2">Maximum file size: {maxFileSize} MB</p>
)}
<div className="flex flex-col items-center gap-y-1">
<p className="text-xs text-muted-foreground">
Drag and drop or{' '}
<a
onClick={() => inputRef.current?.click()}
className="underline cursor-pointer transition hover:text-foreground"
>
select {maxFiles === 1 ? `file` : 'files'}
</a>{' '}
to upload
</p>
{maxFileSize !== Number.POSITIVE_INFINITY && (
<p className="text-xs text-muted-foreground">
Maximum file size: {formatBytes(maxFileSize, 2)}
</p>
)}
</div>
</div>
)
}
@@ -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=<seconds> header. Defaults to 3600 seconds.
*/
cacheControl?: number
/**
* When set to true, the file is overwritten if it exists.
*
* When set to false, an error is thrown if the object already exists. Defaults to `false`
*/
upsert?: boolean
}
type UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>
@@ -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<FileWithPreview[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
const [errors, setErrors] = useState<{ name: string; message: string }[]>([])
const [successes, setSuccesses] = useState<string[]>([])
const isSuccess = useMemo(() => {
if (errors.length === 0 && successes.length === 0) {
return false
}
if (errors.length === 0 && successes.length === files.length) {
return true
}
return false
}, [errors.length, successes.length, files.length])
const onDrop = useCallback(
(acceptedFiles: File[], fileRejections: FileRejection[]) => {
const validFiles = acceptedFiles.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,
}
}
@@ -11,7 +11,7 @@ export const RealtimeCursors = ({ roomName, username }: { roomName: string; user
{Object.keys(cursors).map((id) => (
<Cursor
key={id}
className="absolute transform ease-linear duration-70 z-50"
className="fixed ease-linear duration-70 z-50"
style={{
top: 0,
left: 0,
@@ -1,6 +1,6 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
@@ -13,10 +13,11 @@ const FileUploadDemo = () => {
path: 'test',
allowedMimeTypes: ['image/*'],
maxFiles: 2,
maxFileSize: 1000 * 1000 * 10, // 10MB,
})
return (
<div>
<div className="w-[500px]">
<Dropzone {...props}>
<DropzoneEmptyState />
<DropzoneContent />
+1 -1
View File
@@ -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)
}
+7 -7
View File
@@ -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
+3
View File
@@ -1643,6 +1643,9 @@ export interface components {
* @enum {string}
*/
release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview'
secrets?: {
[key: string]: string
}
}
CreateOrganizationV1Dto: {
name: string
+342 -273
View File
File diff suppressed because it is too large. Load diff
+17 -34
View File
@@ -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