diff --git a/src/lib/server/instruments.ts b/src/lib/server/instruments.ts new file mode 100644 index 0000000..53ce25a --- /dev/null +++ b/src/lib/server/instruments.ts @@ -0,0 +1,29 @@ +import { createServerFn } from "@tanstack/react-start" +import z from "zod" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" + +const getAllInstruments = createServerFn().handler(async () => { + const { data } = await getSupabaseServerClient().from("instruments").select() + return data +}) + +const insertAInstrument = createServerFn() + .inputValidator(z.object({ name: z.string() })) + .handler(async ({ data }) => { + const { data: resp } = await getSupabaseServerClient() + .from("instruments") + .insert(data) + return resp + }) + +const deleteAInstrument = createServerFn() + .inputValidator(z.object({ id: z.number() })) + .handler(async ({ data }) => { + const { data: resp } = await getSupabaseServerClient() + .from("instruments") + .delete() + .eq("id", data.id) + return resp + }) + +export { getAllInstruments, insertAInstrument, deleteAInstrument } diff --git a/src/lib/server/user.ts b/src/lib/server/user.ts index 5f4cbfb..ffc24fb 100644 --- a/src/lib/server/user.ts +++ b/src/lib/server/user.ts @@ -4,7 +4,7 @@ import { getSupabaseServerClient } from "@/integrations/supabase/supabase" import { loginFormSchema, signupFormSchema, - userListParamsSchema, + userListParamsSchema } from "../validation/user" const login = createServerFn({ method: "POST" }) @@ -14,18 +14,18 @@ const login = createServerFn({ method: "POST" }) const login = await supabase.auth.signInWithPassword({ email: data.email, - password: data.password, + password: data.password }) if (login.error) { return { error: true, - message: login.error.message, + message: login.error.message } } return { - error: false, + error: false } }) @@ -36,14 +36,14 @@ const logout = createServerFn().handler(async () => { if (error) { return { error: true, - message: error.message, + message: error.message } } throw redirect({ to: "/", viewTransition: true, - replace: true, + replace: true }) }) @@ -58,18 +58,19 @@ const signup = createServerFn({ method: "POST" }) data: { name: data.name, location: data.location, - }, - }, + role: data.role + } + } }) if (error) { return { error: true, - message: error.message, + message: error.message } } throw redirect({ - href: data.redirectUrl || "/", + href: data.redirectUrl || "/" }) }) @@ -79,7 +80,7 @@ const userData = createServerFn().handler(async () => { if (error || !data.user) { return { error: true, - message: error?.message ?? "Unknown error", + message: error?.message ?? "Unknown error" } } return { @@ -87,9 +88,9 @@ const userData = createServerFn().handler(async () => { id: data.user.id, email: data.user.email, name: data.user.user_metadata.name || "", - location: data.user.user_metadata.location || "", + location: data.user.user_metadata.location || "" }, - error: false, + error: false } }) @@ -102,12 +103,12 @@ const resendConfirmationEmail = createServerFn({ method: "POST" }) if (error) { return { error: true, - message: error.message, + message: error.message } } return { - error: false, + error: false } }) @@ -117,18 +118,18 @@ const userList = createServerFn() const supabase = getSupabaseServerClient() const users = await supabase.auth.admin.listUsers({ page: data.page, - perPage: data.limit, + perPage: data.limit }) if (users.error) { return { error: true, - message: users.error.message, + message: users.error.message } } return { users: users.data, - error: false, + error: false } }) @@ -138,5 +139,5 @@ export const user = { signup, userData, resendConfirmationEmail, - userList, + userList } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5e1abdf..e12e7b4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,3 +4,24 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export async function searchLocation(query: string) { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=1` + ) + const data = await response.json() + + if (data.length > 0) { + const place = data[0] + return { + coords: `(${place.lat}, ${place.lon})`, // Formato para el tipo POINT de Postgres + details: place, + city: + place.address?.city || + place.address?.town || + place.display_name.split(",")[0], + country: place.address?.country + } + } + return null +} diff --git a/src/lib/validation/user.ts b/src/lib/validation/user.ts index 32244cf..8abd12a 100644 --- a/src/lib/validation/user.ts +++ b/src/lib/validation/user.ts @@ -10,7 +10,8 @@ export const signupFormSchema = z.object({ password: z.string().min(6, "La contraseña debe tener al menos 6 caracteres"), name: z.string().min(1, "El nombre es obligatorio"), location: z.string().min(1, "La ubicación es obligatoria"), - redirectUrl: z.string().optional() + redirectUrl: z.string().optional(), + role: z.enum(["pilot", "employer"]) }) // Schema extendido para el formulario cliente (incluye confirmación de contraseña) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 499577b..5ed416a 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -51,7 +51,7 @@ export const Route = createRootRouteWithContext()({ function RootDocument({ children }: { children: React.ReactNode }) { return ( - + diff --git a/src/routes/_auth/dashboard.tsx b/src/routes/_auth/dashboard.tsx index d082982..486ccc3 100644 --- a/src/routes/_auth/dashboard.tsx +++ b/src/routes/_auth/dashboard.tsx @@ -1,4 +1,13 @@ -import { Card } from "@heroui/react" +import { + Button, + Card, + CardContent, + CardHeader, + Chip, + ScrollShadow, + Spinner +} from "@heroui/react" +import { useMutation, useQuery } from "@tanstack/react-query" import { createFileRoute } from "@tanstack/react-router" import { Drone } from "lucide-react" import { useState } from "react" @@ -10,6 +19,11 @@ import { MarkerPopup, MarkerTooltip } from "@/components/maps/map" +import { + deleteAInstrument, + getAllInstruments, + insertAInstrument +} from "@/lib/server/instruments" export const Route = createFileRoute("/_auth/dashboard")({ component: RouteComponent @@ -22,57 +36,134 @@ function RouteComponent() { name: "Location 1", lat: 40.76, lng: -73.98 - }, - { - id: 2, - name: "Location 2", - lat: 40.77, - lng: -73.99 - }, - { - id: 3, - name: "Location 3", - lat: 40.5874827, - lng: -1.7925343 } ] + + const { data: instruments, isPending } = useQuery({ + queryKey: ["instruments"], + queryFn: async () => { + return await getAllInstruments() + } + }) + + const insertInstrument = useMutation({ + mutationKey: ["instruments"], + mutationFn: async (data: { name: string }) => { + return await insertAInstrument({ + data: { + name: data.name + } + }) + } + }) + + const deleteInstrument = useMutation({ + mutationKey: ["instruments"], + mutationFn: async (data: { id: number }) => { + return await deleteAInstrument({ + data: { + id: data.id + } + }) + } + }) + + console.log(instruments) + const [viewport, setViewport] = useState({ center: [40.5874827, -1.7925343], zoom: 8, bearing: 0, pitch: 0 }) + return ( -
- - - {locations.map((location) => ( - - {/* Prueba para ssl */} - - - - {location.name} - -
-

{location.name}

-

- {location.lat.toFixed(4)}, {location.lng.toFixed(4)} -

-
-
-
- ))} -
+
+ + +

