diff --git a/biome.json b/biome.json index 44cdcad..826fa01 100644 --- a/biome.json +++ b/biome.json @@ -6,13 +6,14 @@ "useIgnoreFile": false }, "files": { - "ignoreUnknown": false, + "ignoreUnknown": true, "includes": [ "**/src/**/*", "**/.vscode/**/*", "**/index.html", "**/vite.config.js", - "!**/src/routeTree.gen.ts" + "!**/src/routeTree.gen.ts", + "!**/node_modules/**/*" ] }, "formatter": { diff --git a/src/integrations/heroui/provider.tsx b/src/integrations/heroui/provider.tsx index 3af714c..46c3600 100644 --- a/src/integrations/heroui/provider.tsx +++ b/src/integrations/heroui/provider.tsx @@ -1,5 +1,5 @@ import { HeroUIProvider as HeroProvider } from "@heroui/react" export const HeroUIProvider = ({ children }: { children: React.ReactNode }) => { - return {children} + return {children} } diff --git a/src/lib/db/user.ts b/src/lib/db/user.ts new file mode 100644 index 0000000..be5fb95 --- /dev/null +++ b/src/lib/db/user.ts @@ -0,0 +1,84 @@ +import { redirect } from "@tanstack/react-router" +import { createServerFn } from "@tanstack/react-start" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" +import { loginFormSchema, signupFormSchema } from "../validation/user" + +export const getUser = 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 || "" + }, + error: false + } +}) + +export const loginUser = createServerFn({ + method: "POST" +}) + .validator(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 + } + }) + +export const logoutUser = 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 + }) +}) + +export const signupUser = createServerFn({ method: "POST" }) + .validator(signupFormSchema) + .handler(async ({ data }) => { + const supabase = getSupabaseServerClient() + const { error } = await supabase.auth.signUp({ + email: data.email, + password: data.password + }) + if (error) { + return { + error: true, + message: error.message + } + } + + throw redirect({ + href: data.redirectUrl || "/" + }) + }) diff --git a/src/lib/hooks/useValidation.tsx b/src/lib/hooks/useValidation.tsx index b89253c..f21010d 100644 --- a/src/lib/hooks/useValidation.tsx +++ b/src/lib/hooks/useValidation.tsx @@ -15,7 +15,7 @@ export const useValidation = ({ schema }: { formData: FormDataValidation - schema?: z.ZodSchema + schema?: z.ZodType }) => { const result = schema?.safeParse(formData) ?? defaultSchema?.safeParse(formData) @@ -25,6 +25,7 @@ export const useValidation = ({ } if (!result.success) { + //FIXME: Flatten errors, new in zod v4 setErrors(result.error.flatten().fieldErrors as T) return false } diff --git a/src/lib/hooks/user/useLogin.tsx b/src/lib/hooks/user/useLogin.tsx new file mode 100644 index 0000000..d2d6412 --- /dev/null +++ b/src/lib/hooks/user/useLogin.tsx @@ -0,0 +1,54 @@ +import { useMutation } from "@tanstack/react-query" +import { useNavigate } from "@tanstack/react-router" +import { toast } from "sonner" +import type z from "zod" +import { loginUser } from "@/lib/db/user" +import { loginFormSchema } from "@/lib/validation/user" +import { useValidation } from "../useValidation" + +type TLoginForm = z.infer + +export const useLogin = () => { + const navigate = useNavigate() + const { errors, validate } = useValidation({ + defaultSchema: loginFormSchema + }) + const loginMutation = useMutation({ + mutationKey: ["login"], + mutationFn: async (data: TLoginForm) => + loginUser({ + data + }), + onMutate: () => { + toast.loading("Logging in...", { id: "login" }) + }, + onSuccess: () => { + toast.success("Login successful! Redirecting to posts..", { id: "login" }) + navigate({ + to: "/post" + }) + }, + onError: () => { + toast.error("Failed to log in.", { id: "login" }) + } + }) + + const validateLogin = (formData: TLoginForm) => { + const isValid = validate({ + formData + }) + + if (!isValid) { + toast.error("Error en el formulario.") + return false + } + + loginMutation.mutate(formData) + } + + return { + login: validateLogin, + isPending: loginMutation.isPending, + errors: errors + } +} diff --git a/src/lib/hooks/user/useSignup.tsx b/src/lib/hooks/user/useSignup.tsx new file mode 100644 index 0000000..9b58ae4 --- /dev/null +++ b/src/lib/hooks/user/useSignup.tsx @@ -0,0 +1,44 @@ +import { useMutation } from "@tanstack/react-query" +import { useNavigate } from "@tanstack/react-router" +import { toast } from "sonner" +import type z from "zod" +import { signupUser } from "@/lib/db/user" +import { signupFormSchema } from "@/lib/validation/user" +import { useValidation } from "../useValidation" + +type TSignupForm = z.infer + +export const useSignup = () => { + const navigate = useNavigate() + const { validate, errors } = useValidation({ + defaultSchema: signupFormSchema + }) + const signup = useMutation({ + mutationKey: ["signup"], + mutationFn: async (data: TSignupForm) => signupUser({ data }), + onSuccess: () => { + toast.success("Signup successful! Redirecting to login...", { + id: "signup" + }) + navigate({ + to: "/login" + }) + } + }) + + const validateSignup = (formData: TSignupForm) => { + const isValid = validate({ formData }) + if (!isValid) { + toast.error("Signup failed. Please check your input.", { + id: "signup" + }) + } + signup.mutate(formData) + } + + return { + signup: validateSignup, + errors, + isPending: signup.isPending + } +} diff --git a/src/lib/mutations/mutationLogin.tsx b/src/lib/mutations/mutationLogin.tsx deleted file mode 100644 index fe9fc1b..0000000 --- a/src/lib/mutations/mutationLogin.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useMutation } from "@tanstack/react-query" -import { getRouteApi } from "@tanstack/react-router" -import { createServerFn, useServerFn } from "@tanstack/react-start" -import { toast } from "sonner" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" -import { loginSchema } from "../schemas/login" - -const apiRouter = getRouteApi("/login") - -export const loginFn = createServerFn({ method: "POST" }) - .validator(loginSchema) - .handler(async ({ data }) => { - const supabase = getSupabaseServerClient() - const response = await supabase.auth.signInWithPassword({ - email: data.email, - password: data.password - }) - console.log(response) - if (response.error) { - return { - error: true, - message: response.error.message - } - } - }) -export const mutationLogin = () => { - const navigate = apiRouter.useNavigate() - const loginFunction = useServerFn(loginFn) - const mutation = useMutation({ - mutationKey: ["login"], - mutationFn: async (data: { email: string; password: string }) => { - toast.loading("Logging in...", { id: "login" }) - return loginFunction({ - data: { - email: data.email, - password: data.password - } - }) - }, - onSuccess: (data) => { - if (data?.error) { - toast.error(data.message, { id: "login" }) - return - } - toast.success("Login successful! Redirecting to posts...", { - id: "login" - }) - navigate({ - to: "/post" - }) - }, - onError: (error) => { - toast.error("Login failed. Please try again.", { id: "login" }) - console.log("No se ha podido procesar el login", error) - } - }) - - return mutation -} diff --git a/src/lib/mutations/mutationSignup.tsx b/src/lib/mutations/mutationSignup.tsx deleted file mode 100644 index a4f8596..0000000 --- a/src/lib/mutations/mutationSignup.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useMutation } from "@tanstack/react-query" -import { getRouteApi, redirect } from "@tanstack/react-router" -import { createServerFn, useServerFn } from "@tanstack/react-start" -import { toast } from "sonner" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" -import { signupSchema } from "../schemas/signup" - -const apiRouter = getRouteApi("/signup") -export const signupFn = createServerFn({ method: "POST" }) - .validator(signupSchema) - .handler(async ({ data }) => { - const supabase = getSupabaseServerClient() - const { error } = await supabase.auth.signUp({ - email: data.email, - password: data.password - }) - if (error) { - return { - error: true, - message: error.message - } - } - - throw redirect({ - href: data.redirectUrl || "/" - }) - }) - -export const mutationSignup = () => { - const navigate = apiRouter.useNavigate() - const signup = useServerFn(signupFn) - const mutation = useMutation({ - mutationKey: ["signup"], - mutationFn: async (data: { - email: string - password: string - redirectUrl?: string - }) => { - toast.loading("Signing up...", { id: "signup" }) - return signup({ - data: { - email: data.email, - password: data.password, - redirectUrl: data.redirectUrl - } - }) - }, - onSuccess: () => { - toast.success("Signup successful! Redirecting to login...", { - id: "signup" - }) - navigate({ - to: "/login" - }) - } - }) - - return mutation -} diff --git a/src/lib/schemas/login.ts b/src/lib/schemas/login.ts deleted file mode 100644 index e9ba067..0000000 --- a/src/lib/schemas/login.ts +++ /dev/null @@ -1,6 +0,0 @@ -import z from "zod" - -export const loginSchema = z.object({ - email: z.email("Invalid email address"), - password: z.string().min(6, "Password must be at least 6 characters long") -}) diff --git a/src/lib/schemas/signup.ts b/src/lib/schemas/signup.ts deleted file mode 100644 index 93b081b..0000000 --- a/src/lib/schemas/signup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import z from "zod" - -export const signupSchema = z.object({ - email: z.email("Invalid email address"), - password: z.string().min(6, "Password must be at least 6 characters long"), - redirectUrl: z.string().optional() -}) diff --git a/src/lib/validation/user.ts b/src/lib/validation/user.ts new file mode 100644 index 0000000..a9c8259 --- /dev/null +++ b/src/lib/validation/user.ts @@ -0,0 +1,12 @@ +import z from "zod" + +export const loginFormSchema = z.object({ + email: z.string("Invalid email address"), + password: z.string().min(1, "Password must be at least 1 character long") +}) + +export const signupFormSchema = z.object({ + email: z.string("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters long"), + redirectUrl: z.string().optional() +}) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 18e3354..e424124 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -6,31 +6,18 @@ import { Scripts } from "@tanstack/react-router" import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" -import { createServerFn } from "@tanstack/react-start" import { HeroUIProvider } from "@/integrations/heroui/provider" import { SonnerProvider } from "@/integrations/sonner/provider" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" +import { getUser } from "@/lib/db/user" interface MyRouterContext { queryClient: QueryClient user: null } -const fetchUser = createServerFn({ method: "GET" }).handler(async () => { - const supabase = getSupabaseServerClient() - const { data, error: _error } = await supabase.auth.getUser() - - if (!data.user?.email) { - return null - } - - return { - email: data.user.email - } -}) export const Route = createRootRouteWithContext()({ beforeLoad: async () => { - const user = await fetchUser() + const user = await getUser() return { user } diff --git a/src/routes/_authed.tsx b/src/routes/_authed.tsx index 052bb7e..4580cfd 100644 --- a/src/routes/_authed.tsx +++ b/src/routes/_authed.tsx @@ -1,6 +1,5 @@ import { createFileRoute } from "@tanstack/react-router" - export const Route = createFileRoute("/_authed")({ beforeLoad: ({ context }) => { console.log("contextw", context) @@ -10,9 +9,11 @@ export const Route = createFileRoute("/_authed")({ }, errorComponent: ({ error }) => { if (error.message === "Not authenticated") { - ;

