login #1

Closed
jose wants to merge 3 commits from login into main
20 changed files with 4494 additions and 5733 deletions
Showing only changes of commit 0b50638583 - Show all commits

25
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "pnpm: dev",
"command": "pnpm dev",
"problemMatcher": [],
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
},
{
"type": "shell",
"label": "pnpm: build",
"command": "pnpm build",
"problemMatcher": []
}
]
}

View File

@@ -18,13 +18,13 @@
"@heroui/styles": "^3.0.0-beta.8", "@heroui/styles": "^3.0.0-beta.8",
"@sentry/tanstackstart-react": "^10.42.0", "@sentry/tanstackstart-react": "^10.42.0",
"@supabase/ssr": "^0.9.0", "@supabase/ssr": "^0.9.0",
"@supabase/supabase-js": "^2.98.0", "@supabase/supabase-js": "^2.99.1",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"@tanstack/react-router": "^1.163.3", "@tanstack/react-router": "^1.166.7",
"@tanstack/react-router-ssr-query": "^1.163.3", "@tanstack/react-router-ssr-query": "^1.166.7",
"@tanstack/react-start": "^1.166.1", "@tanstack/react-start": "^1.166.8",
"@tanstack/router-plugin": "^1.164.0", "@tanstack/router-plugin": "^1.166.7",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"nitro": "^3.0.1-alpha.2", "nitro": "^3.0.1-alpha.2",
"react": "^19.2.4", "react": "^19.2.4",
@@ -33,11 +33,11 @@
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.5", "@biomejs/biome": "^2.4.6",
"@inlang/paraglide-js": "^2.13.1", "@inlang/paraglide-js": "^2.13.1",
"@tanstack/devtools-vite": "^0.5.3", "@tanstack/devtools-vite": "^0.5.5",
"@tanstack/react-devtools": "^0.9.9", "@tanstack/react-devtools": "^0.9.13",
"@tanstack/react-router-devtools": "^1.163.3", "@tanstack/react-router-devtools": "^1.166.7",
"@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-query-devtools": "^5.91.3",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",

1368
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,28 @@
import { createServerClient } from "@supabase/ssr" import { createServerClient } from "@supabase/ssr"
import { createServerOnlyFn } from "@tanstack/react-start"
import { getCookies, setCookie } from "@tanstack/react-start/server" import { getCookies, setCookie } from "@tanstack/react-start/server"
const supabase_url = createServerOnlyFn(
() => process.env.VITE_SUPABASE_URL as string
)
const supabase_key = createServerOnlyFn(
() => process.env.VITE_SUPABASE_KEY as string
)
export function getSupabaseServerClient() { export function getSupabaseServerClient() {
return createServerClient( return createServerClient(supabase_url(), supabase_key(), {
process.env.VITE_SUPABASE_URL as string,
process.env.VITE_SUPABASE_KEY as string,
{
cookies: { cookies: {
getAll() { getAll() {
return Object.entries(getCookies()).map(([name, value]) => ({ return Object.entries(getCookies()).map(([name, value]) => ({
name, name,
value, value
})) }))
}, },
setAll(cookies) { setAll(cookies) {
cookies.forEach((cookie) => { cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value) setCookie(cookie.name, cookie.value)
}) })
},
},
} }
) }
})
} }

View File

