mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
Cleanup the registry.
This commit is contained in:
1 parent
c81cee3ad7
commit
bc07855ff6
58 files changed
+2070
-3497
No files matched your search
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/eslintrc",
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { type Registry } from 'shadcn/registry'
|
||||
import { clients } from './clients'
|
||||
import dropzone from './default/blocks/dropzone/registry-item.json' assert { type: 'json' }
|
||||
import passwordBasedAuthNextjs from './default/blocks/password-based-auth-nextjs/registry-item.json' assert { type: 'json' }
|
||||
import passwordBasedAuthReact from './default/blocks/password-based-auth-react/registry-item.json' assert { type: 'json' }
|
||||
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' assert { type: 'json' }
|
||||
import { registryItemAppend } from './utils'
|
||||
|
||||
const combine = (component: Registry['items'][number]) => {
|
||||
return clients.flatMap((client) => {
|
||||
return registryItemAppend(
|
||||
{
|
||||
...component,
|
||||
name: `${component.name}-${client.name.replace('supabase-client-', '')}`,
|
||||
},
|
||||
[client]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const blocks = [
|
||||
passwordBasedAuthNextjs,
|
||||
passwordBasedAuthReact,
|
||||
...combine(dropzone as Registry['items'][number]),
|
||||
...combine(realtimeCursor as Registry['items'][number]),
|
||||
] as Registry['items']
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type Registry } from 'shadcn/registry'
|
||||
import nextjs from './default/clients/nextjs/registry-item.json' assert { type: 'json' }
|
||||
import react from './default/clients/react/registry-item.json' assert { type: 'json' }
|
||||
import remix from './default/clients/remix/registry-item.json' assert { type: 'json' }
|
||||
import tanstack from './default/clients/tanstack/registry-item.json' assert { type: 'json' }
|
||||
|
||||
export const clients = [nextjs, react, remix, tanstack] as Registry['items']
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { 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'
|
||||
|
||||
type DropzoneContextType = Omit<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>
|
||||
|
||||
const DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)
|
||||
|
||||
type DropzoneProps = UseSupabaseUploadReturn & {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Dropzone = ({
|
||||
className,
|
||||
children,
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
...restProps
|
||||
}: PropsWithChildren<DropzoneProps>) => {
|
||||
const isActive = restProps.isDragActive
|
||||
const isInvalid =
|
||||
(restProps.isDragActive && restProps.isDragReject) ||
|
||||
restProps.error ||
|
||||
restProps.files.some((file) => file.errors.length !== 0)
|
||||
|
||||
return (
|
||||
<DropzoneContext.Provider value={{ ...restProps }}>
|
||||
<div
|
||||
{...getRootProps({
|
||||
className: cn(
|
||||
'border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300',
|
||||
className,
|
||||
isActive && 'border-primary bg-primary/10',
|
||||
isInvalid && 'border-destructive bg-destructive/10'
|
||||
),
|
||||
})}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
</div>
|
||||
</DropzoneContext.Provider>
|
||||
)
|
||||
}
|
||||
const DropzoneContent = ({ className }: { className?: string }) => {
|
||||
const { error, files, setFiles, onUpload, loading, success } = useDropzoneContext()
|
||||
|
||||
const handleRemoveFile = useCallback(
|
||||
(fileName: string) => {
|
||||
setFiles(files.filter((file) => file.name !== fileName))
|
||||
},
|
||||
[files, setFiles]
|
||||
)
|
||||
|
||||
if (success) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)}>
|
||||
{files.map((file) => {
|
||||
return (
|
||||
<div
|
||||
key={file.name}
|
||||
className="flex items-center gap-2 border-b p-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">
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
className="justify-self-end"
|
||||
onClick={() => handleRemoveFile(file.name)}
|
||||
>
|
||||
<Trash />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{files.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={onUpload}
|
||||
disabled={files.some((file) => file.errors.length !== 0) || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Uploading...
|
||||
</>
|
||||
) : (
|
||||
<>Upload</>
|
||||
)}
|
||||
</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()
|
||||
|
||||
if ((maxFiles !== 0 && files.length >= maxFiles) || success) {
|
||||
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>
|
||||
</p>
|
||||
{maxFileSize !== Number.POSITIVE_INFINITY && (
|
||||
<p className="text-sm text-muted-foreground mt-2">Maximum file size: {maxFileSize} MB</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useDropzoneContext = () => {
|
||||
const context = useContext(DropzoneContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useDropzoneContext must be used within a Dropzone')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export { Dropzone, DropzoneContent, DropzoneEmptyState, useDropzoneContext }
|
||||
@@ -0,0 +1,122 @@
|
||||
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { FileError, FileRejection, useDropzone } from 'react-dropzone'
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
interface FileWithPreview extends File {
|
||||
preview?: string
|
||||
errors: readonly FileError[]
|
||||
}
|
||||
|
||||
type UseSupabaseUploadOptions = {
|
||||
bucketName: string
|
||||
path: string
|
||||
allowedMimeTypes?: string[]
|
||||
maxFileSize?: number
|
||||
maxFiles?: number
|
||||
}
|
||||
|
||||
type UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>
|
||||
|
||||
const useSupabaseUpload = (options: UseSupabaseUploadOptions) => {
|
||||
const {
|
||||
bucketName,
|
||||
path,
|
||||
allowedMimeTypes = [],
|
||||
maxFileSize = Number.POSITIVE_INFINITY,
|
||||
maxFiles = 0,
|
||||
} = 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 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 invalidFiles = fileRejections.map(({ file, errors }) => {
|
||||
;(file as FileWithPreview).preview = URL.createObjectURL(file)
|
||||
;(file as FileWithPreview).errors = errors
|
||||
return file as FileWithPreview
|
||||
})
|
||||
|
||||
const newFiles = [...files, ...validFiles, ...invalidFiles]
|
||||
|
||||
setFiles(newFiles)
|
||||
},
|
||||
[files, setFiles]
|
||||
)
|
||||
|
||||
const dropzoneProps = useDropzone({
|
||||
onDrop,
|
||||
noClick: true,
|
||||
accept: allowedMimeTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),
|
||||
maxSize: maxFileSize,
|
||||
maxFiles: maxFiles,
|
||||
multiple: maxFiles !== 1,
|
||||
})
|
||||
|
||||
const onUpload = useCallback(async () => {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const { error } = await supabase.storage
|
||||
.from(bucketName)
|
||||
.upload(`${path}/${file.name}`, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
})
|
||||
if (error) {
|
||||
setError(error.message)
|
||||
} else {
|
||||
setSuccess(true)
|
||||
}
|
||||
})
|
||||
)
|
||||
setLoading(false)
|
||||
}, [files, path, bucketName, setError, setLoading])
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length === 0) {
|
||||
setError(null)
|
||||
}
|
||||
|
||||
// If the number of files doesn't exceed the maxFiles parameter, remove the error 'Too many files' from each file
|
||||
if (files.length <= maxFiles) {
|
||||
let changed = false
|
||||
const newFiles = files.map((file) => {
|
||||
if (file.errors.some((e) => e.code === 'too-many-files')) {
|
||||
file.errors = file.errors.filter((e) => e.code !== 'too-many-files')
|
||||
changed = true
|
||||
}
|
||||
return file
|
||||
})
|
||||
if (changed) {
|
||||
setFiles(newFiles)
|
||||
}
|
||||
}
|
||||
}, [files.length, setFiles, maxFiles])
|
||||
|
||||
return {
|
||||
files,
|
||||
setFiles,
|
||||
success,
|
||||
loading,
|
||||
error,
|
||||
setError,
|
||||
onUpload,
|
||||
maxFileSize: maxFileSize,
|
||||
maxFiles: maxFiles,
|
||||
...dropzoneProps,
|
||||
}
|
||||
}
|
||||
|
||||
export { useSupabaseUpload, type UseSupabaseUploadOptions, type UseSupabaseUploadReturn }
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "dropzone",
|
||||
"type": "registry:component",
|
||||
"title": "Dropzone (File Upload)",
|
||||
"description": "Displays a control for easier uploading of files directly to Supabase Storage.",
|
||||
"registryDependencies": ["button", "tooltip", "progress"],
|
||||
"dependencies": ["react-dropzone", "lucide-react"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/blocks/dropzone/components/dropzone.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/dropzone/hooks/use-supabase-upload.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { type EmailOtpType } from '@supabase/supabase-js'
|
||||
import { type NextRequest } from 'next/server'
|
||||
|
||||
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const token_hash = searchParams.get('token_hash')
|
||||
const type = searchParams.get('type') as EmailOtpType | null
|
||||
const next = searchParams.get('next') ?? '/'
|
||||
|
||||
if (token_hash && type) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase.auth.verifyOtp({
|
||||
type,
|
||||
token_hash,
|
||||
})
|
||||
if (!error) {
|
||||
// redirect user to specified redirect URL or root of app
|
||||
redirect(next)
|
||||
} else {
|
||||
// redirect the user to an error page with some instructions
|
||||
redirect(`/auth/error?error=${error?.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// redirect the user to an error page with some instructions
|
||||
redirect(`/auth/error?error=No token hash or type`)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Page({ searchParams }: { searchParams: { error: string } }) {
|
||||
return <div>Sorry, something went wrong. Code error: {searchParams.error}</div>
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { ForgotPasswordForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/forgot-password-form'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<ForgotPasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/login-form'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
|
||||
|
||||
export default async function ProtectedPage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error || !data?.user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
return <p>Hello {data.user.email}</p>
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return <div>Sign up success</div>
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { SignUpForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/sign-up-form'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<SignUpForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { UpdatePasswordForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/update-password-form'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<UpdatePasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
|
||||
import { Button } from '@/registry/default/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/registry/default/components/ui/card'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
const [email, setEmail] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const supabase = createClient()
|
||||
|
||||
const handleForgotPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// The url which will be included in the email. This URL needs to be configured in your redirect URLs in the Supabase dashboard at https://supabase.com/dashboard/project/_/auth/url-configuration
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: 'http://localhost:3000/update-password',
|
||||
})
|
||||
if (error) throw error
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
|
||||
<CardDescription>
|
||||
Type in your email and we'll send you a link to reset your password
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleForgotPassword}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Sending...' : 'Send reset email'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="underline underline-offset-4">
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
|
||||
import { Button } from '@/registry/default/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/registry/default/components/ui/card'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const supabase = createClient()
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
if (error) throw error
|
||||
// Update this route to redirect to an authenticated route. The user already has an active session.
|
||||
router.push('/protected')
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>Enter your email below to login to your account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleLogin}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Login'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/sign-up" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
|
||||
import { Button } from '@/registry/default/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/registry/default/components/ui/card'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [repeatPassword, setRepeatPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const supabase = createClient()
|
||||
|
||||
const handleSignUp = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
if (password !== repeatPassword) {
|
||||
setError('Passwords do not match')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
if (error) throw error
|
||||
router.push('/sign-up-success')
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Sign up</CardTitle>
|
||||
<CardDescription>Create a new account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSignUp}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="repeat-password">Repeat Password</Label>
|
||||
</div>
|
||||
<Input
|
||||
id="repeat-password"
|
||||
type="password"
|
||||
required
|
||||
value={repeatPassword}
|
||||
onChange={(e) => setRepeatPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Creating an account...' : 'Sign up'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="underline underline-offset-4">
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
|
||||
import { Button } from '@/registry/default/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/registry/default/components/ui/card'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function UpdatePasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const supabase = createClient()
|
||||
|
||||
const handleForgotPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.updateUser({ password })
|
||||
if (error) throw error
|
||||
// Update this route to redirect to an authenticated route. The user already has an active session.
|
||||
router.push('/protected')
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
|
||||
<CardDescription>Please enter your new password below.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleForgotPassword}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">New password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Saving...' : 'Save new password'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<a href="/login" className="underline underline-offset-4">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { updateSession } from '@/registry/default/clients/nextjs/lib/supabase/middleware'
|
||||
import { type NextRequest } from 'next/server'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
return await updateSession(request)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* Feel free to modify this pattern to include more paths.
|
||||
*/
|
||||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "password-based-auth-nextjs",
|
||||
"type": "registry:block",
|
||||
"title": "Password Based Auth flow for Nextjs and Supabase",
|
||||
"description": "Password Based Auth flow for Nextjs and Supabase",
|
||||
"registryDependencies": ["button", "card", "input", "label"],
|
||||
"dependencies": ["@supabase/ssr@latest"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/app/login/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/login/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/app/error/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/auth/error/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/protected/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/app/confirm/route.ts",
|
||||
"type": "registry:page",
|
||||
"target": "app/auth/confirm/route.ts"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/middleware.ts",
|
||||
"type": "registry:file",
|
||||
"target": "middleware.ts"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/app/sign-up/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/sign-up/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/app/sign-up-success/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/sign-up-success/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/components/sign-up-form.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/app/forgot-password/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/forgot-password/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/app/update-password/page.tsx",
|
||||
"type": "registry:page",
|
||||
"target": "app/update-password/page.tsx"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/components/forgot-password-form.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/components/update-password-form.tsx",
|
||||
"type": "registry:component"
|
||||
}
|
||||
]
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
|
||||
import { Button } from '@/registry/default/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/registry/default/components/ui/card'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
const [email, setEmail] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const supabase = createClient()
|
||||
|
||||
const handleForgotPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// The url which will be included in the email. This URL needs to be configured in your redirect URLs in the Supabase dashboard at https://supabase.com/dashboard/project/_/auth/url-configuration
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: 'http://localhost:3000/update-password',
|
||||
})
|
||||
if (error) throw error
|
||||
setSuccess(true)
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
|
||||
<CardDescription>
|
||||
Type in your email and we'll send you a link to reset your password
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleForgotPassword}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Sending...' : 'Send reset email'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<a href="/login" className="underline underline-offset-4">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
|
||||
import { Button } from '@/registry/default/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/registry/default/components/ui/card'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const supabase = createClient()
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
// Update this route to redirect to an authenticated route. The user already has an active session.
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>Enter your email below to login to your account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleLogin}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<a
|
||||
href="/forgot-password"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Login'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{' '}
|
||||
<a href="/sign-up" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
|
||||
import { Button } from '@/registry/default/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/registry/default/components/ui/card'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [repeatPassword, setRepeatPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const supabase = createClient()
|
||||
|
||||
const handleSignUp = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
if (password !== repeatPassword) {
|
||||
setError('Passwords do not match')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
if (error) throw error
|
||||
// TODO: Redirect to sign-up success page
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Sign up</CardTitle>
|
||||
<CardDescription>Create a new account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSignUp}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="repeat-password">Repeat Password</Label>
|
||||
</div>
|
||||
<Input
|
||||
id="repeat-password"
|
||||
type="password"
|
||||
required
|
||||
value={repeatPassword}
|
||||
onChange={(e) => setRepeatPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Creating an account...' : 'Sign up'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
|
||||
import { Button } from '@/registry/default/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/registry/default/components/ui/card'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function UpdatePasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const supabase = createClient()
|
||||
|
||||
const handleForgotPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.updateUser({ password })
|
||||
if (error) throw error
|
||||
|
||||
// TODO:Update this route to redirect to an authenticated route. The user already has an active session.
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : 'An error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
|
||||
<CardDescription>Please enter your new password below.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleForgotPassword}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">New password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Saving...' : 'Save new password'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "password-based-auth-react",
|
||||
"type": "registry:block",
|
||||
"title": "Password Based Auth flow for React and Supabase",
|
||||
"description": "Password Based Auth flow for React and Supabase",
|
||||
"registryDependencies": ["button", "card", "input", "label"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-react/components/login-form.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-react/components/forgot-password-form.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-react/components/update-password-form.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/clients/react/lib/supabase/client.ts",
|
||||
"type": "registry:lib"
|
||||
}
|
||||
],
|
||||
"dependencies": ["@supabase/supabase-js@latest"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { MousePointer2 } from 'lucide-react'
|
||||
|
||||
export const Cursor = ({
|
||||
className,
|
||||
style,
|
||||
color,
|
||||
name,
|
||||
}: {
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
color: string
|
||||
name: string
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('pointer-events-none', className)} style={style}>
|
||||
<MousePointer2 color={color} fill={color} size={30} />
|
||||
|
||||
<div
|
||||
className="mt-1 px-2 py-1 rounded text-xs font-bold text-white text-center"
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'
|
||||
import { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'
|
||||
|
||||
export const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {
|
||||
const { cursors } = useRealtimeCursors({ roomName, username })
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(cursors).map((id) => (
|
||||
<Cursor
|
||||
key={id}
|
||||
className="absolute transform ease-linear duration-70 z-50"
|
||||
style={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,
|
||||
}}
|
||||
color={cursors[id].color}
|
||||
name={cursors[id].user.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
|
||||
import { RealtimeChannel } from '@supabase/supabase-js'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
const generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)`
|
||||
|
||||
const generateRandomNumber = () => Math.floor(Math.random() * 100)
|
||||
|
||||
const EVENT_NAME = 'realtime-cursor-move'
|
||||
|
||||
type CursorEventPayload = {
|
||||
position: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
user: {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
color: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export const useRealtimeCursors = ({
|
||||
roomName,
|
||||
username,
|
||||
}: {
|
||||
roomName: string
|
||||
username: string
|
||||
}) => {
|
||||
const [color] = useState(generateRandomColor())
|
||||
const [userId] = useState(generateRandomNumber())
|
||||
const [cursors, setCursors] = useState<Record<string, CursorEventPayload>>({})
|
||||
|
||||
const channelRef = useRef<RealtimeChannel | null>(null)
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
const { clientX, clientY } = event
|
||||
|
||||
const payload: CursorEventPayload = {
|
||||
position: {
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
},
|
||||
user: {
|
||||
id: userId,
|
||||
name: username,
|
||||
},
|
||||
color: color,
|
||||
timestamp: new Date().getTime(),
|
||||
}
|
||||
|
||||
channelRef.current?.send({
|
||||
type: 'broadcast',
|
||||
event: EVENT_NAME,
|
||||
payload: payload,
|
||||
})
|
||||
},
|
||||
[color, userId, username]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase.channel(roomName)
|
||||
channelRef.current = channel
|
||||
|
||||
channel
|
||||
.on('broadcast', { event: EVENT_NAME }, (data: { payload: CursorEventPayload }) => {
|
||||
const { user } = data.payload
|
||||
// Don't render your own cursor
|
||||
if (user.id === userId) return
|
||||
|
||||
setCursors((prev) => {
|
||||
if (prev[userId]) {
|
||||
delete prev[userId]
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[user.id]: data.payload,
|
||||
}
|
||||
})
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
return () => {
|
||||
channel.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Add event listener for mousemove
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [handleMouseMove])
|
||||
|
||||
return { cursors }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "realtime-cursor",
|
||||
"type": "registry:component",
|
||||
"title": "Realtime Cursor",
|
||||
"description": "Component which renders realtime cursors from other users in a room.",
|
||||
"registryDependencies": [],
|
||||
"dependencies": ["lucide-react"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/blocks/realtime-cursor/components/cursor.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Do not run code between createServerClient and
|
||||
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
|
||||
// issues with users being randomly logged out.
|
||||
|
||||
// IMPORTANT: DO NOT REMOVE auth.getUser()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (
|
||||
!user &&
|
||||
!request.nextUrl.pathname.startsWith('/login') &&
|
||||
!request.nextUrl.pathname.startsWith('/auth')
|
||||
) {
|
||||
// no user, potentially respond by redirecting the user to the login page
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = '/login'
|
||||
return NextResponse.redirect(url)
|
||||
}
|
||||
|
||||
// IMPORTANT: You *must* return the supabaseResponse object as it is.
|
||||
// If you're creating a new response object with NextResponse.next() make sure to:
|
||||
// 1. Pass the request in it, like so:
|
||||
// const myNewResponse = NextResponse.next({ request })
|
||||
// 2. Copy over the cookies, like so:
|
||||
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
||||
// 3. Change the myNewResponse object to fit your needs, but avoid changing
|
||||
// the cookies!
|
||||
// 4. Finally:
|
||||
// return myNewResponse
|
||||
// If this is not done, you may be causing the browser and server to go out
|
||||
// of sync and terminate the user's session prematurely!
|
||||
|
||||
return supabaseResponse
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies()
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
)
|
||||
} catch {
|
||||
// The `setAll` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "supabase-client-nextjs",
|
||||
"type": "registry:lib",
|
||||
"title": "Supabase Client for Next.js",
|
||||
"description": "",
|
||||
"registryDependencies": [],
|
||||
"dependencies": ["@supabase/ssr@latest"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
|
||||
"type": "registry:lib"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
|
||||
"type": "registry:lib"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/clients/nextjs/lib/supabase/server.ts",
|
||||
"type": "registry:lib"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createClient as createSupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
export function createClient() {
|
||||
return createSupabaseClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "supabase-client-react",
|
||||
"type": "registry:lib",
|
||||
"title": "Supabase Client for React",
|
||||
"description": "",
|
||||
"registryDependencies": [],
|
||||
"dependencies": ["@supabase/supabase-js@latest"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/clients/react/lib/supabase/client.ts",
|
||||
"type": "registry:lib"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
// Use this function to create a client for the browser. You should pass the env variables through a loader and use them
|
||||
// to instantiate the client.
|
||||
export function createClient(supabaseUrl: string, supabaseAnonKey: string) {
|
||||
return createBrowserClient(supabaseUrl, supabaseAnonKey)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'
|
||||
|
||||
export function createClient(request: Request) {
|
||||
const headers = new Headers()
|
||||
|
||||
const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return parseCookieHeader(request.headers.get('Cookie') ?? '') as {
|
||||
name: string
|
||||
value: string
|
||||
}[]
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return { supabase, headers }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "supabase-client-remix",
|
||||
"type": "registry:lib",
|
||||
"title": "Supabase Client for Remix",
|
||||
"description": "",
|
||||
"registryDependencies": [],
|
||||
"dependencies": ["@supabase/ssr@latest"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/clients/remix/lib/supabase.client.ts",
|
||||
"type": "registry:lib"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/clients/remix/lib/supabase.server.ts",
|
||||
"type": "registry:lib"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient(process.env.VITE_SUPABASE_URL!, process.env.VITE_SUPABASE_ANON_KEY!)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { parseCookies, setCookie } from '@tanstack/react-start/server'
|
||||
|
||||
export function getSupabaseServerClient() {
|
||||
return createServerClient(process.env.VITE_SUPABASE_URL!, process.env.VITE_SUPABASE_ANON_KEY!, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return Object.entries(parseCookies()).map(
|
||||
([name, value]) =>
|
||||
({
|
||||
name,
|
||||
value,
|
||||
}) as { name: string; value: string }
|
||||
)
|
||||
},
|
||||
setAll(cookies) {
|
||||
cookies.forEach((cookie) => {
|
||||
setCookie(cookie.name, cookie.value)
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "supabase-client-tanstack",
|
||||
"type": "registry:lib",
|
||||
"title": "Supabase Client for Tanstack Start",
|
||||
"description": "",
|
||||
"registryDependencies": [],
|
||||
"dependencies": ["@supabase/ssr@latest"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
|
||||
"type": "registry:lib"
|
||||
},
|
||||
{
|
||||
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
|
||||
"type": "registry:lib"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Card.displayName = 'Card'
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardContent.displayName = 'CardContent'
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
||||
)
|
||||
)
|
||||
CardFooter.displayName = 'CardFooter'
|
||||
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn('bg-primary/20 relative h-2 w-full overflow-hidden rounded-full', className)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<'textarea'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Dropzone,
|
||||
DropzoneContent,
|
||||
DropzoneEmptyState,
|
||||
} from '@/registry/default/blocks/dropzone/components/dropzone'
|
||||
import { useSupabaseUpload } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'
|
||||
|
||||
const FileUploadDemo = () => {
|
||||
const props = useSupabaseUpload({
|
||||
bucketName: 'test',
|
||||
path: 'test',
|
||||
allowedMimeTypes: ['image/*'],
|
||||
maxFiles: 2,
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropzone {...props}>
|
||||
<DropzoneEmptyState />
|
||||
<DropzoneContent />
|
||||
</Dropzone>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileUploadDemo
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { RealtimeCursors } from '@/registry/default/blocks/realtime-cursor/components/realtime-cursors'
|
||||
import { Input } from '@/registry/default/components/ui/input'
|
||||
import { Label } from '@/registry/default/components/ui/label'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const names = ['Eren', 'Armin', 'Mikasa', 'Reiner', 'Levi', 'Bertholdt']
|
||||
|
||||
const RealtimeCursorDemo = () => {
|
||||
const [username, setUsername] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setUsername(names[Math.floor(Math.random() * names.length)])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-40">
|
||||
<Label>Username</Label>
|
||||
<Input value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||
<RealtimeCursors roomName="realtime-cursor-example" username={username} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RealtimeCursorDemo
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,17 @@
|
||||
import { type Registry } from 'shadcn/registry'
|
||||
|
||||
import { examples } from '@/registry/examples'
|
||||
import { blocks } from './blocks'
|
||||
import { clients } from './clients'
|
||||
|
||||
export const registry = {
|
||||
name: 'Supabase UI Library',
|
||||
homepage: 'https://supabase.com/ui',
|
||||
items: [
|
||||
...blocks,
|
||||
...clients,
|
||||
|
||||
// Internal use only.
|
||||
...examples,
|
||||
],
|
||||
} satisfies Registry
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Registry } from '@/registry/schema'
|
||||
import { examples } from '@/registry//examples'
|
||||
import { fragments } from '@/registry/fragments'
|
||||
import { charts } from '@/registry/charts'
|
||||
|
||||
export const registry: Registry = [...fragments, ...examples, ...charts]
|
||||
@@ -1,68 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const blockChunkSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
component: z.any(),
|
||||
file: z.string(),
|
||||
code: z.string().optional(),
|
||||
container: z
|
||||
.object({
|
||||
className: z.string().nullish(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const backendProviderValues = ['next-auth', 'supabase', 'auth0']
|
||||
|
||||
const isBackendProvider = (value: unknown): value is (typeof backendProviderValues)[number] =>
|
||||
backendProviderValues.includes(value as string)
|
||||
|
||||
export const registryEntrySchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
devDependencies: z.array(z.string()).optional(),
|
||||
registryDependencies: z.array(z.string()).optional(),
|
||||
files: z.array(z.string()),
|
||||
source: z.string().optional(),
|
||||
type: z.enum([
|
||||
'components:ui',
|
||||
'components:fragment',
|
||||
'components:component',
|
||||
'components:example',
|
||||
'components:block',
|
||||
'docs:example',
|
||||
]),
|
||||
category: z.string().optional(),
|
||||
backendProviders: z.array(z.string()).optional(),
|
||||
subcategory: z.string().optional(),
|
||||
chunks: z.array(blockChunkSchema).optional(),
|
||||
// Optional path of file that is exported from a package
|
||||
optionalPath: z.string().optional(),
|
||||
})
|
||||
|
||||
export const registrySchema = z.array(registryEntrySchema)
|
||||
|
||||
export type RegistryEntry = z.infer<typeof registryEntrySchema>
|
||||
|
||||
export type Registry = z.infer<typeof registrySchema>
|
||||
|
||||
export const blockSchema = registryEntrySchema.extend({
|
||||
type: z.literal('components:block'),
|
||||
style: z.enum(['default', 'new-york']),
|
||||
component: z.any(),
|
||||
container: z
|
||||
.object({
|
||||
height: z.string().optional(),
|
||||
className: z.string().nullish(),
|
||||
})
|
||||
.optional(),
|
||||
code: z.string(),
|
||||
backendProvidersCode: z.any().optional(),
|
||||
highlightedCode: z.string(),
|
||||
})
|
||||
|
||||
export type Block = z.infer<typeof blockSchema>
|
||||
|
||||
export type BlockChunk = z.infer<typeof blockChunkSchema>
|
||||
@@ -3,10 +3,6 @@ export const styles = [
|
||||
name: 'default',
|
||||
label: 'Default',
|
||||
},
|
||||
// {
|
||||
// name: 'new-york',
|
||||
// label: 'New York',
|
||||
// },
|
||||
] as const
|
||||
|
||||
export type Style = (typeof styles)[number]
|
||||
@@ -1,636 +0,0 @@
|
||||
export const themes = [
|
||||
{
|
||||
name: 'zinc',
|
||||
label: 'Zinc',
|
||||
activeColor: {
|
||||
light: '240 5.9% 10%',
|
||||
dark: '240 5.2% 33.9%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '240 10% 3.9%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '240 10% 3.9%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '240 10% 3.9%',
|
||||
primary: '240 5.9% 10%',
|
||||
'primary-foreground': '0 0% 98%',
|
||||
secondary: '240 4.8% 95.9%',
|
||||
'secondary-foreground': '240 5.9% 10%',
|
||||
muted: '240 4.8% 95.9%',
|
||||
'muted-foreground': '240 3.8% 46.1%',
|
||||
accent: '240 4.8% 95.9%',
|
||||
'accent-foreground': '240 5.9% 10%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '0 0% 98%',
|
||||
border: '240 5.9% 90%',
|
||||
input: '240 5.9% 90%',
|
||||
ring: '240 5.9% 10%',
|
||||
radius: '0.5rem',
|
||||
},
|
||||
dark: {
|
||||
background: '240 10% 3.9%',
|
||||
foreground: '0 0% 98%',
|
||||
card: '240 10% 3.9%',
|
||||
'card-foreground': '0 0% 98%',
|
||||
popover: '240 10% 3.9%',
|
||||
'popover-foreground': '0 0% 98%',
|
||||
primary: '0 0% 98%',
|
||||
'primary-foreground': '240 5.9% 10%',
|
||||
secondary: '240 3.7% 15.9%',
|
||||
'secondary-foreground': '0 0% 98%',
|
||||
muted: '240 3.7% 15.9%',
|
||||
'muted-foreground': '240 5% 64.9%',
|
||||
accent: '240 3.7% 15.9%',
|
||||
'accent-foreground': '0 0% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '0 0% 98%',
|
||||
border: '240 3.7% 15.9%',
|
||||
input: '240 3.7% 15.9%',
|
||||
ring: '240 4.9% 83.9%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'slate',
|
||||
label: 'Slate',
|
||||
activeColor: {
|
||||
light: '215.4 16.3% 46.9%',
|
||||
dark: '215.3 19.3% 34.5%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '222.2 84% 4.9%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '222.2 84% 4.9%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '222.2 84% 4.9%',
|
||||
primary: '222.2 47.4% 11.2%',
|
||||
'primary-foreground': '210 40% 98%',
|
||||
secondary: '210 40% 96.1%',
|
||||
'secondary-foreground': '222.2 47.4% 11.2%',
|
||||
muted: '210 40% 96.1%',
|
||||
'muted-foreground': '215.4 16.3% 46.9%',
|
||||
accent: '210 40% 96.1%',
|
||||
'accent-foreground': '222.2 47.4% 11.2%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '210 40% 98%',
|
||||
border: '214.3 31.8% 91.4%',
|
||||
input: '214.3 31.8% 91.4%',
|
||||
ring: '222.2 84% 4.9%',
|
||||
radius: '0.5rem',
|
||||
},
|
||||
dark: {
|
||||
background: '222.2 84% 4.9%',
|
||||
foreground: '210 40% 98%',
|
||||
card: '222.2 84% 4.9%',
|
||||
'card-foreground': '210 40% 98%',
|
||||
popover: '222.2 84% 4.9%',
|
||||
'popover-foreground': '210 40% 98%',
|
||||
primary: '210 40% 98%',
|
||||
'primary-foreground': '222.2 47.4% 11.2%',
|
||||
secondary: '217.2 32.6% 17.5%',
|
||||
'secondary-foreground': '210 40% 98%',
|
||||
muted: '217.2 32.6% 17.5%',
|
||||
'muted-foreground': '215 20.2% 65.1%',
|
||||
accent: '217.2 32.6% 17.5%',
|
||||
'accent-foreground': '210 40% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '210 40% 98%',
|
||||
border: '217.2 32.6% 17.5%',
|
||||
input: '217.2 32.6% 17.5%',
|
||||
ring: '212.7 26.8% 83.9',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'stone',
|
||||
label: 'Stone',
|
||||
activeColor: {
|
||||
light: '25 5.3% 44.7%',
|
||||
dark: '33.3 5.5% 32.4%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '20 14.3% 4.1%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '20 14.3% 4.1%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '20 14.3% 4.1%',
|
||||
primary: '24 9.8% 10%',
|
||||
'primary-foreground': '60 9.1% 97.8%',
|
||||
secondary: '60 4.8% 95.9%',
|
||||
'secondary-foreground': '24 9.8% 10%',
|
||||
muted: '60 4.8% 95.9%',
|
||||
'muted-foreground': '25 5.3% 44.7%',
|
||||
accent: '60 4.8% 95.9%',
|
||||
'accent-foreground': '24 9.8% 10%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '60 9.1% 97.8%',
|
||||
border: '20 5.9% 90%',
|
||||
input: '20 5.9% 90%',
|
||||
ring: '20 14.3% 4.1%',
|
||||
radius: '0.95rem',
|
||||
},
|
||||
dark: {
|
||||
background: '20 14.3% 4.1%',
|
||||
foreground: '60 9.1% 97.8%',
|
||||
card: '20 14.3% 4.1%',
|
||||
'card-foreground': '60 9.1% 97.8%',
|
||||
popover: '20 14.3% 4.1%',
|
||||
'popover-foreground': '60 9.1% 97.8%',
|
||||
primary: '60 9.1% 97.8%',
|
||||
'primary-foreground': '24 9.8% 10%',
|
||||
secondary: '12 6.5% 15.1%',
|
||||
'secondary-foreground': '60 9.1% 97.8%',
|
||||
muted: '12 6.5% 15.1%',
|
||||
'muted-foreground': '24 5.4% 63.9%',
|
||||
accent: '12 6.5% 15.1%',
|
||||
'accent-foreground': '60 9.1% 97.8%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '60 9.1% 97.8%',
|
||||
border: '12 6.5% 15.1%',
|
||||
input: '12 6.5% 15.1%',
|
||||
ring: '24 5.7% 82.9%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gray',
|
||||
label: 'Gray',
|
||||
activeColor: {
|
||||
light: '220 8.9% 46.1%',
|
||||
dark: '215 13.8% 34.1%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '224 71.4% 4.1%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '224 71.4% 4.1%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '224 71.4% 4.1%',
|
||||
primary: '220.9 39.3% 11%',
|
||||
'primary-foreground': '210 20% 98%',
|
||||
secondary: '220 14.3% 95.9%',
|
||||
'secondary-foreground': '220.9 39.3% 11%',
|
||||
muted: '220 14.3% 95.9%',
|
||||
'muted-foreground': '220 8.9% 46.1%',
|
||||
accent: '220 14.3% 95.9%',
|
||||
'accent-foreground': '220.9 39.3% 11%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '210 20% 98%',
|
||||
border: '220 13% 91%',
|
||||
input: '220 13% 91%',
|
||||
ring: '224 71.4% 4.1%',
|
||||
radius: '0.35rem',
|
||||
},
|
||||
dark: {
|
||||
background: '224 71.4% 4.1%',
|
||||
foreground: '210 20% 98%',
|
||||
card: '224 71.4% 4.1%',
|
||||
'card-foreground': '210 20% 98%',
|
||||
popover: '224 71.4% 4.1%',
|
||||
'popover-foreground': '210 20% 98%',
|
||||
primary: '210 20% 98%',
|
||||
'primary-foreground': '220.9 39.3% 11%',
|
||||
secondary: '215 27.9% 16.9%',
|
||||
'secondary-foreground': '210 20% 98%',
|
||||
muted: '215 27.9% 16.9%',
|
||||
'muted-foreground': '217.9 10.6% 64.9%',
|
||||
accent: '215 27.9% 16.9%',
|
||||
'accent-foreground': '210 20% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '210 20% 98%',
|
||||
border: '215 27.9% 16.9%',
|
||||
input: '215 27.9% 16.9%',
|
||||
ring: '216 12.2% 83.9%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'neutral',
|
||||
label: 'Neutral',
|
||||
activeColor: {
|
||||
light: '0 0% 45.1%',
|
||||
dark: '0 0% 32.2%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '0 0% 3.9%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '0 0% 3.9%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '0 0% 3.9%',
|
||||
primary: '0 0% 9%',
|
||||
'primary-foreground': '0 0% 98%',
|
||||
secondary: '0 0% 96.1%',
|
||||
'secondary-foreground': '0 0% 9%',
|
||||
muted: '0 0% 96.1%',
|
||||
'muted-foreground': '0 0% 45.1%',
|
||||
accent: '0 0% 96.1%',
|
||||
'accent-foreground': '0 0% 9%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '0 0% 98%',
|
||||
border: '0 0% 89.8%',
|
||||
input: '0 0% 89.8%',
|
||||
ring: '0 0% 3.9%',
|
||||
},
|
||||
dark: {
|
||||
background: '0 0% 3.9%',
|
||||
foreground: '0 0% 98%',
|
||||
card: '0 0% 3.9%',
|
||||
'card-foreground': '0 0% 98%',
|
||||
popover: '0 0% 3.9%',
|
||||
'popover-foreground': '0 0% 98%',
|
||||
primary: '0 0% 98%',
|
||||
'primary-foreground': '0 0% 9%',
|
||||
secondary: '0 0% 14.9%',
|
||||
'secondary-foreground': '0 0% 98%',
|
||||
muted: '0 0% 14.9%',
|
||||
'muted-foreground': '0 0% 63.9%',
|
||||
accent: '0 0% 14.9%',
|
||||
'accent-foreground': '0 0% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '0 0% 98%',
|
||||
border: '0 0% 14.9%',
|
||||
input: '0 0% 14.9%',
|
||||
ring: '0 0% 83.1%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'red',
|
||||
label: 'Red',
|
||||
activeColor: {
|
||||
light: '0 72.2% 50.6%',
|
||||
dark: '0 72.2% 50.6%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '0 0% 3.9%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '0 0% 3.9%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '0 0% 3.9%',
|
||||
primary: '0 72.2% 50.6%',
|
||||
'primary-foreground': '0 85.7% 97.3%',
|
||||
secondary: '0 0% 96.1%',
|
||||
'secondary-foreground': '0 0% 9%',
|
||||
muted: '0 0% 96.1%',
|
||||
'muted-foreground': '0 0% 45.1%',
|
||||
accent: '0 0% 96.1%',
|
||||
'accent-foreground': '0 0% 9%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '0 0% 98%',
|
||||
border: '0 0% 89.8%',
|
||||
input: '0 0% 89.8%',
|
||||
ring: '0 72.2% 50.6%',
|
||||
radius: '0.4rem',
|
||||
},
|
||||
dark: {
|
||||
background: '0 0% 3.9%',
|
||||
foreground: '0 0% 98%',
|
||||
card: '0 0% 3.9%',
|
||||
'card-foreground': '0 0% 98%',
|
||||
popover: '0 0% 3.9%',
|
||||
'popover-foreground': '0 0% 98%',
|
||||
primary: '0 72.2% 50.6%',
|
||||
'primary-foreground': '0 85.7% 97.3%',
|
||||
secondary: '0 0% 14.9%',
|
||||
'secondary-foreground': '0 0% 98%',
|
||||
muted: '0 0% 14.9%',
|
||||
'muted-foreground': '0 0% 63.9%',
|
||||
accent: '0 0% 14.9%',
|
||||
'accent-foreground': '0 0% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '0 0% 98%',
|
||||
border: '0 0% 14.9%',
|
||||
input: '0 0% 14.9%',
|
||||
ring: '0 72.2% 50.6%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rose',
|
||||
label: 'Rose',
|
||||
activeColor: {
|
||||
light: '346.8 77.2% 49.8%',
|
||||
dark: '346.8 77.2% 49.8%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '240 10% 3.9%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '240 10% 3.9%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '240 10% 3.9%',
|
||||
primary: '346.8 77.2% 49.8%',
|
||||
'primary-foreground': '355.7 100% 97.3%',
|
||||
secondary: '240 4.8% 95.9%',
|
||||
'secondary-foreground': '240 5.9% 10%',
|
||||
muted: '240 4.8% 95.9%',
|
||||
'muted-foreground': '240 3.8% 46.1%',
|
||||
accent: '240 4.8% 95.9%',
|
||||
'accent-foreground': '240 5.9% 10%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '0 0% 98%',
|
||||
border: '240 5.9% 90%',
|
||||
input: '240 5.9% 90%',
|
||||
ring: '346.8 77.2% 49.8%',
|
||||
radius: '0.5rem',
|
||||
},
|
||||
dark: {
|
||||
background: '20 14.3% 4.1%',
|
||||
foreground: '0 0% 95%',
|
||||
popover: '0 0% 9%',
|
||||
'popover-foreground': '0 0% 95%',
|
||||
card: '24 9.8% 10%',
|
||||
'card-foreground': '0 0% 95%',
|
||||
primary: '346.8 77.2% 49.8%',
|
||||
'primary-foreground': '355.7 100% 97.3%',
|
||||
secondary: '240 3.7% 15.9%',
|
||||
'secondary-foreground': '0 0% 98%',
|
||||
muted: '0 0% 15%',
|
||||
'muted-foreground': '240 5% 64.9%',
|
||||
accent: '12 6.5% 15.1%',
|
||||
'accent-foreground': '0 0% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '0 85.7% 97.3%',
|
||||
border: '240 3.7% 15.9%',
|
||||
input: '240 3.7% 15.9%',
|
||||
ring: '346.8 77.2% 49.8%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'orange',
|
||||
label: 'Orange',
|
||||
activeColor: {
|
||||
light: '24.6 95% 53.1%',
|
||||
dark: '20.5 90.2% 48.2%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '20 14.3% 4.1%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '20 14.3% 4.1%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '20 14.3% 4.1%',
|
||||
primary: '24.6 95% 53.1%',
|
||||
'primary-foreground': '60 9.1% 97.8%',
|
||||
secondary: '60 4.8% 95.9%',
|
||||
'secondary-foreground': '24 9.8% 10%',
|
||||
muted: '60 4.8% 95.9%',
|
||||
'muted-foreground': '25 5.3% 44.7%',
|
||||
accent: '60 4.8% 95.9%',
|
||||
'accent-foreground': '24 9.8% 10%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '60 9.1% 97.8%',
|
||||
border: '20 5.9% 90%',
|
||||
input: '20 5.9% 90%',
|
||||
ring: '24.6 95% 53.1%',
|
||||
radius: '0.95rem',
|
||||
},
|
||||
dark: {
|
||||
background: '20 14.3% 4.1%',
|
||||
foreground: '60 9.1% 97.8%',
|
||||
card: '20 14.3% 4.1%',
|
||||
'card-foreground': '60 9.1% 97.8%',
|
||||
popover: '20 14.3% 4.1%',
|
||||
'popover-foreground': '60 9.1% 97.8%',
|
||||
primary: '20.5 90.2% 48.2%',
|
||||
'primary-foreground': '60 9.1% 97.8%',
|
||||
secondary: '12 6.5% 15.1%',
|
||||
'secondary-foreground': '60 9.1% 97.8%',
|
||||
muted: '12 6.5% 15.1%',
|
||||
'muted-foreground': '24 5.4% 63.9%',
|
||||
accent: '12 6.5% 15.1%',
|
||||
'accent-foreground': '60 9.1% 97.8%',
|
||||
destructive: '0 72.2% 50.6%',
|
||||
'destructive-foreground': '60 9.1% 97.8%',
|
||||
border: '12 6.5% 15.1%',
|
||||
input: '12 6.5% 15.1%',
|
||||
ring: '20.5 90.2% 48.2%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'green',
|
||||
label: 'Green',
|
||||
activeColor: {
|
||||
light: '142.1 76.2% 36.3%',
|
||||
dark: '142.1 70.6% 45.3%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '240 10% 3.9%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '240 10% 3.9%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '240 10% 3.9%',
|
||||
primary: '142.1 76.2% 36.3%',
|
||||
'primary-foreground': '355.7 100% 97.3%',
|
||||
secondary: '240 4.8% 95.9%',
|
||||
'secondary-foreground': '240 5.9% 10%',
|
||||
muted: '240 4.8% 95.9%',
|
||||
'muted-foreground': '240 3.8% 46.1%',
|
||||
accent: '240 4.8% 95.9%',
|
||||
'accent-foreground': '240 5.9% 10%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '0 0% 98%',
|
||||
border: '240 5.9% 90%',
|
||||
input: '240 5.9% 90%',
|
||||
ring: '142.1 76.2% 36.3%',
|
||||
},
|
||||
dark: {
|
||||
background: '20 14.3% 4.1%',
|
||||
foreground: '0 0% 95%',
|
||||
popover: '0 0% 9%',
|
||||
'popover-foreground': '0 0% 95%',
|
||||
card: '24 9.8% 10%',
|
||||
'card-foreground': '0 0% 95%',
|
||||
primary: '142.1 70.6% 45.3%',
|
||||
'primary-foreground': '144.9 80.4% 10%',
|
||||
secondary: '240 3.7% 15.9%',
|
||||
'secondary-foreground': '0 0% 98%',
|
||||
muted: '0 0% 15%',
|
||||
'muted-foreground': '240 5% 64.9%',
|
||||
accent: '12 6.5% 15.1%',
|
||||
'accent-foreground': '0 0% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '0 85.7% 97.3%',
|
||||
border: '240 3.7% 15.9%',
|
||||
input: '240 3.7% 15.9%',
|
||||
ring: '142.4 71.8% 29.2%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'blue',
|
||||
label: 'Blue',
|
||||
activeColor: {
|
||||
light: '221.2 83.2% 53.3%',
|
||||
dark: '217.2 91.2% 59.8%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '222.2 84% 4.9%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '222.2 84% 4.9%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '222.2 84% 4.9%',
|
||||
primary: '221.2 83.2% 53.3%',
|
||||
'primary-foreground': '210 40% 98%',
|
||||
secondary: '210 40% 96.1%',
|
||||
'secondary-foreground': '222.2 47.4% 11.2%',
|
||||
muted: '210 40% 96.1%',
|
||||
'muted-foreground': '215.4 16.3% 46.9%',
|
||||
accent: '210 40% 96.1%',
|
||||
'accent-foreground': '222.2 47.4% 11.2%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '210 40% 98%',
|
||||
border: '214.3 31.8% 91.4%',
|
||||
input: '214.3 31.8% 91.4%',
|
||||
ring: '221.2 83.2% 53.3%',
|
||||
},
|
||||
dark: {
|
||||
background: '222.2 84% 4.9%',
|
||||
foreground: '210 40% 98%',
|
||||
card: '222.2 84% 4.9%',
|
||||
'card-foreground': '210 40% 98%',
|
||||
popover: '222.2 84% 4.9%',
|
||||
'popover-foreground': '210 40% 98%',
|
||||
primary: '217.2 91.2% 59.8%',
|
||||
'primary-foreground': '222.2 47.4% 11.2%',
|
||||
secondary: '217.2 32.6% 17.5%',
|
||||
'secondary-foreground': '210 40% 98%',
|
||||
muted: '217.2 32.6% 17.5%',
|
||||
'muted-foreground': '215 20.2% 65.1%',
|
||||
accent: '217.2 32.6% 17.5%',
|
||||
'accent-foreground': '210 40% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '210 40% 98%',
|
||||
border: '217.2 32.6% 17.5%',
|
||||
input: '217.2 32.6% 17.5%',
|
||||
ring: '224.3 76.3% 48%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'yellow',
|
||||
label: 'Yellow',
|
||||
activeColor: {
|
||||
light: '47.9 95.8% 53.1%',
|
||||
dark: '47.9 95.8% 53.1%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '20 14.3% 4.1%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '20 14.3% 4.1%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '20 14.3% 4.1%',
|
||||
primary: '47.9 95.8% 53.1%',
|
||||
'primary-foreground': '26 83.3% 14.1%',
|
||||
secondary: '60 4.8% 95.9%',
|
||||
'secondary-foreground': '24 9.8% 10%',
|
||||
muted: '60 4.8% 95.9%',
|
||||
'muted-foreground': '25 5.3% 44.7%',
|
||||
accent: '60 4.8% 95.9%',
|
||||
'accent-foreground': '24 9.8% 10%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '60 9.1% 97.8%',
|
||||
border: '20 5.9% 90%',
|
||||
input: '20 5.9% 90%',
|
||||
ring: '20 14.3% 4.1%',
|
||||
radius: '0.95rem',
|
||||
},
|
||||
dark: {
|
||||
background: '20 14.3% 4.1%',
|
||||
foreground: '60 9.1% 97.8%',
|
||||
card: '20 14.3% 4.1%',
|
||||
'card-foreground': '60 9.1% 97.8%',
|
||||
popover: '20 14.3% 4.1%',
|
||||
'popover-foreground': '60 9.1% 97.8%',
|
||||
primary: '47.9 95.8% 53.1%',
|
||||
'primary-foreground': '26 83.3% 14.1%',
|
||||
secondary: '12 6.5% 15.1%',
|
||||
'secondary-foreground': '60 9.1% 97.8%',
|
||||
muted: '12 6.5% 15.1%',
|
||||
'muted-foreground': '24 5.4% 63.9%',
|
||||
accent: '12 6.5% 15.1%',
|
||||
'accent-foreground': '60 9.1% 97.8%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '60 9.1% 97.8%',
|
||||
border: '12 6.5% 15.1%',
|
||||
input: '12 6.5% 15.1%',
|
||||
ring: '35.5 91.7% 32.9%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'violet',
|
||||
label: 'Violet',
|
||||
activeColor: {
|
||||
light: '262.1 83.3% 57.8%',
|
||||
dark: '263.4 70% 50.4%',
|
||||
},
|
||||
cssVars: {
|
||||
light: {
|
||||
background: '0 0% 100%',
|
||||
foreground: '224 71.4% 4.1%',
|
||||
card: '0 0% 100%',
|
||||
'card-foreground': '224 71.4% 4.1%',
|
||||
popover: '0 0% 100%',
|
||||
'popover-foreground': '224 71.4% 4.1%',
|
||||
primary: '262.1 83.3% 57.8%',
|
||||
'primary-foreground': '210 20% 98%',
|
||||
secondary: '220 14.3% 95.9%',
|
||||
'secondary-foreground': '220.9 39.3% 11%',
|
||||
muted: '220 14.3% 95.9%',
|
||||
'muted-foreground': '220 8.9% 46.1%',
|
||||
accent: '220 14.3% 95.9%',
|
||||
'accent-foreground': '220.9 39.3% 11%',
|
||||
destructive: '0 84.2% 60.2%',
|
||||
'destructive-foreground': '210 20% 98%',
|
||||
border: '220 13% 91%',
|
||||
input: '220 13% 91%',
|
||||
ring: '262.1 83.3% 57.8%',
|
||||
},
|
||||
dark: {
|
||||
background: '224 71.4% 4.1%',
|
||||
foreground: '210 20% 98%',
|
||||
card: '224 71.4% 4.1%',
|
||||
'card-foreground': '210 20% 98%',
|
||||
popover: '224 71.4% 4.1%',
|
||||
'popover-foreground': '210 20% 98%',
|
||||
primary: '263.4 70% 50.4%',
|
||||
'primary-foreground': '210 20% 98%',
|
||||
secondary: '215 27.9% 16.9%',
|
||||
'secondary-foreground': '210 20% 98%',
|
||||
muted: '215 27.9% 16.9%',
|
||||
'muted-foreground': '217.9 10.6% 64.9%',
|
||||
accent: '215 27.9% 16.9%',
|
||||
'accent-foreground': '210 20% 98%',
|
||||
destructive: '0 62.8% 30.6%',
|
||||
'destructive-foreground': '210 20% 98%',
|
||||
border: '215 27.9% 16.9%',
|
||||
input: '215 27.9% 16.9%',
|
||||
ring: '263.4 70% 50.4%',
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
|
||||
export type Theme = (typeof themes)[number]
|
||||
@@ -0,0 +1,23 @@
|
||||
import lodash from 'lodash'
|
||||
import { Registry } from 'shadcn/registry'
|
||||
const { uniq, uniqBy } = lodash
|
||||
|
||||
const registryItemAppend = (item: Registry['items'][number], items: Registry['items']) => {
|
||||
const neededRegDependencies = [
|
||||
...(item.registryDependencies || []),
|
||||
...items.flatMap((i) => i.registryDependencies),
|
||||
]
|
||||
const neededDependencies = [...(item.dependencies || []), ...items.flatMap((i) => i.dependencies)]
|
||||
const neededFiles = [...(item.files || []), ...items.flatMap((i) => i.files)]
|
||||
|
||||
const registryBlock = {
|
||||
...item,
|
||||
registryDependencies: uniq(neededRegDependencies),
|
||||
dependencies: uniq(neededDependencies),
|
||||
files: uniqBy(neededFiles, (file) => file?.path),
|
||||
}
|
||||
|
||||
return registryBlock
|
||||
}
|
||||
|
||||
export { registryItemAppend }
|
||||
Reference in new issue
Block a user