Dashboard

+
+ +
+
+ + Instrumentos + +
{ + e.preventDefault() + const formData = Object.fromEntries( + new FormData(e.currentTarget) + ) + + console.log(formData) + insertInstrument.mutate(formData as { name: string }) + }} + > + + +
+
+ {isPending && } +
+ + {instruments?.map((instrument) => ( +
+

{instrument.name}

+ +
+ ))} +
+
+
+ + {locations.map((location) => ( + + {/* Prueba para ssl */} + + + + {location.name} + +
+

+ {location.name} +

+

+ {location.lat.toFixed(4)}, {location.lng.toFixed(4)} +

+
+
+
+ ))} +
+
+
+
) diff --git a/src/routes/access.login.tsx b/src/routes/access.login.tsx index 4111127..5adf2a7 100644 --- a/src/routes/access.login.tsx +++ b/src/routes/access.login.tsx @@ -5,11 +5,10 @@ import { Form, Input, Label, - Spinner, TextField } from "@heroui/react" import { createFileRoute } from "@tanstack/react-router" -import { LogIn } from "lucide-react" +import { ArrowRight, Drone } from "lucide-react" import { useLogin } from "@/lib/hooks/useLogin" export const Route = createFileRoute("/access/login")({ @@ -37,13 +36,11 @@ function RouteComponent() { type="email" name="email" variant="secondary" - className="py-4 text-lg" + className=" text-lg" isRequired defaultValue={import.meta.env.VITE_LOGIN_USER} > - + @@ -51,13 +48,11 @@ function RouteComponent() { type="password" name="password" variant="secondary" - className="py-4 text-lg" + className=" text-lg" isRequired defaultValue={import.meta.env.VITE_PASSWORD_USER} > - + @@ -70,12 +65,16 @@ function RouteComponent() { size="lg" isPending={isPending} > - {isPending ? : } Entrar + {isPending ? ( + + ) : ( + + )}
- -
- - -
-
-
-
- TanStack Logo -

- TANSTACK{" "} - - START - -

-
-

- The framework for next generation AI applications +

+
+
+

+ Panel de Control +

+

+ ID: 550e8400-e29b-41d4-a716-446655440000

-

- Full-stack framework powered by TanStack Router for React and Solid. - Build modern applications with server functions, streaming, and type - safety. -

-
- - Documentation - -

- Begin your TanStack Start journey by editing{" "} - - /src/routes/index.tsx - +

+
+ +
{" "} + ACTIVO + +
+
+ +
+ {[ + { label: "OFERTAS", val: "+300", unit: "", color: "text-white" }, + { + label: "PILOTOS", + val: "+400", + unit: "", + color: "text-gray-200/60" + }, + { + label: "TRAYECTOS", + val: "+400", + unit: "", + color: "text-white" + }, + { + label: "PUESTOS", + val: "+300", + unit: "", + color: "text-gray-200/60" + } + ].map((s, i) => ( +
+

+ {s.label} +

+

+ {s.val} + {s.unit}

-
-
+ ))} +
-
-
- {features.map((feature, index) => ( -
-
{feature.icon}
-

- {feature.title} -

-

- {feature.description} -

-
- ))} +
+
+ + {(locations || [])?.map((location) => ( + + {/* Prueba para ssl */} + + {/* */} +
+ + {location.name} + +
+

+ {location.name} +

+

+ {location.lat.toFixed(4)}, {location.lng.toFixed(4)} +

+
+
+ + ))} +
-
+
+ +
+
+
+
+
+
+
+ ) +} + +function App() { + const navigate = Route.useNavigate() + return ( +
+ {/* NAVBAR */} + + + {/* HERO / LANDING */} +
+
+
+
+
+
+
+ + v4.0.2 +
+ +

+ ENCUENTRA A
+ + TU PILOTO + +

+ +

+ La plataforma definitiva para la gestión de pilotos de drones. + Conecta con los mejores profesionales y lleva tus proyectos al + siguiente nivel. +

+ +
+ + +
+
+
+ {[ + { name: "PostgreSQL", icon: }, + { name: "React", icon: }, + { name: "Tailwind", icon: }, + { name: "TanStack", icon: }, + { name: "TypeScript", icon: }, + { name: "Vite", icon: } + ].map((logo, index) => ( +
+
{logo.icon}
+ + {logo.name} + +
+ ))} +
+ + {/* Gradientes laterales para suavizar la entrada/salida (Fading effect) */} +
+
+
+
+
+ {/* DASHBOARD DEMO SECTION */} +
+
+ {/* Cabecera del simulador */} +
+
+
+
+
+
+
+ BUFFER: 0ms + + PG_CONNECTED + +
+
+ +
+
+ + + {/* Main Content Area */} +
+ +
+
+
+
+
+
+ + {/* FOOTER TÉCNICO */} +
) } diff --git a/src/styles/globals.css b/src/styles/globals.css index 740cdab..8e53306 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,22 +1,60 @@ -@import 'tailwindcss'; -@import "@heroui/styles"; +@import "tailwindcss"; +@import "@heroui/styles"; -@import 'tw-animate-css'; +@import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); +@theme { + /* Animación de balanceo suave (vuelo) */ + @keyframes float { + 0%, + 100% { + transform: translateY(0) rotate(0deg); + } + 50% { + transform: translateY(-15px) rotate(1deg); + } + } + + /* Animación de zoom de cámara de dron */ + @keyframes drone-zoom { + from { + transform: scale(1); + } + to { + transform: scale(1.1); + } + } + + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } + } + + --animate-marquee: marquee 30s linear infinite; + + /* Registro de las animaciones para usarlas como clases */ + --animate-float: float 6s ease-in-out infinite; + --animate-drone-zoom: drone-zoom 20s linear infinite alternate; +} + body { - @apply m-0; - font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', - 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + @apply m-0; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } code { - font-family: - source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; + font-family: + source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } /* * HeroUI Theme Customization @@ -24,85 +62,91 @@ code { * Only includes base variables from variables.css * @see https://v3.heroui.com/docs/react/getting-started/theming */ +/* + * HeroUI Theme Customization + * Add this to your global.css after importing @heroui/styles + * Only includes base variables from variables.css + * @see https://heroui.com/docs/react/getting-started/theming + */ :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); + /* Theme Colors (Light Mode) */ + --accent: oklch(76.97% 0.2124 148.67); + --accent-foreground: oklch(15% 0.03 148.67); + --background: oklch(97.02% 0.0069 148.67); + --border: oklch(90.00% 0.0069 148.67); + --danger: oklch(65.32% 0.236 13.12); + --danger-foreground: oklch(99.11% 0 0); + --default: oklch(94.00% 0.0069 148.67); + --default-foreground: oklch(21.03% 0.0059 148.67); + --field-background: oklch(100.00% 0.0034 148.67); + --field-foreground: oklch(21.03% 0.0069 148.67); + --field-placeholder: oklch(55.17% 0.0138 148.67); + --focus: oklch(76.97% 0.2124 148.67); + --foreground: oklch(21.03% 0.0069 148.67); + --muted: oklch(55.17% 0.0138 148.67); + --overlay: oklch(100.00% 0.0021 148.67); + --overlay-foreground: oklch(21.03% 0.0069 148.67); + --scrollbar: oklch(87.10% 0.0069 148.67); + --segment: oklch(100.00% 0.0069 148.67); + --segment-foreground: oklch(21.03% 0.0069 148.67); + --separator: oklch(92.00% 0.0069 148.67); + --success: oklch(73.29% 0.1962 138.19); + --success-foreground: oklch(21.03% 0.0059 138.19); + --surface: oklch(100.00% 0.0034 148.67); + --surface-foreground: oklch(21.03% 0.0069 148.67); + --surface-secondary: oklch(95.24% 0.0055 148.67); + --surface-secondary-foreground: oklch(21.03% 0.0069 148.67); + --surface-tertiary: oklch(93.73% 0.0055 148.67); + --surface-tertiary-foreground: oklch(21.03% 0.0069 148.67); + --warning: oklch(78.19% 0.1607 59.71); + --warning-foreground: oklch(21.03% 0.0059 59.71); - /* Border Radius */ - --radius: 0.125rem; - --field-radius: 0.125rem; + /* Border Radius */ + --radius: 0.5rem; + --field-radius: 0.125rem; - /* Font Family */ - /* Make sure to load Inter font in your app */ - --font-sans: var(--font-inter); + /* Font Family */ + /* Make sure to load Hanken Grotesk font in your app */ + --font-sans: var(--font-hanken-grotesk); } .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); -} \ No newline at end of file + color-scheme: dark; + /* Theme Colors (Dark Mode) */ + --accent: oklch(76.97% 0.2124 148.67); + --accent-foreground: oklch(15% 0.03 148.67); + --background: oklch(12.00% 0.0069 148.67); + --border: oklch(28.00% 0.0069 148.67); + --danger: oklch(59.40% 0.1994 12.01); + --danger-foreground: oklch(99.11% 0 0); + --default: oklch(27.40% 0.0069 148.67); + --default-foreground: oklch(99.11% 0 0); + --field-background: oklch(21.03% 0.0138 148.67); + --field-foreground: oklch(99.11% 0.0069 148.67); + --field-placeholder: oklch(70.50% 0.0138 148.67); + --focus: oklch(76.97% 0.2124 148.67); + --foreground: oklch(99.11% 0.0069 148.67); + --muted: oklch(70.50% 0.0138 148.67); + --overlay: oklch(21.03% 0.0138 148.67); + --overlay-foreground: oklch(99.11% 0.0069 148.67); + --scrollbar: oklch(70.50% 0.0069 148.67); + --segment: oklch(39.64% 0.0069 148.67); + --segment-foreground: oklch(99.11% 0.0069 148.67); + --separator: oklch(25.00% 0.0069 148.67); + --success: oklch(73.29% 0.1962 138.19); + --success-foreground: oklch(21.03% 0.0059 138.19); + --surface: oklch(21.03% 0.0138 148.67); + --surface-foreground: oklch(99.11% 0.0069 148.67); + --surface-secondary: oklch(25.70% 0.0103 148.67); + --surface-secondary-foreground: oklch(99.11% 0.0069 148.67); + --surface-tertiary: oklch(27.21% 0.0103 148.67); + --surface-tertiary-foreground: oklch(99.11% 0.0069 148.67); + --warning: oklch(82.03% 0.1407 63.72); + --warning-foreground: oklch(21.03% 0.0059 63.72); +}