feat: Establish core application structure with user authentication, routing, and Supabase integration.
This commit is contained in:
@@ -1,112 +1,111 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Link } from "@tanstack/react-router"
|
||||
import { Globe, Home, Languages, Menu, Network, X } from "lucide-react"
|
||||
|
||||
import ParaglideLocaleSwitcher from './LocaleSwitcher.tsx'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Globe, Home, Languages, Menu, Network, X } from 'lucide-react'
|
||||
import { useState } from "react"
|
||||
import ParaglideLocaleSwitcher from "./LocaleSwitcher.tsx"
|
||||
|
||||
export default function Header() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg">
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu size={24} />
|
||||
</button>
|
||||
<h1 className="ml-4 text-xl font-semibold">
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/tanstack-word-logo-white.svg"
|
||||
alt="TanStack Logo"
|
||||
className="h-10"
|
||||
/>
|
||||
</Link>
|
||||
</h1>
|
||||
</header>
|
||||
return (
|
||||
<>
|
||||
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg hidden">
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu size={24} />
|
||||
</button>
|
||||
<h1 className="ml-4 text-xl font-semibold">
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/tanstack-word-logo-white.svg"
|
||||
alt="TanStack Logo"
|
||||
className="h-10"
|
||||
/>
|
||||
</Link>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<aside
|
||||
className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${
|
||||
isOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<h2 className="text-xl font-bold">Navigation</h2>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<aside
|
||||
className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${
|
||||
isOpen ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<h2 className="text-xl font-bold">Navigation</h2>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 overflow-y-auto">
|
||||
<Link
|
||||
to="/"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<Home size={20} />
|
||||
<span className="font-medium">Home</span>
|
||||
</Link>
|
||||
<nav className="flex-1 p-4 overflow-y-auto">
|
||||
<Link
|
||||
to="/"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
"flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2",
|
||||
}}
|
||||
>
|
||||
<Home size={20} />
|
||||
<span className="font-medium">Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Demo Links Start */}
|
||||
{/* Demo Links Start */}
|
||||
|
||||
<Link
|
||||
to="/demo/tanstack-query"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<Network size={20} />
|
||||
<span className="font-medium">TanStack Query</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/demo/tanstack-query"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
"flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2",
|
||||
}}
|
||||
>
|
||||
<Network size={20} />
|
||||
<span className="font-medium">TanStack Query</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/demo/sentry/testing"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<Globe size={20} />
|
||||
<span className="font-medium">Sentry</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/demo/sentry/testing"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
"flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2",
|
||||
}}
|
||||
>
|
||||
<Globe size={20} />
|
||||
<span className="font-medium">Sentry</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/demo/i18n"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
||||
}}
|
||||
>
|
||||
<Languages size={20} />
|
||||
<span className="font-medium">I18n example</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/demo/i18n"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
||||
activeProps={{
|
||||
className:
|
||||
"flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2",
|
||||
}}
|
||||
>
|
||||
<Languages size={20} />
|
||||
<span className="font-medium">I18n example</span>
|
||||
</Link>
|
||||
|
||||
{/* Demo Links End */}
|
||||
</nav>
|
||||
{/* Demo Links End */}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-700 bg-gray-800 flex flex-col gap-2">
|
||||
<ParaglideLocaleSwitcher />
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
<div className="p-4 border-t border-gray-700 bg-gray-800 flex flex-col gap-2">
|
||||
<ParaglideLocaleSwitcher />
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
24
src/integrations/supabase/supabase.ts
Normal file
24
src/integrations/supabase/supabase.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createServerClient } from "@supabase/ssr"
|
||||
import { getCookies, setCookie } from "@tanstack/react-start/server"
|
||||
|
||||
export function getSupabaseServerClient() {
|
||||
return createServerClient(
|
||||
process.env.VITE_SUPABASE_URL as string,
|
||||
process.env.VITE_SUPABASE_KEY as string,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return Object.entries(getCookies()).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
}))
|
||||
},
|
||||
setAll(cookies) {
|
||||
cookies.forEach((cookie) => {
|
||||
setCookie(cookie.name, cookie.value)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
49
src/lib/hooks/useLogin.tsx
Normal file
49
src/lib/hooks/useLogin.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
import { toast } from "sonner"
|
||||
import type z from "zod"
|
||||
import { user } from "@/lib/server/user"
|
||||
import { loginFormSchema } from "../validation/user"
|
||||
|
||||
type TLoginForm = z.infer<typeof loginFormSchema>
|
||||
|
||||
export const useLogin = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationKey: ["login"],
|
||||
mutationFn: async (data: TLoginForm) => {
|
||||
const response = await user.login({ data })
|
||||
if (response.error) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
},
|
||||
onMutate: () => {
|
||||
toast.loading("Logging in...", { id: "login" })
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Login successful! Redirecting to posts..", { id: "login" })
|
||||
navigate({
|
||||
to: "/",
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message, { id: "login" })
|
||||
},
|
||||
})
|
||||
|
||||
const validateLogin = (formData: TLoginForm) => {
|
||||
if (!loginFormSchema.safeParse(formData).success) {
|
||||
toast.error("Error en el formulario.")
|
||||
return false
|
||||
}
|
||||
|
||||
loginMutation.mutate(formData)
|
||||
}
|
||||
|
||||
return {
|
||||
login: validateLogin,
|
||||
isPending: loginMutation.isPending,
|
||||
errors: errors,
|
||||
}
|
||||
}
|
||||
142
src/lib/server/user.ts
Normal file
142
src/lib/server/user.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { redirect } from "@tanstack/react-router"
|
||||
import { createServerFn } from "@tanstack/react-start"
|
||||
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
|
||||
import {
|
||||
loginFormSchema,
|
||||
signupFormSchema,
|
||||
userListParamsSchema,
|
||||
} from "../validation/user"
|
||||
|
||||
const login = createServerFn({ method: "POST" })
|
||||
.inputValidator(loginFormSchema)
|
||||
.handler(async ({ data }) => {
|
||||
const supabase = getSupabaseServerClient()
|
||||
|
||||
const login = await supabase.auth.signInWithPassword({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
})
|
||||
|
||||
if (login.error) {
|
||||
return {
|
||||
error: true,
|
||||
message: login.error.message,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: false,
|
||||
}
|
||||
})
|
||||
|
||||
const logout = createServerFn().handler(async () => {
|
||||
const supabase = getSupabaseServerClient()
|
||||
|
||||
const { error } = await supabase.auth.signOut()
|
||||
if (error) {
|
||||
return {
|
||||
error: true,
|
||||
message: error.message,
|
||||
}
|
||||
}
|
||||
|
||||
throw redirect({
|
||||
to: "/",
|
||||
viewTransition: true,
|
||||
replace: true,
|
||||
})
|
||||
})
|
||||
|
||||
const signup = createServerFn({ method: "POST" })
|
||||
.inputValidator(signupFormSchema)
|
||||
.handler(async ({ data }) => {
|
||||
const supabase = getSupabaseServerClient()
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
options: {
|
||||
data: {
|
||||
name: data.name,
|
||||
location: data.location,
|
||||
},
|
||||
},
|
||||
})
|
||||
if (error) {
|
||||
return {
|
||||
error: true,
|
||||
message: error.message,
|
||||
}
|
||||
}
|
||||
|
||||
throw redirect({
|
||||
href: data.redirectUrl || "/",
|
||||
})
|
||||
})
|
||||
|
||||
const userData = createServerFn().handler(async () => {
|
||||
const supabase = getSupabaseServerClient()
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error || !data.user) {
|
||||
return {
|
||||
error: true,
|
||||
message: error?.message ?? "Unknown error",
|
||||
}
|
||||
}
|
||||
return {
|
||||
user: {
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
name: data.user.user_metadata.name || "",
|
||||
location: data.user.user_metadata.location || "",
|
||||
},
|
||||
error: false,
|
||||
}
|
||||
})
|
||||
|
||||
const resendConfirmationEmail = createServerFn({ method: "POST" })
|
||||
.inputValidator(signupFormSchema.pick({ email: true }))
|
||||
.handler(async ({ data }) => {
|
||||
const supabase = getSupabaseServerClient()
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(data.email)
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
error: true,
|
||||
message: error.message,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: false,
|
||||
}
|
||||
})
|
||||
|
||||
const userList = createServerFn()
|
||||
.inputValidator(userListParamsSchema)
|
||||
.handler(async ({ data }) => {
|
||||
const supabase = getSupabaseServerClient()
|
||||
const users = await supabase.auth.admin.listUsers({
|
||||
page: data.page,
|
||||
perPage: data.limit,
|
||||
})
|
||||
|
||||
if (users.error) {
|
||||
return {
|
||||
error: true,
|
||||
message: users.error.message,
|
||||
}
|
||||
}
|
||||
return {
|
||||
users: users.data,
|
||||
error: false,
|
||||
}
|
||||
})
|
||||
|
||||
export const user = {
|
||||
login,
|
||||
logout,
|
||||
signup,
|
||||
userData,
|
||||
resendConfirmationEmail,
|
||||
userList,
|
||||
}
|
||||
25
src/lib/validation/user.ts
Normal file
25
src/lib/validation/user.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as z from "zod"
|
||||
|
||||
export const loginFormSchema = z.object({
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(1, "Password must be at least 1 character long"),
|
||||
})
|
||||
|
||||
export const signupFormSchema = z.object({
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters long"),
|
||||
name: z.string().min(1, "The field is required"),
|
||||
location: z.string().min(1, "The field is required"),
|
||||
redirectUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
export const profileFormSchema = z.object({
|
||||
id: z.uuid(),
|
||||
firstName: z.string().min(1, "First name is required"),
|
||||
lastName: z.string().min(1, "Last name is required"),
|
||||
})
|
||||
|
||||
export const userListParamsSchema = z.object({
|
||||
page: z.number().min(1).default(1),
|
||||
limit: z.number().min(1).max(100).default(10),
|
||||
})
|
||||
@@ -9,11 +9,17 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as LoginRouteRouteImport } from './routes/login/route'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
||||
import { Route as DemoI18nRouteImport } from './routes/demo.i18n'
|
||||
import { Route as DemoSentryTestingRouteImport } from './routes/demo/sentry.testing'
|
||||
|
||||
const LoginRouteRoute = LoginRouteRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@@ -37,12 +43,14 @@ const DemoSentryTestingRoute = DemoSentryTestingRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/login': typeof LoginRouteRoute
|
||||
'/demo/i18n': typeof DemoI18nRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/login': typeof LoginRouteRoute
|
||||
'/demo/i18n': typeof DemoI18nRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||
@@ -50,6 +58,7 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/login': typeof LoginRouteRoute
|
||||
'/demo/i18n': typeof DemoI18nRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||
@@ -58,14 +67,21 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/demo/i18n'
|
||||
| '/demo/tanstack-query'
|
||||
| '/demo/sentry/testing'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/demo/i18n' | '/demo/tanstack-query' | '/demo/sentry/testing'
|
||||
to:
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/demo/i18n'
|
||||
| '/demo/tanstack-query'
|
||||
| '/demo/sentry/testing'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/demo/i18n'
|
||||
| '/demo/tanstack-query'
|
||||
| '/demo/sentry/testing'
|
||||
@@ -73,6 +89,7 @@ export interface FileRouteTypes {
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
LoginRouteRoute: typeof LoginRouteRoute
|
||||
DemoI18nRoute: typeof DemoI18nRoute
|
||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||
DemoSentryTestingRoute: typeof DemoSentryTestingRoute
|
||||
@@ -80,6 +97,13 @@ export interface RootRouteChildren {
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof LoginRouteRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@@ -113,6 +137,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
LoginRouteRoute: LoginRouteRoute,
|
||||
DemoI18nRoute: DemoI18nRoute,
|
||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||
DemoSentryTestingRoute: DemoSentryTestingRoute,
|
||||
|
||||
@@ -53,7 +53,6 @@ function App() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900">
|
||||
<Button> Hola</Button>
|
||||
<section className="relative py-20 px-6 text-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10"></div>
|
||||
<div className="relative max-w-5xl mx-auto">
|
||||
|
||||
142
src/routes/login/route.tsx
Normal file
142
src/routes/login/route.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Button, Card, Input, Label, Tabs } from "@heroui/react"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { LogIn } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
export const Route = createFileRoute("/login")({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const [values, setValues] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
})
|
||||
return (
|
||||
<div className="grid-cols-2 grid min-h-screen">
|
||||
<div className=" p-5 bg-default">
|
||||
<h1 className="text-4xl font-bold text-end ">
|
||||
Find<span className="text-accent">your</span>Pilot
|
||||
</h1>
|
||||
<div className="flex items-center justify-center min-h-[90vh]">
|
||||
<Card className="w-full max-w-md bg-white/90 backdrop-blur-2xl border-3 border-accent-soft">
|
||||
<Card.Header>
|
||||
<Tabs className="w-full max-w-md">
|
||||
<Tabs.ListContainer>
|
||||
<Tabs.List aria-label="Options">
|
||||
<Tabs.Tab id="login" className="text-lg">
|
||||
Acceso
|
||||
<Tabs.Indicator className="bg-accent" />
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab id="register" className="text-lg ">
|
||||
<Tabs.Separator />
|
||||
Registro
|
||||
<Tabs.Indicator className="bg-accent" />
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs.ListContainer>
|
||||
</Tabs>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form className="flex flex-col gap-4">
|
||||
<Label isRequired className="ml-4 text-lg">
|
||||
Correo
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Introduce tu correo"
|
||||
type="email"
|
||||
value={values.email}
|
||||
onChange={(e) =>
|
||||
setValues({ ...values, email: e.target.value })
|
||||
}
|
||||
variant="secondary"
|
||||
className="py-4 text-lg "
|
||||
required
|
||||
/>
|
||||
<Label isRequired className="ml-4 text-lg">
|
||||
Contraseña
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Introduce tu contraseña"
|
||||
type="password"
|
||||
value={values.password}
|
||||
onChange={(e) =>
|
||||
setValues({ ...values, password: e.target.value })
|
||||
}
|
||||
variant="secondary"
|
||||
className="py-4 text-lg "
|
||||
required
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="py-6 px-8 w-full text-white text-lg"
|
||||
size="lg"
|
||||
>
|
||||
<LogIn size={18} />
|
||||
Entrar
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<Card.Footer>
|
||||
<div className="flex justify-evenly w-full gap-4">
|
||||
<Button size="lg" className="w-full" variant="secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<g fill="none" fill-rule="evenodd" clip-rule="evenodd">
|
||||
<path
|
||||
fill="#f44336"
|
||||
d="M7.209 1.061c.725-.081 1.154-.081 1.933 0a6.57 6.57 0 0 1 3.65 1.82a100 100 0 0 0-1.986 1.93q-1.876-1.59-4.188-.734q-1.696.78-2.362 2.528a78 78 0 0 1-2.148-1.658a.26.26 0 0 0-.16-.027q1.683-3.245 5.26-3.86"
|
||||
opacity="0.987"
|
||||
/>
|
||||
<path
|
||||
fill="#ffc107"
|
||||
d="M1.946 4.92q.085-.013.161.027a78 78 0 0 0 2.148 1.658A7.6 7.6 0 0 0 4.04 7.99q.037.678.215 1.331L2 11.116Q.527 8.038 1.946 4.92"
|
||||
opacity="0.997"
|
||||
/>
|
||||
<path
|
||||
fill="#448aff"
|
||||
d="M12.685 13.29a26 26 0 0 0-2.202-1.74q1.15-.812 1.396-2.228H8.122V6.713q3.25-.027 6.497.055q.616 3.345-1.423 6.032a7 7 0 0 1-.51.49"
|
||||
opacity="0.999"
|
||||
/>
|
||||
<path
|
||||
fill="#43a047"
|
||||
d="M4.255 9.322q1.23 3.057 4.51 2.854a3.94 3.94 0 0 0 1.718-.626q1.148.812 2.202 1.74a6.62 6.62 0 0 1-4.027 1.684a6.4 6.4 0 0 1-1.02 0Q3.82 14.524 2 11.116z"
|
||||
opacity="0.993"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
<Button size="lg" className="w-full" variant="secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 256 256"
|
||||
>
|
||||
<path
|
||||
fill="#1877f2"
|
||||
d="M256 128C256 57.308 198.692 0 128 0S0 57.308 0 128c0 63.888 46.808 116.843 108 126.445V165H75.5v-37H108V99.8c0-32.08 19.11-49.8 48.348-49.8C170.352 50 185 52.5 185 52.5V84h-16.14C152.959 84 148 93.867 148 103.99V128h35.5l-5.675 37H148v89.445c61.192-9.602 108-62.556 108-126.445"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="m177.825 165l5.675-37H148v-24.01C148 93.866 152.959 84 168.86 84H185V52.5S170.352 50 156.347 50C127.11 50 108 67.72 108 99.8V128H75.5v37H108v89.445A129 129 0 0 0 128 256a129 129 0 0 0 20-1.555V165z"
|
||||
/>
|
||||
</svg>
|
||||
Facebook
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-accent bg-[url('https://cdn.pixabay.com/photo/2023/03/22/22/37/mavic-2-7870679_1280.jpg')] bg-cover"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
201
src/styles.css
201
src/styles.css
@@ -18,122 +18,91 @@ code {
|
||||
font-family:
|
||||
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
/*
|
||||
* HeroUI Theme Customization
|
||||
* Add this to your global.css after importing @heroui/styles
|
||||
* Only includes base variables from variables.css
|
||||
* @see https://v3.heroui.com/docs/react/getting-started/theming
|
||||
*/
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.871 0.006 286.286);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.871 0.006 286.286);
|
||||
:root,
|
||||
.light,
|
||||
.default,
|
||||
[data-theme="light"],
|
||||
[data-theme="default"] {
|
||||
/* Theme Colors (Light Mode) */
|
||||
--accent: oklch(66.78% 0.2232 43.72);
|
||||
--accent-foreground: oklch(15% 0.0300 43.72);
|
||||
--background: oklch(97.02% 0.0064 43.72);
|
||||
--border: oklch(90.00% 0.0064 43.72);
|
||||
--danger: oklch(65.32% 0.2358 0.53);
|
||||
--danger-foreground: oklch(99.11% 0 0);
|
||||
--default: oklch(94.00% 0.0064 43.72);
|
||||
--default-foreground: oklch(21.03% 0.0059 43.72);
|
||||
--field-background: oklch(100.00% 0.0032 43.72);
|
||||
--field-foreground: oklch(21.03% 0.0064 43.72);
|
||||
--field-placeholder: oklch(55.17% 0.0128 43.72);
|
||||
--focus: oklch(66.78% 0.2232 43.72);
|
||||
--foreground: oklch(21.03% 0.0064 43.72);
|
||||
--muted: oklch(55.17% 0.0128 43.72);
|
||||
--overlay: oklch(100.00% 0.0019 43.72);
|
||||
--overlay-foreground: oklch(21.03% 0.0064 43.72);
|
||||
--scrollbar: oklch(87.10% 0.0064 43.72);
|
||||
--segment: oklch(100.00% 0.0064 43.72);
|
||||
--segment-foreground: oklch(21.03% 0.0064 43.72);
|
||||
--separator: oklch(92.00% 0.0064 43.72);
|
||||
--success: oklch(73.29% 0.1960 125.60);
|
||||
--success-foreground: oklch(21.03% 0.0059 125.60);
|
||||
--surface: oklch(100.00% 0.0032 43.72);
|
||||
--surface-foreground: oklch(21.03% 0.0064 43.72);
|
||||
--surface-secondary: oklch(95.24% 0.0051 43.72);
|
||||
--surface-secondary-foreground: oklch(21.03% 0.0064 43.72);
|
||||
--surface-tertiary: oklch(93.73% 0.0051 43.72);
|
||||
--surface-tertiary-foreground: oklch(21.03% 0.0064 43.72);
|
||||
--warning: oklch(78.19% 0.1605 47.12);
|
||||
--warning-foreground: oklch(21.03% 0.0059 47.12);
|
||||
|
||||
/* Border Radius */
|
||||
--radius: 0.125rem;
|
||||
--field-radius: 0.125rem;
|
||||
|
||||
/* Font Family */
|
||||
/* Make sure to load Inter font in your app */
|
||||
--font-sans: var(--font-inter);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.141 0.005 285.823);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.141 0.005 285.823);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.274 0.006 286.033);
|
||||
--input: oklch(0.274 0.006 286.033);
|
||||
--ring: oklch(0.442 0.017 285.786);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.274 0.006 286.033);
|
||||
--sidebar-ring: oklch(0.442 0.017 285.786);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
.dark,
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
/* Theme Colors (Dark Mode) */
|
||||
--accent: oklch(66.78% 0.2232 43.72);
|
||||
--accent-foreground: oklch(15% 0.0300 43.72);
|
||||
--background: oklch(12.00% 0.0064 43.72);
|
||||
--border: oklch(28.00% 0.0064 43.72);
|
||||
--danger: oklch(59.40% 0.1992 359.42);
|
||||
--danger-foreground: oklch(99.11% 0 0);
|
||||
--default: oklch(27.40% 0.0064 43.72);
|
||||
--default-foreground: oklch(99.11% 0 0);
|
||||
--field-background: oklch(21.03% 0.0128 43.72);
|
||||
--field-foreground: oklch(99.11% 0.0064 43.72);
|
||||
--field-placeholder: oklch(70.50% 0.0128 43.72);
|
||||
--focus: oklch(66.78% 0.2232 43.72);
|
||||
--foreground: oklch(99.11% 0.0064 43.72);
|
||||
--muted: oklch(70.50% 0.0128 43.72);
|
||||
--overlay: oklch(21.03% 0.0128 43.72);
|
||||
--overlay-foreground: oklch(99.11% 0.0064 43.72);
|
||||
--scrollbar: oklch(70.50% 0.0064 43.72);
|
||||
--segment: oklch(39.64% 0.0064 43.72);
|
||||
--segment-foreground: oklch(99.11% 0.0064 43.72);
|
||||
--separator: oklch(25.00% 0.0064 43.72);
|
||||
--success: oklch(73.29% 0.1960 125.60);
|
||||
--success-foreground: oklch(21.03% 0.0059 125.60);
|
||||
--surface: oklch(21.03% 0.0128 43.72);
|
||||
--surface-foreground: oklch(99.11% 0.0064 43.72);
|
||||
--surface-secondary: oklch(25.70% 0.0096 43.72);
|
||||
--surface-secondary-foreground: oklch(99.11% 0.0064 43.72);
|
||||
--surface-tertiary: oklch(27.21% 0.0096 43.72);
|
||||
--surface-tertiary-foreground: oklch(99.11% 0.0064 43.72);
|
||||
--warning: oklch(82.03% 0.1406 51.13);
|
||||
--warning-foreground: oklch(21.03% 0.0059 51.13);
|
||||
}
|
||||
Reference in New Issue
Block a user