mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Chore/move framework (#34426)
* init * Update component-preview.tsx * move framework selector * simplified framework selection * moaar * remove * Update framework-selector.tsx * side menu item fix --------- Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
This commit is contained in:
1 parent
c8fad32371
commit
e8698432bc
10 files changed
+233
-176
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { FrameworkSelector } from '@/components/framework-selector'
|
||||
import { Mdx } from '@/components/mdx-components'
|
||||
import { SourcePanel } from '@/components/source-panel'
|
||||
import { DashboardTableOfContents } from '@/components/toc'
|
||||
@@ -87,13 +88,16 @@ export default async function DocPage({ params }: DocPageProps) {
|
||||
<ChevronRight className="h-4 w-4 text-foreground-muted" />
|
||||
<div className="text-foreground-lighter">{doc.title}</div>
|
||||
</div>
|
||||
<div className="space-y-2 mb-5">
|
||||
<h1 className={cn('scroll-m-20 text-4xl tracking-tight')}>{doc.title}</h1>
|
||||
{doc.description && (
|
||||
<p className="text-lg text-foreground-light">
|
||||
<Balancer>{doc.description}</Balancer>
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-end justify-between mb-5">
|
||||
<div className="space-y-2 mb-5">
|
||||
<h1 className={cn('scroll-m-20 text-4xl tracking-tight')}>{doc.title}</h1>
|
||||
{doc.description && (
|
||||
<p className="text-lg text-foreground-light">
|
||||
<Balancer>{doc.description}</Balancer>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<FrameworkSelector />
|
||||
</div>
|
||||
<Separator className="mb-6" />
|
||||
<SourcePanel doc={doc} />
|
||||
|
||||
@@ -5,12 +5,15 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { ThemeProviderProps } from 'next-themes/dist/types'
|
||||
|
||||
import { TooltipProvider } from 'ui'
|
||||
import { FrameworkProvider } from '@/context/framework-context'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
<JotaiProvider>
|
||||
<NextThemesProvider {...props}>
|
||||
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
||||
<FrameworkProvider>
|
||||
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
||||
</FrameworkProvider>
|
||||
</NextThemesProvider>
|
||||
</JotaiProvider>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
|
||||
import { componentPages, frameworkTitles } from '@/config/docs'
|
||||
import { useFramework } from '@/context/framework-context'
|
||||
import { SelectValue } from '@ui/components/shadcn/ui/select'
|
||||
import {
|
||||
Select_Shadcn_,
|
||||
SelectContent_Shadcn_,
|
||||
SelectGroup_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
const frameworks = Object.keys(frameworkTitles)
|
||||
|
||||
export function FrameworkSelector() {
|
||||
const pathname = usePathname()
|
||||
|
||||
// Extract framework and docTitle from pathname
|
||||
// Example: /ui/docs/nextjs/password-based-auth
|
||||
const pathParts = pathname.split('/')
|
||||
const docTitle = pathParts[pathParts.length - 1]
|
||||
const framework = pathParts[pathParts.length - 2]
|
||||
|
||||
const router = useRouter()
|
||||
const { setFramework: setPreferredFramework } = useFramework()
|
||||
const selectedFramework = frameworks.includes(framework) ? framework : 'nextjs'
|
||||
|
||||
if (!framework) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the supported frameworks for this component
|
||||
const supportedFrameworks = componentPages[docTitle]?.supportedFrameworks || []
|
||||
|
||||
// Don't show selector if component only supports one framework
|
||||
if (supportedFrameworks.length <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
// Get current path parts
|
||||
const currentPathParts = [...pathParts]
|
||||
// Replace the framework part (second to last) with the new framework
|
||||
currentPathParts[currentPathParts.length - 2] = value
|
||||
// Build the new path
|
||||
const newPath = currentPathParts.join('/')
|
||||
|
||||
router.push(newPath)
|
||||
|
||||
// Also update the context/localStorage for future use
|
||||
setPreferredFramework(value as keyof typeof frameworkTitles)
|
||||
}
|
||||
|
||||
const options = supportedFrameworks.map((f) => ({
|
||||
label: frameworkTitles[f],
|
||||
value: f,
|
||||
}))
|
||||
|
||||
return (
|
||||
<Select_Shadcn_ value={selectedFramework} onValueChange={onSelect}>
|
||||
<SelectTrigger_Shadcn_ className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
<SelectGroup_Shadcn_>
|
||||
{options.map((f) => (
|
||||
<SelectItem_Shadcn_ key={f.value} value={f.value}>
|
||||
{f.label}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectGroup_Shadcn_>
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
)
|
||||
}
|
||||
@@ -8,12 +8,27 @@ import React from 'react'
|
||||
|
||||
const NavigationItem: React.FC<{ item: SidebarNavItem }> = React.memo(({ item }) => {
|
||||
const pathname = usePathname()
|
||||
const pathParts = pathname.split('/')
|
||||
const slug = pathParts[pathParts.length - 1]
|
||||
const framework = pathParts[pathParts.length - 2]
|
||||
|
||||
const isActive = pathname === item.href
|
||||
// Build URL with framework param if provided
|
||||
let href = item.href
|
||||
|
||||
// Handle component items with slug and framework
|
||||
if (!href && slug) {
|
||||
if (framework) {
|
||||
href = `/docs/${framework}/${slug}`
|
||||
} else {
|
||||
href = `/docs/${slug}`
|
||||
}
|
||||
}
|
||||
|
||||
const isActive = pathname === href
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`${item.href}`}
|
||||
href={href || '#'}
|
||||
className={cn(
|
||||
'relative',
|
||||
'flex',
|
||||
@@ -30,7 +45,6 @@ const NavigationItem: React.FC<{ item: SidebarNavItem }> = React.memo(({ item })
|
||||
className={cn(
|
||||
'transition',
|
||||
'absolute left-0 w-1 h-full bg-foreground',
|
||||
|
||||
isActive ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
></div>
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import NavigationItem from '@/components/side-navigation-item'
|
||||
import { aiEditorsRules, frameworkPages, gettingStarted } from '@/config/docs'
|
||||
import { SelectValue } from '@ui/components/shadcn/ui/select'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import {
|
||||
Select_Shadcn_,
|
||||
SelectContent_Shadcn_,
|
||||
SelectGroup_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
const frameworks = Object.keys(frameworkPages)
|
||||
import { aiEditorsRules, componentPages, gettingStarted } from '@/config/docs'
|
||||
import { useFramework } from '@/context/framework-context'
|
||||
|
||||
function SideNavigation() {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { framework: preferredFramework } = useFramework()
|
||||
|
||||
const pathRegex = /\/docs\/([^/]+)/
|
||||
const match = pathname?.match(pathRegex)
|
||||
const selectedFramework = frameworks.includes(match?.[1] ?? '') ? match?.[1]! : 'nextjs'
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
const firstUrl = frameworkPages[value].items[0].href
|
||||
router.push(firstUrl as string)
|
||||
// Create a function to build URLs
|
||||
const buildUrl = (slug: string, framework?: string) => {
|
||||
if (!framework) return `/docs/${slug}`
|
||||
return `/docs/${framework}/${slug}`
|
||||
}
|
||||
|
||||
const options = frameworks.map((f) => ({
|
||||
label: frameworkPages[f].title,
|
||||
value: f,
|
||||
}))
|
||||
// Get all component pages
|
||||
const componentItems = Object.entries(componentPages).map(([slug, component]) => {
|
||||
let frameworkToUse = undefined
|
||||
|
||||
const selectedFrameworkConfig = frameworkPages[selectedFramework]
|
||||
// Add framework param only if needed
|
||||
if (preferredFramework && component.supportedFrameworks.includes(preferredFramework)) {
|
||||
frameworkToUse = preferredFramework
|
||||
} else if (component.supportedFrameworks.length > 0) {
|
||||
frameworkToUse = component.supportedFrameworks[0]
|
||||
}
|
||||
|
||||
return {
|
||||
title: component.title,
|
||||
href: buildUrl(slug, frameworkToUse),
|
||||
items: [],
|
||||
commandItemLabel: component.commandItemLabel,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<nav className="min-w-[220px]">
|
||||
@@ -46,27 +44,12 @@ function SideNavigation() {
|
||||
</div>
|
||||
<div className="pb-10 space-y-2">
|
||||
<div className="font-mono uppercase text-xs text-foreground-lighter/75 mb-2 px-6 tracking-widest">
|
||||
Select a framework
|
||||
</div>
|
||||
<div className="px-6">
|
||||
<Select_Shadcn_ value={selectedFramework} onValueChange={onSelect}>
|
||||
<SelectTrigger_Shadcn_ className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
<SelectGroup_Shadcn_>
|
||||
{options.map((f) => (
|
||||
<SelectItem_Shadcn_ key={f.value} value={f.value}>
|
||||
{f.label}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectGroup_Shadcn_>
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
Blocks
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{selectedFrameworkConfig.items.map((item, i) => (
|
||||
<NavigationItem item={item} key={`${item.href}-${i}`} />
|
||||
{/* Render items based on component definitions */}
|
||||
{componentItems.map((item, i) => (
|
||||
<NavigationItem item={item} key={`${item.href?.toString() || item.title}-${i}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+42
-113
@@ -41,116 +41,47 @@ export const aiEditorsRules: SidebarNavGroup = {
|
||||
],
|
||||
}
|
||||
|
||||
export const frameworkPages: Record<string, SidebarNavGroup> = {
|
||||
nextjs: {
|
||||
title: 'Next.js',
|
||||
items: [
|
||||
{
|
||||
title: 'Client',
|
||||
href: '/docs/nextjs/client',
|
||||
items: [],
|
||||
commandItemLabel: 'Supabase Client for Next.js',
|
||||
},
|
||||
{
|
||||
title: 'Password-Based Auth',
|
||||
href: '/docs/nextjs/password-based-auth',
|
||||
items: [],
|
||||
commandItemLabel: 'Password-Based Auth for Next.js',
|
||||
},
|
||||
{
|
||||
title: 'Dropzone',
|
||||
href: '/docs/nextjs/dropzone',
|
||||
items: [],
|
||||
commandItemLabel: 'Dropzone for Next.js',
|
||||
},
|
||||
{
|
||||
title: 'Realtime Cursor',
|
||||
href: '/docs/nextjs/realtime-cursor',
|
||||
items: [],
|
||||
commandItemLabel: 'Realtime Cursor for Next.js',
|
||||
},
|
||||
],
|
||||
// Framework titles for display
|
||||
export const frameworkTitles: Record<string, string> = {
|
||||
nextjs: 'Next.js',
|
||||
'react-router': 'React Router',
|
||||
tanstack: 'Tanstack Start',
|
||||
react: 'React SPA',
|
||||
}
|
||||
|
||||
// Component definitions with supported frameworks
|
||||
export const componentPages: Record<
|
||||
string,
|
||||
{
|
||||
title: string
|
||||
supportedFrameworks: string[]
|
||||
commandItemLabel: string
|
||||
href: string
|
||||
}
|
||||
> = {
|
||||
client: {
|
||||
title: 'Client',
|
||||
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
|
||||
commandItemLabel: 'Supabase Client',
|
||||
href: '/docs/client',
|
||||
},
|
||||
'react-router': {
|
||||
title: 'React Router',
|
||||
items: [
|
||||
{
|
||||
title: 'Client',
|
||||
href: '/docs/react-router/client',
|
||||
items: [],
|
||||
commandItemLabel: 'Supabase Client for React Router',
|
||||
},
|
||||
{
|
||||
title: 'Dropzone',
|
||||
href: '/docs/react-router/dropzone',
|
||||
items: [],
|
||||
commandItemLabel: 'Dropzone for React Router',
|
||||
},
|
||||
{
|
||||
title: 'Realtime Cursor',
|
||||
href: '/docs/react-router/realtime-cursor',
|
||||
items: [],
|
||||
commandItemLabel: 'Realtime Cursor for React Router',
|
||||
},
|
||||
],
|
||||
'password-based-auth': {
|
||||
title: 'Password-Based Auth',
|
||||
supportedFrameworks: ['nextjs', 'tanstack', 'react'],
|
||||
commandItemLabel: 'Password-Based Auth',
|
||||
href: '/docs/password-based-auth',
|
||||
},
|
||||
tanstack: {
|
||||
title: 'Tanstack Start',
|
||||
items: [
|
||||
{
|
||||
title: 'Client',
|
||||
href: '/docs/tanstack/client',
|
||||
items: [],
|
||||
commandItemLabel: 'Supabase Client for Tanstack Start',
|
||||
},
|
||||
{
|
||||
title: 'Password-Based Auth',
|
||||
href: '/docs/tanstack/password-based-auth',
|
||||
items: [],
|
||||
commandItemLabel: 'Password-Based Auth for Tanstack Start',
|
||||
},
|
||||
{
|
||||
title: 'Dropzone',
|
||||
href: '/docs/tanstack/dropzone',
|
||||
items: [],
|
||||
commandItemLabel: 'Dropzone for Tanstack Start',
|
||||
},
|
||||
{
|
||||
title: 'Realtime Cursor',
|
||||
href: '/docs/tanstack/realtime-cursor',
|
||||
items: [],
|
||||
commandItemLabel: 'Realtime Cursor for Tanstack Start',
|
||||
},
|
||||
],
|
||||
dropzone: {
|
||||
title: 'Dropzone',
|
||||
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
|
||||
commandItemLabel: 'Dropzone (File Upload)',
|
||||
href: '/docs/dropzone',
|
||||
},
|
||||
react: {
|
||||
title: 'React SPA',
|
||||
items: [
|
||||
{
|
||||
title: 'Client',
|
||||
href: '/docs/react/client',
|
||||
items: [],
|
||||
commandItemLabel: 'Supabase Client for React SPA',
|
||||
},
|
||||
{
|
||||
title: 'Password-Based Auth',
|
||||
href: '/docs/react/password-based-auth',
|
||||
items: [],
|
||||
commandItemLabel: 'Password-Based Auth for React',
|
||||
},
|
||||
{
|
||||
title: 'Dropzone',
|
||||
href: '/docs/react/dropzone',
|
||||
items: [],
|
||||
commandItemLabel: 'Dropzone for React',
|
||||
},
|
||||
{
|
||||
title: 'Realtime Cursor',
|
||||
href: '/docs/react/realtime-cursor',
|
||||
items: [],
|
||||
commandItemLabel: 'Realtime Cursor for React',
|
||||
},
|
||||
],
|
||||
'realtime-cursor': {
|
||||
title: 'Realtime Cursor',
|
||||
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
|
||||
commandItemLabel: 'Realtime Cursor',
|
||||
href: '/docs/realtime-cursor',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -163,10 +94,8 @@ export const COMMAND_ITEMS = [
|
||||
label: item.commandItemLabel,
|
||||
href: item.href,
|
||||
})),
|
||||
...Object.values(frameworkPages).flatMap((group) =>
|
||||
group.items.map((item) => ({
|
||||
label: item.commandItemLabel,
|
||||
href: item.href,
|
||||
}))
|
||||
),
|
||||
...Object.entries(componentPages).map(([_, component]) => ({
|
||||
label: component.commandItemLabel,
|
||||
href: component.href,
|
||||
})),
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
|
||||
import { frameworkTitles } from '@/config/docs'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
type Framework = keyof typeof frameworkTitles
|
||||
type FrameworkContextType = {
|
||||
framework: Framework
|
||||
setFramework: (framework: Framework) => void
|
||||
}
|
||||
|
||||
const FrameworkContext = createContext<FrameworkContextType | undefined>(undefined)
|
||||
|
||||
export function FrameworkProvider({ children }: { children: React.ReactNode }) {
|
||||
const [framework, setFrameworkState] = useState<Framework>('nextjs')
|
||||
|
||||
// Initialize from localStorage on mount (client-side only)
|
||||
useEffect(() => {
|
||||
const storedFramework = localStorage.getItem('preferredFramework')
|
||||
if (storedFramework && Object.keys(frameworkTitles).includes(storedFramework)) {
|
||||
setFrameworkState(storedFramework as Framework)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Update localStorage when framework changes
|
||||
const setFramework = (newFramework: Framework) => {
|
||||
setFrameworkState(newFramework)
|
||||
localStorage.setItem('preferredFramework', newFramework)
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkContext.Provider value={{ framework, setFramework }}>
|
||||
{children}
|
||||
</FrameworkContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Custom hook to use the framework context
|
||||
export function useFramework() {
|
||||
const context = useContext(FrameworkContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useFramework must be used within a FrameworkProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# Supabase UI Library
|
||||
Last updated: 2025-03-23T14:40:10.160Z
|
||||
Last updated: 2025-03-27T07:03:14.930Z
|
||||
|
||||
## Overview
|
||||
Library of components for your project. The components integrate with Supabase and are shadcn compatible.
|
||||
@@ -10,7 +10,7 @@ Library of components for your project. The components integrate with Supabase a
|
||||
- [FAQ](https://supabase.com/ui/docs/getting-started/faq)
|
||||
- Frequently asked questions
|
||||
- [Introduction](https://supabase.com/ui/docs/getting-started/introduction)
|
||||
- Components
|
||||
- A flexible, open-source UI component library built on shadcn/ui, designed to simplify Supabase-powered projects with pre-built Auth, Storage, and Realtime features.
|
||||
- [Quick Start](https://supabase.com/ui/docs/getting-started/quickstart)
|
||||
- Components
|
||||
- [Next.js](https://supabase.com/ui/docs/nextjs/client)
|
||||
@@ -27,12 +27,12 @@ Library of components for your project. The components integrate with Supabase a
|
||||
- Displays a control for easier uploading of files directly to Supabase Storage
|
||||
- [Realtime Cursor](https://supabase.com/ui/docs/react-router/realtime-cursor)
|
||||
- Real-time cursor sharing for collaborative applications
|
||||
- [React](https://supabase.com/ui/docs/react/client)
|
||||
- Supabase client for React
|
||||
- [React Single Page Applications](https://supabase.com/ui/docs/react/client)
|
||||
- Supabase client for React Single Page Applications
|
||||
- [Dropzone (File Upload)](https://supabase.com/ui/docs/react/dropzone)
|
||||
- Displays a control for easier uploading of files directly to Supabase Storage
|
||||
- [Password-based Auth (React)](https://supabase.com/ui/docs/react/password-based-auth)
|
||||
- Password-based Auth block for React
|
||||
- [Password-based Authentication](https://supabase.com/ui/docs/react/password-based-auth)
|
||||
- Password-based authentication block for React Single Page Applications
|
||||
- [Realtime Cursor](https://supabase.com/ui/docs/react/realtime-cursor)
|
||||
- Real-time cursor sharing for collaborative applications
|
||||
- [Tanstack Start](https://supabase.com/ui/docs/tanstack/client)
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
{
|
||||
"path": "registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx",
|
||||
"content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const router = useRouter()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n router.push('/protected')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>Enter your email below to login to your account</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleLogin}>\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n />\n </div>\n <div className=\"grid gap-2\">\n <div className=\"flex items-center\">\n <Label htmlFor=\"password\">Password</Label>\n <Link\n href=\"/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </Link>\n </div>\n <Input\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Login'}\n </Button>\n </div>\n <div className=\"mt-4 text-center text-sm\">\n Don't have an account?{' '}\n <Link href=\"/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
|
||||
"content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const router = useRouter()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n console.log('login successful')\n // Update this route to redirect to an authenticated route. The user already has an active session.\n router.push('/protected')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>Enter your email below to login to your account</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleLogin}>\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n />\n </div>\n <div className=\"grid gap-2\">\n <div className=\"flex items-center\">\n <Label htmlFor=\"password\">Password</Label>\n <Link\n href=\"/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </Link>\n </div>\n <Input\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Login'}\n </Button>\n </div>\n <div className=\"mt-4 text-center text-sm\">\n Don't have an account?{' '}\n <Link href=\"/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
|
||||
"type": "registry:component"
|
||||
},
|
||||
{
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
|
||||
password,
|
||||
})
|
||||
if (error) throw error
|
||||
console.log('login successful')
|
||||
// Update this route to redirect to an authenticated route. The user already has an active session.
|
||||
router.push('/example/password-based-auth/protected')
|
||||
} catch (error: unknown) {
|
||||
|
||||
Reference in new issue
Block a user