places - feat: implement user place management with CRUD operations and validation

This commit is contained in:
juan 2025-11-20 19:26:04 +01:00
parent 66c60829ab
commit 653a9b2dc5
7 changed files with 68 additions and 135 deletions

View File

@ -17,7 +17,6 @@ export const useLogin = () => {
mutationKey: ["login"],
mutationFn: async (data: TLoginForm) => {
const response = await user.login({ data })
if (response.error) {
throw new Error(response.message)
}

View File

@ -1,17 +1,48 @@
import { createServerFn } from "@tanstack/react-start"
import { eq } from "drizzle-orm"
import { db } from "@/integrations/drizzle"
import { places } from "@/integrations/drizzle/db/schema"
import { places as placesSchema } from "@/integrations/drizzle/db/schema"
import { paginatedPlacesSchema, placeSchema } from "../validation/places"
export const insertUserPlace = createServerFn({
method: "POST"
}).handler(async ({ data }) => {
await db.insert(places).values(data).returning()
})
.inputValidator(placeSchema)
.handler(async ({ data }) => {
await db.insert(placesSchema).values(data).returning()
})
export const getUserPlaces = createServerFn().handler(async () => {
return await db
.select()
.from(places)
.where(eq(places.id_user, "e6472b9d-01a9-4e2e-8bdc-0ddaa9baf5d8")) // No haría falta el where puedo filtrar mediante RSL que solo pueda ver sus places solo los datos que el ha insertado
export const editUserPlace = createServerFn({
method: "POST"
})
.inputValidator(
placeSchema.pick({
hidden_place: true,
name: true,
description: true,
id_user: true
})
)
.handler(async ({ data }) => {
await db
.update(placesSchema)
.set(data)
.where(eq(placesSchema.id_user, data.id_user))
})
export const getUserPlacesById = createServerFn()
.inputValidator(paginatedPlacesSchema)
.handler(async ({ data }) => {
return await db
.select()
.from(placesSchema)
.where(eq(placesSchema.id_user, data.id_user))
.limit(data.limit)
.offset((data.page - 1) * data.limit)
})
export const places = {
insertUserPlace,
editUserPlace,
getUserPlacesById
}

View File

@ -82,7 +82,6 @@ const userData = createServerFn().handler(async () => {
message: error?.message ?? "Unknown error"
}
}
console.log(data)
return {
user: {
id: data.user.id,

View File

@ -0,0 +1,18 @@
import z from "zod"
export const placeSchema = z.object({
name: z.string(),
description: z.string(),
coord_x: z.string(),
coord_y: z.string(),
id_user: z.string(),
hidden_place: z.boolean()
})
export const paginatedPlacesSchema = z.object({
page: z.number(),
limit: z.number(),
id_user: z.string(),
})

View File

@ -14,11 +14,6 @@ export const Route = createFileRoute("/_authed")({
// TODO: Redirect to login page
}
},
loader: ({ context }) => {
context.queryClient.ensureQueryData({
queryKey: ["profile"]
})
},
errorComponent: ({ error }) => {
if (error.message === "Not authenticated") {
return (
@ -38,8 +33,6 @@ function RouteComponent() {
<div>
<nav className="flex gap-2 p-2 mb-2 rounded-md bg-clip-padding backdrop-filter backdrop-blur-xl bg-opacity-10 sticky top-0 z-20">
<div className="w-full justify-between flex items-center">
<div />
<h1 className="font-black text-2xl md:text-4xl">FindYourPilots</h1>
<Dropdown>
<DropdownTrigger>
<Avatar isBordered src="/profile.png" />

View File

@ -1,11 +1,3 @@
import {
Avatar,
Card,
CardBody,
CardFooter,
CardHeader,
Chip
} from "@heroui/react"
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/_authed/dashboard")({
@ -14,106 +6,6 @@ export const Route = createFileRoute("/_authed/dashboard")({
function RouteComponent() {
const { user } = Route.useRouteContext()
return (
<div className="flex justify-center items-center flex-col gap-2">
<Card className="max-w-6xl border-none" fullWidth shadow="none">
<CardHeader className="mb-0 pb-0 justify-between flex-wrap gap-2">
<Chip variant="light" size="lg">
Inicio {user?.name ?? "demo"}
</Chip>
<div className="rounded-md bg-warning/30 border-2 border-warning/70 px-3 py-1">
<p> 🏗 La web está en mantenimiento</p>
</div>
</CardHeader>
<CardBody>
{/* === GRID PRINCIPAL === */}
<div className="grid sm:grid-cols-3 grid-rows-[auto_1fr] gap-5 mt-2">
{/* === Tarjeta 1: Mis drones === */}
<Card
shadow="none"
className="rounded-md max-w-sm bg-default-50"
isPressable
>
<CardHeader className="flex justify-between items-center gap-2">
<div className="flex flex-col gap-1 justify-start items-start">
<h2 className="text-start font-semibold text-lg">
Mis drones
</h2>
<p className="text-default-500 text-sm text-start">
Actualiza tus drones en uso o pilotados.
</p>
</div>
<Avatar
src="/drone.webp"
alt="Drone"
className="rounded-full"
/>
</CardHeader>
<CardFooter>
<p className="text-default-500 text-sm text-start">Ver más</p>
</CardFooter>
</Card>
{/* === Tarjeta 2: Datos personales === */}
<Card
shadow="none"
className="rounded-md max-w-sm bg-default-50"
isPressable
>
<CardHeader className="flex justify-between items-center gap-2">
<div className="flex flex-col gap-1 justify-start items-start">
<h2 className="text-start font-semibold text-lg">
Datos personales
</h2>
<p className="text-default-500 text-sm text-start">
Actualiza tu información, ubicación y detalles.
</p>
</div>
<Avatar
src="/profile.png"
alt="User"
className="rounded-full"
/>
</CardHeader>
<CardFooter>
<p className="text-default-500 text-sm">Ver más</p>
</CardFooter>
</Card>
{/* === Tarjeta 3: Ofertas de vuelo === */}
<Card
shadow="none"
className="rounded-md max-w-sm bg-default-50 row-span-2 self-end h-full"
isPressable
>
<CardHeader className="flex justify-between items-center gap-2">
<div className="flex flex-col gap-1 justify-start items-start">
<h2 className="text-start font-semibold text-lg">
Certificados
</h2>
<p className="text-default-500 text-sm text-start"></p>
</div>
<Avatar
src="/profile.png"
alt="User"
className="rounded-full"
/>
</CardHeader>
<CardBody>
<p className="text-default-500 text-sm">Próximamente...</p>
</CardBody>
<CardFooter>
<p className="text-default-500 text-sm">Ver más</p>
</CardFooter>
</Card>
{/* === Mapa (ocupa 2 columnas) === */}
<div className="w-full h-[400px] bg-default-100 col-span-2 rounded-md flex justify-center items-center">
{/* Aquí irá el mapa */}
</div>
</div>
</CardBody>
</Card>
</div>
)
console.log(user)
return <div>Dashboard</div>
}

View File

@ -1,30 +1,31 @@
import { Button } from "@heroui/react"
import { createFileRoute } from "@tanstack/react-router"
import { getUserPlaces, insertUserPlace } from "@/lib/server/places"
import { places } from "@/lib/server/places"
import { user } from "@/lib/server/user"
export const Route = createFileRoute("/places")({
component: RouteComponent
})
function RouteComponent() {
const submitUser = async () => {
await insertUserPlace({
const userId = await user.userData()
await places.insertUserPlace({
data: {
name: "Place 1",
description: "A nice place",
coord_x: "40.7128",
coord_y: "-74.0060",
id_user: "e6472b9d-01a9-4e2e-8bdc-0ddaa9baf5d8", // Replace with actual user ID
id_user: userId.user?.id as string, // Replace with actual user ID
hidden_place: false
}
})
}
const fetchPlaces = async () => {
const places = await getUserPlaces();
console.log(places);
}
const fetchPlaces = async () => {
const allPlacesByUser = await places.getUserPlacesById()
console.log(allPlacesByUser)
}
return (
<div>