@@ -1,6 +1,6 @@
import { toast } from "@heroui/react"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router" import { useNavigate } from "@tanstack/react-router"
import { toast } from "sonner"
import type z from "zod" import type z from "zod"
import { user } from "@/lib/server/user" import { user } from "@/lib/server/user"
import { loginFormSchema } from "../validation/user" import { loginFormSchema } from "../validation/user"
@@ -18,32 +18,29 @@ export const useLogin = () => {
throw new Error(response.message) throw new Error(response.message)
} }
}, },
onMutate: () => {
toast.loading("Logging in...", { id: "login" })
},
onSuccess: () => { onSuccess: () => {
toast.success("Login successful! Redirecting to posts..", { id: "login" })
navigate({ navigate({
to: "/", to: "/"
}) })
}, }
onError: (error) => {
toast.error(error.message, { id: "login" })
},
}) })
const validateLogin = (formData: TLoginForm) => { const validateLogin = (formData: TLoginForm) => {
if (!loginFormSchema.safeParse(formData).success) { if (!loginFormSchema.safeParse(formData).success) {
toast.error("Error en el formulario.") toast.danger("Error en el formulario.")
return false return false
} }
const promise = loginMutation.mutateAsync(formData)
loginMutation.mutate(formData) toast.promise(promise, {
loading: "Logging in...",
success: "Login successful! Redirecting to posts..",
error: (error) => error.message
})
} }
return { return {
login: validateLogin, login: validateLogin,
isPending: loginMutation.isPending, isPending: loginMutation.isPending
errors: errors,
} }
} }

View File

@@ -0,0 +1,38 @@
import { toast } from "@heroui/react"
import { useMutation } from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router"
import type z from "zod"
import { user } from "@/lib/server/user"
import { signupFormSchema } from "@/lib/validation/user"
type TSignupForm = z.infer<typeof signupFormSchema>
export const useSignup = () => {
const navigate = useNavigate()
const signup = useMutation({
mutationKey: ["signup"],
mutationFn: async (data: TSignupForm) => user.signup({ data }),
onSuccess: () => {
navigate({
to: "/access/login"
})
}
})
const validateSignup = (formData: TSignupForm) => {
if (!signupFormSchema.safeParse(formData).success) {
toast.danger("Signup failed. Please check your input.")
}
const promise = signup.mutateAsync(formData)
toast.promise(promise, {
loading: "Signing up...",
success: "Signup successful! Redirecting to login...",
error: "Signup failed!"
})
}
return {
signup: validateSignup,
isPending: signup.isPending
}
}

View File

@@ -1,25 +1,25 @@
import * as z from "zod" import * as z from "zod"
export const loginFormSchema = z.object({ export const loginFormSchema = z.object({
email: z.string().email("Invalid email address"), email: z.email("Invalid email address"),
password: z.string().min(1, "Password must be at least 1 character long"), password: z.string().min(1, "Password must be at least 1 character long")
}) })
export const signupFormSchema = z.object({ export const signupFormSchema = z.object({
email: z.string().email("Invalid email address"), email: z.email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters long"), password: z.string().min(6, "Password must be at least 6 characters long"),
name: z.string().min(1, "The field is required"), name: z.string().min(1, "The field is required"),
location: z.string().min(1, "The field is required"), location: z.string().min(1, "The field is required"),
redirectUrl: z.string().optional(), redirectUrl: z.string().optional()
}) })
export const profileFormSchema = z.object({ export const profileFormSchema = z.object({
id: z.uuid(), id: z.uuid(),
firstName: z.string().min(1, "First name is required"), firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"), lastName: z.string().min(1, "Last name is required")
}) })
export const userListParamsSchema = z.object({ export const userListParamsSchema = z.object({
page: z.number().min(1).default(1), page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(10), limit: z.number().min(1).max(100).default(10)
}) })

View File