- Not authenticated. Please login. -

+ return ( +

+ Not authenticated. Please login. +

+ ) } throw error diff --git a/src/routes/index.tsx b/src/routes/index.tsx index fc2ac80..94c5e07 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,13 +1,13 @@ import logo from "@assets/logo.svg" import { Button } from "@heroui/react" -import { createFileRoute, getRouteApi } from "@tanstack/react-router" +import { createFileRoute } from "@tanstack/react-router" export const Route = createFileRoute("/")({ component: App }) -const apiRouter = getRouteApi("/") + function App() { - const navigate = apiRouter.useNavigate() + const navigate = Route.useNavigate() return (
diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 33aa73a..a506496 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -1,42 +1,37 @@ import { Button, Form, Input } from "@heroui/react" import { createFileRoute } from "@tanstack/react-router" -import { useValidation } from "@/lib/hooks/useValidation" -import { mutationLogin } from "@/lib/mutations/mutationLogin" -import { loginSchema } from "@/lib/schemas/login" +import type { FormEvent } from "react" +import { useLogin } from "@/lib/hooks/user/useLogin" export const Route = createFileRoute("/login")({ component: LoginComp }) - - function LoginComp() { - const loginMutation = mutationLogin() - const { errors, validate } = useValidation({ - defaultSchema: loginSchema - }) + const { errors, isPending, login } = useLogin() + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + + const formData = new FormData(e.currentTarget) + + login({ + email: formData.get("email") as string, + password: formData.get("password") as string + }) + } + return (

Login

{ - e.preventDefault() - const formData = new FormData(e.currentTarget) - const email = formData.get("email") as string - const password = formData.get("password") as string - if ( - validate({ - formData: { email, password } - }) - ) - loginMutation.mutate({ email, password }) - }} + onSubmit={handleSubmit} > -
diff --git a/src/routes/logout.tsx b/src/routes/logout.tsx index b5bf84e..add9371 100644 --- a/src/routes/logout.tsx +++ b/src/routes/logout.tsx @@ -1,26 +1,7 @@ -import { createFileRoute, redirect } from "@tanstack/react-router" -import { createServerFn } from "@tanstack/react-start" -import { toast } from "sonner" -import { getSupabaseServerClient } from "@/integrations/supabase/supabase" - -const logoutFn = createServerFn().handler(async () => { - const supabase = getSupabaseServerClient() - const { error } = await supabase.auth.signOut() - - if (error) { - toast.error("Logout failed. Please try again.") - return { - error: true, - message: error.message - } - } - - throw redirect({ - href: "/" - }) -}) +import { createFileRoute } from "@tanstack/react-router" +import { logoutUser } from "@/lib/db/user" export const Route = createFileRoute("/logout")({ preload: false, - loader: () => logoutFn() + loader: () => logoutUser() }) diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index 7fa08b9..c9c4223 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -1,43 +1,37 @@ import { Button, Form, Input } from "@heroui/react" import { createFileRoute } from "@tanstack/react-router" -import { useValidation } from "@/lib/hooks/useValidation" -import { mutationSignup } from "@/lib/mutations/mutationSignup" -import { signupSchema } from "@/lib/schemas/signup" +import type { FormEvent } from "react" +import { useSignup } from "@/lib/hooks/user/useSignup" export const Route = createFileRoute("/signup")({ component: SignupComp }) function SignupComp() { - const signupMutation = mutationSignup() - const { errors, validate } = useValidation({ - defaultSchema: signupSchema - }) + const { signup, errors, isPending } = useSignup() + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + + const formData = new FormData(e.currentTarget) + + signup({ + email: formData.get("email") as string, + password: formData.get("password") as string + }) + } + return (

Signup

{ - e.preventDefault() - const formData = new FormData(e.currentTarget) - const email = formData.get("email") as string - const password = formData.get("password") as string - if ( - validate({ - formData: { email, password } - }) - ) - signupMutation.mutate({ - email, - password - }) - }} + onSubmit={handleSubmit} > -