places - feat: implement user place management with CRUD operations and validation
This commit is contained in:
parent
66c60829ab
commit
653a9b2dc5
@ -17,7 +17,6 @@ export const useLogin = () => {
|
|||||||
mutationKey: ["login"],
|
mutationKey: ["login"],
|
||||||
mutationFn: async (data: TLoginForm) => {
|
mutationFn: async (data: TLoginForm) => {
|
||||||
const response = await user.login({ data })
|
const response = await user.login({ data })
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw new Error(response.message)
|
throw new Error(response.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,48 @@
|
|||||||
import { createServerFn } from "@tanstack/react-start"
|
import { createServerFn } from "@tanstack/react-start"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { db } from "@/integrations/drizzle"
|
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({
|
export const insertUserPlace = createServerFn({
|
||||||
method: "POST"
|
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 () => {
|
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
|
return await db
|
||||||
.select()
|
.select()
|
||||||
.from(places)
|
.from(placesSchema)
|
||||||
.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
|
.where(eq(placesSchema.id_user, data.id_user))
|
||||||
})
|
.limit(data.limit)
|
||||||
|
.offset((data.page - 1) * data.limit)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const places = {
|
||||||
|
insertUserPlace,
|
||||||
|
editUserPlace,
|
||||||
|
getUserPlacesById
|
||||||
|
}
|
||||||
|
|||||||
@ -82,7 +82,6 @@ const userData = createServerFn().handler(async () => {
|
|||||||
message: error?.message ?? "Unknown error"
|
message: error?.message ?? "Unknown error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(data)
|
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
|
|||||||
18
src/lib/validation/places.ts
Normal file
18
src/lib/validation/places.ts
Normal 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(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -14,11 +14,6 @@ export const Route = createFileRoute("/_authed")({
|
|||||||
// TODO: Redirect to login page
|
// TODO: Redirect to login page
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loader: ({ context }) => {
|
|
||||||
context.queryClient.ensureQueryData({
|
|
||||||
queryKey: ["profile"]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
errorComponent: ({ error }) => {
|
errorComponent: ({ error }) => {
|
||||||
if (error.message === "Not authenticated") {
|
if (error.message === "Not authenticated") {
|
||||||
return (
|
return (
|
||||||
@ -38,8 +33,6 @@ function RouteComponent() {
|
|||||||
<div>
|
<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">
|
<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 className="w-full justify-between flex items-center">
|
||||||
<div />
|
|
||||||
<h1 className="font-black text-2xl md:text-4xl">FindYourPilots</h1>
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Avatar isBordered src="/profile.png" />
|
<Avatar isBordered src="/profile.png" />
|
||||||
|
|||||||
@ -1,11 +1,3 @@
|
|||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
Chip
|
|
||||||
} from "@heroui/react"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/dashboard")({
|
export const Route = createFileRoute("/_authed/dashboard")({
|
||||||
@ -14,106 +6,6 @@ export const Route = createFileRoute("/_authed/dashboard")({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { user } = Route.useRouteContext()
|
const { user } = Route.useRouteContext()
|
||||||
|
console.log(user)
|
||||||
return (
|
return <div>Dashboard</div>
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,30 @@
|
|||||||
import { Button } from "@heroui/react"
|
import { Button } from "@heroui/react"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
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")({
|
export const Route = createFileRoute("/places")({
|
||||||
component: RouteComponent
|
component: RouteComponent
|
||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
|
|
||||||
const submitUser = async () => {
|
const submitUser = async () => {
|
||||||
await insertUserPlace({
|
const userId = await user.userData()
|
||||||
|
await places.insertUserPlace({
|
||||||
data: {
|
data: {
|
||||||
name: "Place 1",
|
name: "Place 1",
|
||||||
description: "A nice place",
|
description: "A nice place",
|
||||||
coord_x: "40.7128",
|
coord_x: "40.7128",
|
||||||
coord_y: "-74.0060",
|
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
|
hidden_place: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPlaces = async () => {
|
const fetchPlaces = async () => {
|
||||||
const places = await getUserPlaces();
|
const allPlacesByUser = await places.getUserPlacesById()
|
||||||
console.log(places);
|
console.log(allPlacesByUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user