@@ -9,15 +9,23 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // 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 rootRouteImport } from './routes/__root'
import { Route as LoginRouteRouteImport } from './routes/login/route' import { Route as AccessRouteImport } from './routes/access'
import { Route as AuthedRouteImport } from './routes/_authed'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query' import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
import { Route as DemoI18nRouteImport } from './routes/demo.i18n' import { Route as DemoI18nRouteImport } from './routes/demo.i18n'
import { Route as AccessRegisterRouteImport } from './routes/access.register'
import { Route as AccessLoginRouteImport } from './routes/access.login'
import { Route as AuthedDashboardRouteImport } from './routes/_authed/dashboard'
import { Route as DemoSentryTestingRouteImport } from './routes/demo/sentry.testing' import { Route as DemoSentryTestingRouteImport } from './routes/demo/sentry.testing'
const LoginRouteRoute = LoginRouteRouteImport.update({ const AccessRoute = AccessRouteImport.update({
id: '/login', id: '/access',
path: '/login', path: '/access',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedRoute = AuthedRouteImport.update({
id: '/_authed',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
@@ -35,6 +43,21 @@ const DemoI18nRoute = DemoI18nRouteImport.update({
path: '/demo/i18n', path: '/demo/i18n',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AccessRegisterRoute = AccessRegisterRouteImport.update({
id: '/register',
path: '/register',
getParentRoute: () => AccessRoute,
} as any)
const AccessLoginRoute = AccessLoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => AccessRoute,
} as any)
const AuthedDashboardRoute = AuthedDashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => AuthedRoute,
} as any)
const DemoSentryTestingRoute = DemoSentryTestingRouteImport.update({ const DemoSentryTestingRoute = DemoSentryTestingRouteImport.update({
id: '/demo/sentry/testing', id: '/demo/sentry/testing',
path: '/demo/sentry/testing', path: '/demo/sentry/testing',
@@ -43,14 +66,20 @@ const DemoSentryTestingRoute = DemoSentryTestingRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRouteRoute '/access': typeof AccessRouteWithChildren
'/dashboard': typeof AuthedDashboardRoute
'/access/login': typeof AccessLoginRoute
'/access/register': typeof AccessRegisterRoute
'/demo/i18n': typeof DemoI18nRoute '/demo/i18n': typeof DemoI18nRoute
'/demo/tanstack-query': typeof DemoTanstackQueryRoute '/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/demo/sentry/testing': typeof DemoSentryTestingRoute '/demo/sentry/testing': typeof DemoSentryTestingRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRouteRoute '/access': typeof AccessRouteWithChildren
'/dashboard': typeof AuthedDashboardRoute
'/access/login': typeof AccessLoginRoute
'/access/register': typeof AccessRegisterRoute
'/demo/i18n': typeof DemoI18nRoute '/demo/i18n': typeof DemoI18nRoute
'/demo/tanstack-query': typeof DemoTanstackQueryRoute '/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/demo/sentry/testing': typeof DemoSentryTestingRoute '/demo/sentry/testing': typeof DemoSentryTestingRoute
@@ -58,7 +87,11 @@ export interface FileRoutesByTo {
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRouteRoute '/_authed': typeof AuthedRouteWithChildren
'/access': typeof AccessRouteWithChildren
'/_authed/dashboard': typeof AuthedDashboardRoute
'/access/login': typeof AccessLoginRoute
'/access/register': typeof AccessRegisterRoute
'/demo/i18n': typeof DemoI18nRoute '/demo/i18n': typeof DemoI18nRoute
'/demo/tanstack-query': typeof DemoTanstackQueryRoute '/demo/tanstack-query': typeof DemoTanstackQueryRoute
'/demo/sentry/testing': typeof DemoSentryTestingRoute '/demo/sentry/testing': typeof DemoSentryTestingRoute
@@ -67,21 +100,31 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '/' | '/'
| '/login' | '/access'
| '/dashboard'
| '/access/login'
| '/access/register'
| '/demo/i18n' | '/demo/i18n'
| '/demo/tanstack-query' | '/demo/tanstack-query'
| '/demo/sentry/testing' | '/demo/sentry/testing'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/login' | '/access'
| '/dashboard'
| '/access/login'
| '/access/register'
| '/demo/i18n' | '/demo/i18n'
| '/demo/tanstack-query' | '/demo/tanstack-query'
| '/demo/sentry/testing' | '/demo/sentry/testing'
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/login' | '/_authed'
| '/access'
| '/_authed/dashboard'
| '/access/login'
| '/access/register'
| '/demo/i18n' | '/demo/i18n'
| '/demo/tanstack-query' | '/demo/tanstack-query'
| '/demo/sentry/testing' | '/demo/sentry/testing'
@@ -89,7 +132,8 @@ export interface FileRouteTypes {
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
LoginRouteRoute: typeof LoginRouteRoute AuthedRoute: typeof AuthedRouteWithChildren
AccessRoute: typeof AccessRouteWithChildren
DemoI18nRoute: typeof DemoI18nRoute DemoI18nRoute: typeof DemoI18nRoute
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
DemoSentryTestingRoute: typeof DemoSentryTestingRoute DemoSentryTestingRoute: typeof DemoSentryTestingRoute
@@ -97,11 +141,18 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/login': { '/access': {
id: '/login' id: '/access'
path: '/login' path: '/access'
fullPath: '/login' fullPath: '/access'
preLoaderRoute: typeof LoginRouteRouteImport preLoaderRoute: typeof AccessRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed': {
id: '/_authed'
path: ''
fullPath: '/'
preLoaderRoute: typeof AuthedRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/': { '/': {
@@ -125,6 +176,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DemoI18nRouteImport preLoaderRoute: typeof DemoI18nRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/access/register': {
id: '/access/register'
path: '/register'
fullPath: '/access/register'
preLoaderRoute: typeof AccessRegisterRouteImport
parentRoute: typeof AccessRoute
}
'/access/login': {
id: '/access/login'
path: '/login'
fullPath: '/access/login'
preLoaderRoute: typeof AccessLoginRouteImport
parentRoute: typeof AccessRoute
}
'/_authed/dashboard': {
id: '/_authed/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof AuthedDashboardRouteImport
parentRoute: typeof AuthedRoute
}
'/demo/sentry/testing': { '/demo/sentry/testing': {
id: '/demo/sentry/testing' id: '/demo/sentry/testing'
path: '/demo/sentry/testing' path: '/demo/sentry/testing'
@@ -135,9 +207,34 @@ declare module '@tanstack/react-router' {
} }
} }
interface AuthedRouteChildren {
AuthedDashboardRoute: typeof AuthedDashboardRoute
}
const AuthedRouteChildren: AuthedRouteChildren = {
AuthedDashboardRoute: AuthedDashboardRoute,
}
const AuthedRouteWithChildren =
AuthedRoute._addFileChildren(AuthedRouteChildren)
interface AccessRouteChildren {
AccessLoginRoute: typeof AccessLoginRoute
AccessRegisterRoute: typeof AccessRegisterRoute
}
const AccessRouteChildren: AccessRouteChildren = {
AccessLoginRoute: AccessLoginRoute,
AccessRegisterRoute: AccessRegisterRoute,
}
const AccessRouteWithChildren =
AccessRoute._addFileChildren(AccessRouteChildren)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
LoginRouteRoute: LoginRouteRoute, AuthedRoute: AuthedRouteWithChildren,
AccessRoute: AccessRouteWithChildren,
DemoI18nRoute: DemoI18nRoute, DemoI18nRoute: DemoI18nRoute,
DemoTanstackQueryRoute: DemoTanstackQueryRoute, DemoTanstackQueryRoute: DemoTanstackQueryRoute,
DemoSentryTestingRoute: DemoSentryTestingRoute, DemoSentryTestingRoute: DemoSentryTestingRoute,

View File

@@ -8,7 +8,8 @@ export function getRouter() {
const router = createTanStackRouter({ const router = createTanStackRouter({
routeTree, routeTree,
context: { context: {
queryClient queryClient,
user: null
}, },
scrollRestoration: true, scrollRestoration: true,
defaultPreload: "intent", defaultPreload: "intent",

View File

@@ -6,11 +6,12 @@ import {
} from "@tanstack/react-router" } from "@tanstack/react-router"
import { Devtools } from "@/integrations/devtools" import { Devtools } from "@/integrations/devtools"
import { getLocale } from "@/integrations/paraglide/runtime" import { getLocale } from "@/integrations/paraglide/runtime"
import { user } from "@/lib/server/user"
import appCss from "@/styles/globals.css?url" import appCss from "@/styles/globals.css?url"
import Header from "../components/Header"
interface MyRouterContext { interface MyRouterContext {
queryClient: QueryClient queryClient: QueryClient
user: null
} }
export const Route = createRootRouteWithContext<MyRouterContext>()({ export const Route = createRootRouteWithContext<MyRouterContext>()({
@@ -20,8 +21,8 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
document.documentElement.setAttribute("lang", getLocale()) document.documentElement.setAttribute("lang", getLocale())
} }
return await user.userData()
}, },
head: () => ({ head: () => ({
meta: [ meta: [
{ {
@@ -52,7 +53,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<HeadContent /> <HeadContent />
</head> </head>
<body> <body>
<Header />
{children} {children}
<Devtools /> <Devtools />
<Scripts /> <Scripts />

31
src/routes/_authed.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { createFileRoute, Link, Outlet } from "@tanstack/react-router"
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
if (context.error) {
throw new Error("Not authenticated")
}
},
errorComponent: ({ error }) => {
if (error.message === "Not authenticated") {
return (
<p>
Not authenticated. Please <Link to="/access/login">login</Link>.
</p>
)
}
throw error
},
component: RouteComponent
})
function RouteComponent() {
return (
<div className="min-h-screen flex gap-2">
<div className="p-2"></div>
<div className="sm:pl-25 w-full">
<Outlet />
</div>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authed/dashboard')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/_authed/dashboard"!</div>
}

117
src/routes/access.login.tsx Normal file
View File

@@ -0,0 +1,117 @@
import { Button, Card, Form, Input, Label, Spinner } from "@heroui/react"
import { createFileRoute } from "@tanstack/react-router"
import { LogIn } from "lucide-react"
import { useLogin } from "@/lib/hooks/useLogin"
export const Route = createFileRoute("/access/login")({
component: RouteComponent
})
function RouteComponent() {
const { login, isPending } = useLogin()
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const email = formData.get("email") as string
const password = formData.get("password") as string
login({ email, password })
}
return (
<div>
<Card.Content>
<Form onSubmit={handleFormSubmit} className="flex flex-col gap-4">
<Label isRequired className="ml-4 text-lg">
Correo
</Label>
<Input
placeholder="Introduce tu correo"
type="email"
name="email"
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"
name="password"
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"
isPending={isPending}
>
{isPending ? <Spinner /> : <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" fillRule="evenodd" clipRule="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>
</div>
)
}

View File

@@ -0,0 +1,120 @@
import { Button, Form, Input, Label, Spinner } from "@heroui/react"
import { createFileRoute } from "@tanstack/react-router"
import { LogIn } from "lucide-react"
import { useSignup } from "@/lib/hooks/useSignup"
export const Route = createFileRoute("/access/register")({
component: RouteComponent
})
function RouteComponent() {
const { signup, isPending } = useSignup()
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const email = formData.get("email") as string
const password = formData.get("password") as string
const location = formData.get("location") as string
const name = formData.get("name") as string
signup({
email,
password,
location,
name
})
}
return (
<div>
<Form onSubmit={handleFormSubmit} className="flex flex-col gap-4">
<Label isRequired className="ml-4 text-lg">
Correo
</Label>
<Input
placeholder="Introduce tu correo"
type="email"
name="email"
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"
name="password"
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"
isPending={isPending}
>
{isPending ? <Spinner /> : <LogIn size={18} />}
Entrar
</Button>
</div>
</Form>
<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" fillRule="evenodd" clipRule="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>
</div>
)
}

61
src/routes/access.tsx Normal file
View File

@@ -0,0 +1,61 @@
import { Card, type Key, Tabs } from "@heroui/react"
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"
export const Route = createFileRoute("/access")({
beforeLoad: ({ location }) => {
if (location.pathname === "/access") {
redirect({
to: "/access/login",
replace: true,
throw: true
})
}
},
component: RouteComponent
})
function RouteComponent() {
const navigate = Route.useNavigate()
const onSelectTab = (tabId: Key) => {
navigate({
to: `/access/${tabId}`,
viewTransition: true
})
}
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" onSelectionChange={onSelectTab}>
<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>
<Outlet />
</Card.Content>
</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>
)
}

View File

@@ -1,142 +0,0 @@
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>
)
}