mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
5cb0f327e7
57 files changed
+780
-516
No files matched your search
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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.
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 />
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
Vendored
+3
@@ -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
File diff suppressed because it is too large.
Load diff
Generated
+17
-34
@@ -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
|
||||
|
||||
Reference in new issue
Block a user