Compare commits
20 Commits
main
...
feature/dr
| Author | SHA1 | Date | |
|---|---|---|---|
| 06f99c0eef | |||
| e5c6278c90 | |||
|
|
e1c0866cdf | ||
|
|
974337af2b | ||
| 96ebac5547 | |||
| 970c8b33db | |||
|
|
c0764fcc84 | ||
|
|
c8fcdd7f29 | ||
|
|
6326d0bc06 | ||
|
|
8489d37979 | ||
|
|
7bc5fcb249 | ||
|
|
d4d384ba2b | ||
|
|
a2ae7d5b5a | ||
|
|
e45772e2a9 | ||
|
|
51c7b9f86d | ||
|
|
83b86ab0f0 | ||
|
|
5d178709ef | ||
|
|
775133281e | ||
|
|
d68a0113b2 | ||
|
|
05a83e3bcb |
5
.env
Normal file
5
.env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
SUPABASE_URL="https://qsssikzgwomudkwfmgad.supabase.co"
|
||||||
|
# DATABASE_URL="postgresql://postgres.qsssikzgwomudkwfmgad:etrTXNz3ZOwaLJmT@aws-0-eu-north-1.pooler.supabase.com:6543/postgres"
|
||||||
|
DATABASE_URL="postgresql://postgres.qsssikzgwomudkwfmgad:Wrongly1-Untimed0-Peculiar0-Unlikable7-Cubbyhole8@aws-0-eu-north-1.pooler.supabase.com:6543/postgre"
|
||||||
|
APIKEY_MAPS="AIzaSyAwfOShBqkBcS46WqmlsIVWQJ8gpdOPk_4"
|
||||||
|
SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFzc3Npa3pnd29tdWRrd2ZtZ2FkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQzMjY1NTQsImV4cCI6MjA2OTkwMjU1NH0.BTSscdTcPP1GVmMB-H5caLpWsfuAw1V6mXiqogF8TjU"
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -1 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
count.txt
|
||||||
|
.nitro
|
||||||
|
.tanstack
|
||||||
|
.output
|
||||||
|
.vinxi
|
||||||
35
.vscode/settings.json
vendored
Normal file
35
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"files.readonlyInclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports.biome": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,13 +6,14 @@
|
|||||||
"useIgnoreFile": false
|
"useIgnoreFile": false
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": false,
|
"ignoreUnknown": true,
|
||||||
"includes": [
|
"includes": [
|
||||||
"**/src/**/*",
|
"**/src/**/*",
|
||||||
"**/.vscode/**/*",
|
"**/.vscode/**/*",
|
||||||
"**/index.html",
|
"**/index.html",
|
||||||
"**/vite.config.js",
|
"**/vite.config.js",
|
||||||
"!**/src/routeTree.gen.ts"
|
"!**/src/routeTree.gen.ts",
|
||||||
|
"!**/node_modules/**/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|||||||
10
drizzle.config.ts
Normal file
10
drizzle.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: './src/integrations/drizzle/db/schema.ts',
|
||||||
|
out: './src/integrations/supabase/migrations',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!
|
||||||
|
},
|
||||||
|
});
|
||||||
879
package-lock.json
generated
879
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -13,6 +13,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroui/react": "^2.8.2",
|
"@heroui/react": "^2.8.2",
|
||||||
|
"@supabase/ssr": "^0.6.1",
|
||||||
|
"@supabase/supabase-js": "^2.53.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.84.1",
|
"@tanstack/react-query": "^5.84.1",
|
||||||
"@tanstack/react-query-devtools": "^5.84.1",
|
"@tanstack/react-query-devtools": "^5.84.1",
|
||||||
@ -21,17 +23,25 @@
|
|||||||
"@tanstack/react-router-with-query": "^1.130.12",
|
"@tanstack/react-router-with-query": "^1.130.12",
|
||||||
"@tanstack/react-start": "^1.130.15",
|
"@tanstack/react-start": "^1.130.15",
|
||||||
"@tanstack/router-plugin": "^1.130.15",
|
"@tanstack/router-plugin": "^1.130.15",
|
||||||
|
"@vis.gl/react-google-maps": "^1.5.5",
|
||||||
|
"drizzle-orm": "^0.44.4",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"zod": "^4.0.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.1.3",
|
"@biomejs/biome": "2.1.3",
|
||||||
|
"@tanstack/react-router-ssr-query": "^1.131.5",
|
||||||
|
"@types/google.maps": "^3.58.1",
|
||||||
"@types/react": "^19.1.9",
|
"@types/react": "^19.1.9",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"drizzle-kit": "^0.31.4",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
|
|||||||
BIN
public/drone.webp
Normal file
BIN
public/drone.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 402 KiB |
BIN
public/profile.png
Normal file
BIN
public/profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
4
readme.md
Normal file
4
readme.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
## Supabase
|
||||||
|
Usuarios de prueba demo12 - juan.penalver@outlook.com
|
||||||
|
|
||||||
|
test
|
||||||
131
src/integrations/drizzle/db/schema.ts
Normal file
131
src/integrations/drizzle/db/schema.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { sql } from "drizzle-orm"
|
||||||
|
import {
|
||||||
|
doublePrecision,
|
||||||
|
foreignKey,
|
||||||
|
integer,
|
||||||
|
pgPolicy,
|
||||||
|
pgTable,
|
||||||
|
serial,
|
||||||
|
text,
|
||||||
|
uuid,
|
||||||
|
varchar
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
import { authenticatedRole, authUsers } from "drizzle-orm/supabase"
|
||||||
|
|
||||||
|
export const users = pgTable("demo", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
fullName: text("full_name"),
|
||||||
|
phone: varchar("phone", { length: 256 })
|
||||||
|
})
|
||||||
|
|
||||||
|
export const profiles = pgTable(
|
||||||
|
"profiles",
|
||||||
|
{
|
||||||
|
id: uuid("id").notNull().primaryKey(),
|
||||||
|
firstName: text("first_name"),
|
||||||
|
lastName: text("last_name")
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.id],
|
||||||
|
foreignColumns: [authUsers.id],
|
||||||
|
name: "profiles_id_fkey"
|
||||||
|
}).onDelete("cascade"),
|
||||||
|
pgPolicy("select-own-profile", {
|
||||||
|
for: "select",
|
||||||
|
to: authenticatedRole,
|
||||||
|
using: sql`${table.id} = auth.uid()`
|
||||||
|
}),
|
||||||
|
pgPolicy("update-own-profile", {
|
||||||
|
for: "update",
|
||||||
|
to: authenticatedRole,
|
||||||
|
using: sql`${table.id} = auth.uid()`,
|
||||||
|
withCheck: sql`${table.id} = auth.uid()`
|
||||||
|
}),
|
||||||
|
pgPolicy("insert-profile", {
|
||||||
|
for: "insert",
|
||||||
|
to: authenticatedRole,
|
||||||
|
withCheck: sql`${table.id} = auth.uid()`
|
||||||
|
})
|
||||||
|
]
|
||||||
|
).enableRLS()
|
||||||
|
|
||||||
|
// === Catálogo de certificaciones ===
|
||||||
|
export const certifications = pgTable(
|
||||||
|
"certifications",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
name: text("name").notNull()
|
||||||
|
},
|
||||||
|
() => [
|
||||||
|
// Política: todos los usuarios autenticados pueden leer, nadie puede escribir
|
||||||
|
pgPolicy("select-certifications", {
|
||||||
|
for: "select",
|
||||||
|
to: authenticatedRole,
|
||||||
|
using: sql`true`
|
||||||
|
})
|
||||||
|
]
|
||||||
|
).enableRLS()
|
||||||
|
|
||||||
|
// === Catálogo de modelos de drones ===
|
||||||
|
export const droneModels = pgTable(
|
||||||
|
"drone_models",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
name: text("name").notNull()
|
||||||
|
},
|
||||||
|
() => [
|
||||||
|
pgPolicy("select-drone-models", {
|
||||||
|
for: "select",
|
||||||
|
to: authenticatedRole,
|
||||||
|
using: sql`true`
|
||||||
|
})
|
||||||
|
]
|
||||||
|
).enableRLS()
|
||||||
|
|
||||||
|
// === Tabla principal de pilotos ===
|
||||||
|
export const pilots = pgTable(
|
||||||
|
"pilots",
|
||||||
|
{
|
||||||
|
id: uuid("id").notNull().primaryKey(), // Igual que auth.uid()
|
||||||
|
name: text("name"),
|
||||||
|
location: text("location"),
|
||||||
|
latitude: doublePrecision("latitude"),
|
||||||
|
longitude: doublePrecision("longitude"),
|
||||||
|
company: text("company"),
|
||||||
|
position: text("position"),
|
||||||
|
description: text("description"),
|
||||||
|
differentiation: text("differentiation"),
|
||||||
|
coverageAreas: text("coverage_areas"),
|
||||||
|
specializationAreas: text("specialization_areas"),
|
||||||
|
certificationIds: integer("certification_ids").array(), // IDs de tabla certifications
|
||||||
|
droneModelIds: integer("drone_model_ids").array(), // IDs de tabla drone_models
|
||||||
|
email: text("email")
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
// Relación con la tabla auth.users
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.id],
|
||||||
|
foreignColumns: [authUsers.id],
|
||||||
|
name: "pilots_id_fkey"
|
||||||
|
}).onDelete("cascade"),
|
||||||
|
|
||||||
|
// === RLS ===
|
||||||
|
pgPolicy("select-own-pilot", {
|
||||||
|
for: "select",
|
||||||
|
to: authenticatedRole,
|
||||||
|
using: sql`${table.id} = auth.uid()`
|
||||||
|
}),
|
||||||
|
pgPolicy("update-own-pilot", {
|
||||||
|
for: "update",
|
||||||
|
to: authenticatedRole,
|
||||||
|
using: sql`${table.id} = auth.uid()`,
|
||||||
|
withCheck: sql`${table.id} = auth.uid()`
|
||||||
|
}),
|
||||||
|
pgPolicy("insert-pilot", {
|
||||||
|
for: "insert",
|
||||||
|
to: authenticatedRole,
|
||||||
|
withCheck: sql`${table.id} = auth.uid()`
|
||||||
|
})
|
||||||
|
]
|
||||||
|
).enableRLS()
|
||||||
10
src/integrations/drizzle/index.tsx
Normal file
10
src/integrations/drizzle/index.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||||
|
import postgres from 'postgres'
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL!
|
||||||
|
|
||||||
|
// Disable prefetch as it is not supported for "Transaction" pool mode
|
||||||
|
const client = postgres(connectionString, { prepare: false })
|
||||||
|
export const db = drizzle(client);
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { HeroUIProvider as HeroProvider } from "@heroui/react"
|
import { HeroUIProvider as HeroProvider } from "@heroui/react"
|
||||||
|
|
||||||
export const HeroUIProvider = ({ children }: { children: React.ReactNode }) => {
|
export const HeroUIProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
return <HeroProvider>{children}</HeroProvider>
|
return <HeroProvider validationBehavior="native">{children}</HeroProvider>
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/integrations/sonner/provider.tsx
Normal file
13
src/integrations/sonner/provider.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Toaster } from "sonner"
|
||||||
|
|
||||||
|
export const SonnerProvider = () => {
|
||||||
|
return (
|
||||||
|
<Toaster
|
||||||
|
position="top-right"
|
||||||
|
duration={3000}
|
||||||
|
closeButton
|
||||||
|
richColors
|
||||||
|
visibleToasts={3}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
IF NOT EXISTS CREATE TABLE "demo" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"full_name" text,
|
||||||
|
"phone" varchar(256)
|
||||||
|
);
|
||||||
11
src/integrations/supabase/migrations/0001_nice_gargoyle.sql
Normal file
11
src/integrations/supabase/migrations/0001_nice_gargoyle.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE "profiles" (
|
||||||
|
"id" uuid PRIMARY KEY NOT NULL,
|
||||||
|
"first_name" text,
|
||||||
|
"last_name" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "profiles" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_id_fkey" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE POLICY "select-own-profile" ON "profiles" AS PERMISSIVE FOR SELECT TO "authenticated" USING ("profiles"."id" = auth.uid());--> statement-breakpoint
|
||||||
|
CREATE POLICY "update-own-profile" ON "profiles" AS PERMISSIVE FOR UPDATE TO "authenticated" USING ("profiles"."id" = auth.uid()) WITH CHECK ("profiles"."id" = auth.uid());--> statement-breakpoint
|
||||||
|
CREATE POLICY "insert-profile" ON "profiles" AS PERMISSIVE FOR INSERT TO "authenticated" WITH CHECK ("profiles"."id" = auth.uid());
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
CREATE TABLE "certifications" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "certifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
CREATE TABLE "drone_models" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "drone_models" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
CREATE TABLE "pilots" (
|
||||||
|
"id" uuid PRIMARY KEY NOT NULL,
|
||||||
|
"name" text,
|
||||||
|
"location" text,
|
||||||
|
"latitude" double precision,
|
||||||
|
"longitude" double precision,
|
||||||
|
"company" text,
|
||||||
|
"position" text,
|
||||||
|
"description" text,
|
||||||
|
"differentiation" text,
|
||||||
|
"coverage_areas" text,
|
||||||
|
"specialization_areas" text,
|
||||||
|
"certification_ids" integer[],
|
||||||
|
"drone_model_ids" integer[],
|
||||||
|
"email" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "pilots" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
ALTER TABLE "pilots" ADD CONSTRAINT "pilots_id_fkey" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE POLICY "select-certifications" ON "certifications" AS PERMISSIVE FOR SELECT TO "authenticated" USING (true);--> statement-breakpoint
|
||||||
|
CREATE POLICY "select-drone-models" ON "drone_models" AS PERMISSIVE FOR SELECT TO "authenticated" USING (true);--> statement-breakpoint
|
||||||
|
CREATE POLICY "select-own-pilot" ON "pilots" AS PERMISSIVE FOR SELECT TO "authenticated" USING ("pilots"."id" = auth.uid());--> statement-breakpoint
|
||||||
|
CREATE POLICY "update-own-pilot" ON "pilots" AS PERMISSIVE FOR UPDATE TO "authenticated" USING ("pilots"."id" = auth.uid()) WITH CHECK ("pilots"."id" = auth.uid());--> statement-breakpoint
|
||||||
|
CREATE POLICY "insert-pilot" ON "pilots" AS PERMISSIVE FOR INSERT TO "authenticated" WITH CHECK ("pilots"."id" = auth.uid());
|
||||||
50
src/integrations/supabase/migrations/meta/0000_snapshot.json
Normal file
50
src/integrations/supabase/migrations/meta/0000_snapshot.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"id": "c266fe94-b863-4b6c-930c-44af8af68c1a",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.demo": {
|
||||||
|
"name": "demo",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"name": "full_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"name": "phone",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/integrations/supabase/migrations/meta/0001_snapshot.json
Normal file
125
src/integrations/supabase/migrations/meta/0001_snapshot.json
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"id": "7d0d4272-65ba-45cf-9dd3-a5e2008d3744",
|
||||||
|
"prevId": "c266fe94-b863-4b6c-930c-44af8af68c1a",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.profiles": {
|
||||||
|
"name": "profiles",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"name": "first_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"name": "last_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"profiles_id_fkey": {
|
||||||
|
"name": "profiles_id_fkey",
|
||||||
|
"tableFrom": "profiles",
|
||||||
|
"tableTo": "users",
|
||||||
|
"schemaTo": "auth",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {
|
||||||
|
"select-own-profile": {
|
||||||
|
"name": "select-own-profile",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "SELECT",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"using": "\"profiles\".\"id\" = auth.uid()"
|
||||||
|
},
|
||||||
|
"update-own-profile": {
|
||||||
|
"name": "update-own-profile",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "UPDATE",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"using": "\"profiles\".\"id\" = auth.uid()",
|
||||||
|
"withCheck": "\"profiles\".\"id\" = auth.uid()"
|
||||||
|
},
|
||||||
|
"insert-profile": {
|
||||||
|
"name": "insert-profile",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "INSERT",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"withCheck": "\"profiles\".\"id\" = auth.uid()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.demo": {
|
||||||
|
"name": "demo",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"name": "full_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"name": "phone",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
336
src/integrations/supabase/migrations/meta/0002_snapshot.json
Normal file
336
src/integrations/supabase/migrations/meta/0002_snapshot.json
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
{
|
||||||
|
"id": "462e99b2-ba6b-4c91-9f6b-891795695955",
|
||||||
|
"prevId": "7d0d4272-65ba-45cf-9dd3-a5e2008d3744",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.certifications": {
|
||||||
|
"name": "certifications",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {
|
||||||
|
"select-certifications": {
|
||||||
|
"name": "select-certifications",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "SELECT",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"using": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.drone_models": {
|
||||||
|
"name": "drone_models",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {
|
||||||
|
"select-drone-models": {
|
||||||
|
"name": "select-drone-models",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "SELECT",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"using": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.pilots": {
|
||||||
|
"name": "pilots",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"name": "location",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"latitude": {
|
||||||
|
"name": "latitude",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"longitude": {
|
||||||
|
"name": "longitude",
|
||||||
|
"type": "double precision",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"company": {
|
||||||
|
"name": "company",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"name": "position",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"differentiation": {
|
||||||
|
"name": "differentiation",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"coverage_areas": {
|
||||||
|
"name": "coverage_areas",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"specialization_areas": {
|
||||||
|
"name": "specialization_areas",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"certification_ids": {
|
||||||
|
"name": "certification_ids",
|
||||||
|
"type": "integer[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"drone_model_ids": {
|
||||||
|
"name": "drone_model_ids",
|
||||||
|
"type": "integer[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"pilots_id_fkey": {
|
||||||
|
"name": "pilots_id_fkey",
|
||||||
|
"tableFrom": "pilots",
|
||||||
|
"tableTo": "users",
|
||||||
|
"schemaTo": "auth",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {
|
||||||
|
"select-own-pilot": {
|
||||||
|
"name": "select-own-pilot",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "SELECT",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"using": "\"pilots\".\"id\" = auth.uid()"
|
||||||
|
},
|
||||||
|
"update-own-pilot": {
|
||||||
|
"name": "update-own-pilot",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "UPDATE",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"using": "\"pilots\".\"id\" = auth.uid()",
|
||||||
|
"withCheck": "\"pilots\".\"id\" = auth.uid()"
|
||||||
|
},
|
||||||
|
"insert-pilot": {
|
||||||
|
"name": "insert-pilot",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "INSERT",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"withCheck": "\"pilots\".\"id\" = auth.uid()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.profiles": {
|
||||||
|
"name": "profiles",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"name": "first_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"name": "last_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"profiles_id_fkey": {
|
||||||
|
"name": "profiles_id_fkey",
|
||||||
|
"tableFrom": "profiles",
|
||||||
|
"tableTo": "users",
|
||||||
|
"schemaTo": "auth",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {
|
||||||
|
"select-own-profile": {
|
||||||
|
"name": "select-own-profile",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "SELECT",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"using": "\"profiles\".\"id\" = auth.uid()"
|
||||||
|
},
|
||||||
|
"update-own-profile": {
|
||||||
|
"name": "update-own-profile",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "UPDATE",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"using": "\"profiles\".\"id\" = auth.uid()",
|
||||||
|
"withCheck": "\"profiles\".\"id\" = auth.uid()"
|
||||||
|
},
|
||||||
|
"insert-profile": {
|
||||||
|
"name": "insert-profile",
|
||||||
|
"as": "PERMISSIVE",
|
||||||
|
"for": "INSERT",
|
||||||
|
"to": [
|
||||||
|
"authenticated"
|
||||||
|
],
|
||||||
|
"withCheck": "\"profiles\".\"id\" = auth.uid()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.demo": {
|
||||||
|
"name": "demo",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"name": "full_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"name": "phone",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/integrations/supabase/migrations/meta/_journal.json
Normal file
27
src/integrations/supabase/migrations/meta/_journal.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754932713212,
|
||||||
|
"tag": "0000_talented_doorman",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1755013739316,
|
||||||
|
"tag": "0001_nice_gargoyle",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1755075449620,
|
||||||
|
"tag": "0002_bouncy_apocalypse",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
24
src/integrations/supabase/supabase.ts
Normal file
24
src/integrations/supabase/supabase.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createServerClient } from "@supabase/ssr"
|
||||||
|
import { parseCookies, setCookie } from "@tanstack/react-start/server"
|
||||||
|
|
||||||
|
export function getSupabaseServerClient() {
|
||||||
|
return createServerClient(
|
||||||
|
process.env.SUPABASE_URL as string,
|
||||||
|
process.env.SUPABASE_ANON_KEY as string,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
getAll() {
|
||||||
|
return Object.entries(parseCookies()).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
setAll(cookies) {
|
||||||
|
cookies.forEach((cookie) => {
|
||||||
|
setCookie(cookie.name, cookie.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
37
src/lib/hooks/useValidation.tsx
Normal file
37
src/lib/hooks/useValidation.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
type FormDataValidation = Record<string, FormDataEntryValue>
|
||||||
|
|
||||||
|
export const useValidation = <T,>({
|
||||||
|
defaultSchema
|
||||||
|
}: {
|
||||||
|
defaultSchema?: z.ZodSchema<T>
|
||||||
|
}) => {
|
||||||
|
const [errors, setErrors] = useState<T>()
|
||||||
|
|
||||||
|
const validate = ({
|
||||||
|
formData,
|
||||||
|
schema
|
||||||
|
}: {
|
||||||
|
formData: FormDataValidation
|
||||||
|
schema?: z.ZodType<T>
|
||||||
|
}) => {
|
||||||
|
const result =
|
||||||
|
schema?.safeParse(formData) ?? defaultSchema?.safeParse(formData)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("No schema provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setErrors(z.flattenError(result.error).fieldErrors as T)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(undefined)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return { errors, validate }
|
||||||
|
}
|
||||||
39
src/lib/hooks/user/useCreateUser.tsx
Normal file
39
src/lib/hooks/user/useCreateUser.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import type z from "zod"
|
||||||
|
import { createProfile } from "@/lib/server/user"
|
||||||
|
import { profileFormSchema } from "@/lib/validation/user"
|
||||||
|
import { useValidation } from "../useValidation"
|
||||||
|
|
||||||
|
type TProfileForm = z.infer<typeof profileFormSchema>
|
||||||
|
|
||||||
|
export const useProfile = () => {
|
||||||
|
const { validate, errors } = useValidation({
|
||||||
|
defaultSchema: profileFormSchema
|
||||||
|
})
|
||||||
|
const signup = useMutation({
|
||||||
|
mutationKey: ["create-profile"],
|
||||||
|
mutationFn: async (data: TProfileForm) => createProfile({ data }),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Your profile is created..", {
|
||||||
|
id: "create-profile"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateSignup = (formData: TProfileForm) => {
|
||||||
|
const isValid = validate({ formData })
|
||||||
|
if (!isValid) {
|
||||||
|
toast.error("Don't create", {
|
||||||
|
id: "create-profile"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
signup.mutate(formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile: validateSignup,
|
||||||
|
errors,
|
||||||
|
isPending: signup.isPending
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/lib/hooks/user/useLogin.tsx
Normal file
59
src/lib/hooks/user/useLogin.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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/server/user"
|
||||||
|
import { loginFormSchema } from "@/lib/validation/user"
|
||||||
|
import { useValidation } from "../useValidation"
|
||||||
|
|
||||||
|
type TLoginForm = z.infer<typeof loginFormSchema>
|
||||||
|
|
||||||
|
export const useLogin = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { errors, validate } = useValidation({
|
||||||
|
defaultSchema: loginFormSchema
|
||||||
|
})
|
||||||
|
const loginMutation = useMutation({
|
||||||
|
mutationKey: ["login"],
|
||||||
|
mutationFn: async (data: TLoginForm) => {
|
||||||
|
const response = await loginUser({
|
||||||
|
data
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMutate: () => {
|
||||||
|
toast.loading("Logging in...", { id: "login" })
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Login successful! Redirecting to posts..", { id: "login" })
|
||||||
|
navigate({
|
||||||
|
to: "/dashboard"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message, { 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
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/lib/hooks/user/useSignup.tsx
Normal file
44
src/lib/hooks/user/useSignup.tsx
Normal file
@ -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/server/user"
|
||||||
|
import { signupFormSchema } from "@/lib/validation/user"
|
||||||
|
import { useValidation } from "../useValidation"
|
||||||
|
|
||||||
|
type TSignupForm = z.infer<typeof signupFormSchema>
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/lib/server/user.ts
Normal file
122
src/lib/server/user.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { redirect } from "@tanstack/react-router"
|
||||||
|
import { createServerFn } from "@tanstack/react-start"
|
||||||
|
import { eq } from "drizzle-orm"
|
||||||
|
import { db } from "@/integrations/drizzle"
|
||||||
|
import { profiles, users } from "@/integrations/drizzle/db/schema"
|
||||||
|
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
|
||||||
|
import {
|
||||||
|
loginFormSchema,
|
||||||
|
profileFormSchema,
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(data)
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: data.user.id,
|
||||||
|
email: data.user.email,
|
||||||
|
name: data.user.user_metadata.name || "",
|
||||||
|
location: data.user.user_metadata.location || ""
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
options: {
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
location: data.location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (error) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
message: error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw redirect({
|
||||||
|
href: data.redirectUrl || "/"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getAllUsers = createServerFn().handler(async () => {
|
||||||
|
const response = await db.select().from(users)
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
|
||||||
|
export const createProfile = createServerFn({ method: "POST" })
|
||||||
|
.validator(profileFormSchema)
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
await db.insert(profiles).values(data).returning()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getProfile = createServerFn({ method: "POST" })
|
||||||
|
.validator((data: { id: string }) => data)
|
||||||
|
.handler(async ({ data }) => {
|
||||||
|
const { id } = data
|
||||||
|
const response = await db
|
||||||
|
.select()
|
||||||
|
.from(profiles)
|
||||||
|
.where(eq(profiles.id, id))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
return response[0] ?? null
|
||||||
|
})
|
||||||
18
src/lib/validation/user.ts
Normal file
18
src/lib/validation/user.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import z from "zod"
|
||||||
|
|
||||||
|
export const loginFormSchema = z.object({
|
||||||
|
email: z.email("Invalid email address"),
|
||||||
|
password: z.string().min(1, "Password must be at least 1 character long")
|
||||||
|
})
|
||||||
|
|
||||||
|
export const signupFormSchema = z.object({
|
||||||
|
email: z.email("Invalid email address"),
|
||||||
|
password: z.string().min(6, "Password must be at least 6 characters long"),
|
||||||
|
name: z.string().min(1, "The field is required"),
|
||||||
|
location: z.string().min(1, "The field is required"),
|
||||||
|
redirectUrl: z.string().optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const profileFormSchema= z.object({
|
||||||
|
id: z.uuid()
|
||||||
|
})
|
||||||
@ -9,38 +9,129 @@
|
|||||||
// 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 SignupRouteImport } from './routes/signup'
|
||||||
|
import { Route as LogoutRouteImport } from './routes/logout'
|
||||||
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
|
import { Route as AuthedRouteImport } from './routes/_authed'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
import { Route as AuthedRegisterRouteImport } from './routes/_authed/register'
|
||||||
|
import { Route as AuthedDashboardRouteImport } from './routes/_authed/dashboard'
|
||||||
|
|
||||||
|
const SignupRoute = SignupRouteImport.update({
|
||||||
|
id: '/signup',
|
||||||
|
path: '/signup',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const LogoutRoute = LogoutRouteImport.update({
|
||||||
|
id: '/logout',
|
||||||
|
path: '/logout',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const LoginRoute = LoginRouteImport.update({
|
||||||
|
id: '/login',
|
||||||
|
path: '/login',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AuthedRoute = AuthedRouteImport.update({
|
||||||
|
id: '/_authed',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthedRegisterRoute = AuthedRegisterRouteImport.update({
|
||||||
|
id: '/register',
|
||||||
|
path: '/register',
|
||||||
|
getParentRoute: () => AuthedRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthedDashboardRoute = AuthedDashboardRouteImport.update({
|
||||||
|
id: '/dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
getParentRoute: () => AuthedRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
|
'/signup': typeof SignupRoute
|
||||||
|
'/dashboard': typeof AuthedDashboardRoute
|
||||||
|
'/register': typeof AuthedRegisterRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
|
'/signup': typeof SignupRoute
|
||||||
|
'/dashboard': typeof AuthedDashboardRoute
|
||||||
|
'/register': typeof AuthedRegisterRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/_authed': typeof AuthedRouteWithChildren
|
||||||
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
|
'/signup': typeof SignupRoute
|
||||||
|
'/_authed/dashboard': typeof AuthedDashboardRoute
|
||||||
|
'/_authed/register': typeof AuthedRegisterRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/'
|
fullPaths: '/' | '/login' | '/logout' | '/signup' | '/dashboard' | '/register'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/'
|
to: '/' | '/login' | '/logout' | '/signup' | '/dashboard' | '/register'
|
||||||
id: '__root__' | '/'
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/_authed'
|
||||||
|
| '/login'
|
||||||
|
| '/logout'
|
||||||
|
| '/signup'
|
||||||
|
| '/_authed/dashboard'
|
||||||
|
| '/_authed/register'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
AuthedRoute: typeof AuthedRouteWithChildren
|
||||||
|
LoginRoute: typeof LoginRoute
|
||||||
|
LogoutRoute: typeof LogoutRoute
|
||||||
|
SignupRoute: typeof SignupRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/signup': {
|
||||||
|
id: '/signup'
|
||||||
|
path: '/signup'
|
||||||
|
fullPath: '/signup'
|
||||||
|
preLoaderRoute: typeof SignupRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/logout': {
|
||||||
|
id: '/logout'
|
||||||
|
path: '/logout'
|
||||||
|
fullPath: '/logout'
|
||||||
|
preLoaderRoute: typeof LogoutRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/login': {
|
||||||
|
id: '/login'
|
||||||
|
path: '/login'
|
||||||
|
fullPath: '/login'
|
||||||
|
preLoaderRoute: typeof LoginRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/_authed': {
|
||||||
|
id: '/_authed'
|
||||||
|
path: ''
|
||||||
|
fullPath: ''
|
||||||
|
preLoaderRoute: typeof AuthedRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@ -48,11 +139,42 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/_authed/register': {
|
||||||
|
id: '/_authed/register'
|
||||||
|
path: '/register'
|
||||||
|
fullPath: '/register'
|
||||||
|
preLoaderRoute: typeof AuthedRegisterRouteImport
|
||||||
|
parentRoute: typeof AuthedRoute
|
||||||
|
}
|
||||||
|
'/_authed/dashboard': {
|
||||||
|
id: '/_authed/dashboard'
|
||||||
|
path: '/dashboard'
|
||||||
|
fullPath: '/dashboard'
|
||||||
|
preLoaderRoute: typeof AuthedDashboardRouteImport
|
||||||
|
parentRoute: typeof AuthedRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthedRouteChildren {
|
||||||
|
AuthedDashboardRoute: typeof AuthedDashboardRoute
|
||||||
|
AuthedRegisterRoute: typeof AuthedRegisterRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthedRouteChildren: AuthedRouteChildren = {
|
||||||
|
AuthedDashboardRoute: AuthedDashboardRoute,
|
||||||
|
AuthedRegisterRoute: AuthedRegisterRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthedRouteWithChildren =
|
||||||
|
AuthedRoute._addFileChildren(AuthedRouteChildren)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
AuthedRoute: AuthedRouteWithChildren,
|
||||||
|
LoginRoute: LoginRoute,
|
||||||
|
LogoutRoute: LogoutRoute,
|
||||||
|
SignupRoute: SignupRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@ -1,24 +1,32 @@
|
|||||||
import { createRouter as createTanstackRouter } from "@tanstack/react-router"
|
import { createRouter as createTanstackRouter } from "@tanstack/react-router"
|
||||||
import { routerWithQueryClient } from "@tanstack/react-router-with-query"
|
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"
|
||||||
import { createQueryContext } from "./integrations/tanstack-query/context.tsx"
|
import { createQueryContext } from "./integrations/tanstack-query/context.tsx"
|
||||||
import { QueryProvider } from "./integrations/tanstack-query/provider.tsx"
|
import { QueryProvider } from "./integrations/tanstack-query/provider.tsx"
|
||||||
import { routeTree } from "./routeTree.gen.ts"
|
import { routeTree } from "./routeTree.gen.ts"
|
||||||
|
|
||||||
export const createRouter = () => {
|
export const createRouter = () => {
|
||||||
const rqContext = createQueryContext()
|
const { queryClient } = createQueryContext()
|
||||||
|
|
||||||
return routerWithQueryClient(
|
const router = createTanstackRouter({
|
||||||
createTanstackRouter({
|
|
||||||
routeTree,
|
routeTree,
|
||||||
context: { ...rqContext },
|
context: { queryClient, user: null },
|
||||||
defaultPreload: "intent",
|
defaultPreload: "intent",
|
||||||
Wrap: (props: { children: React.ReactNode }) => {
|
Wrap: (props: { children: React.ReactNode }) => {
|
||||||
return <QueryProvider {...rqContext}>{props.children}</QueryProvider>
|
return (
|
||||||
}
|
<QueryProvider {...{ queryClient }}>{props.children}</QueryProvider>
|
||||||
}),
|
|
||||||
rqContext.queryClient
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setupRouterSsrQueryIntegration({
|
||||||
|
router,
|
||||||
|
queryClient,
|
||||||
|
handleRedirects: true,
|
||||||
|
wrapQueryClient: false
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
interface Register {
|
interface Register {
|
||||||
|
|||||||
@ -7,12 +7,21 @@ import {
|
|||||||
} from "@tanstack/react-router"
|
} from "@tanstack/react-router"
|
||||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
||||||
import { HeroUIProvider } from "@/integrations/heroui/provider"
|
import { HeroUIProvider } from "@/integrations/heroui/provider"
|
||||||
|
import { SonnerProvider } from "@/integrations/sonner/provider"
|
||||||
|
import { getUser } from "@/lib/server/user"
|
||||||
|
|
||||||
interface MyRouterContext {
|
interface MyRouterContext {
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
|
user: null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||||
|
beforeLoad: async () => {
|
||||||
|
const user = await getUser()
|
||||||
|
return {
|
||||||
|
...user
|
||||||
|
}
|
||||||
|
},
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
@ -44,7 +53,16 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
<HeadContent />
|
<HeadContent />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div
|
||||||
|
className="min-h-screen bg-gradient-to-br from-green-200 to-emerald-400"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"radial-gradient(50% 50% at 95% 5%, #34d399 0%, #6ee7b7 70%, #f5f5f5 100%)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<HeroUIProvider>{children}</HeroUIProvider>
|
<HeroUIProvider>{children}</HeroUIProvider>
|
||||||
|
</div>
|
||||||
|
<SonnerProvider />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
67
src/routes/_authed.tsx
Normal file
67
src/routes/_authed.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownTrigger
|
||||||
|
} from "@heroui/react"
|
||||||
|
import { createFileRoute, Outlet } from "@tanstack/react-router"
|
||||||
|
import { getProfile } from "@/lib/server/user"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_authed")({
|
||||||
|
beforeLoad: ({ context }) => {
|
||||||
|
if (context.error) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
// TODO: Redirect to login page
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loader: ({ context: { queryClient, user } }) => {
|
||||||
|
queryClient.ensureQueryData({
|
||||||
|
queryKey: ["profile"],
|
||||||
|
queryFn: () => getProfile({ data: { id: user?.id as string } })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
errorComponent: ({ error }) => {
|
||||||
|
if (error.message === "Not authenticated") {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Not authenticated. Please <a href="/login">login</a>.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
},
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const navigate = Route.useNavigate()
|
||||||
|
return (
|
||||||
|
<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" />
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
onAction={(key) => {
|
||||||
|
if (key === "exit") {
|
||||||
|
navigate({ to: "/logout" })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownItem color="danger" key="exit">
|
||||||
|
Logout
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</nav>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
141
src/routes/_authed/dashboard.tsx
Normal file
141
src/routes/_authed/dashboard.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
Chip,
|
||||||
|
Divider
|
||||||
|
} from "@heroui/react"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { getAllUsers, getProfile } from "@/lib/server/user"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_authed/dashboard")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { user } = Route.useRouteContext()
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["users"],
|
||||||
|
queryFn: async () => {
|
||||||
|
return getAllUsers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: profile } = useQuery({
|
||||||
|
queryKey: ["profile"],
|
||||||
|
queryFn: async () => {
|
||||||
|
return getProfile({ data: { id: user?.id as string } })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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 {profile?.firstName ?? "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">
|
||||||
|
Ofertas de vuelo
|
||||||
|
</h2>
|
||||||
|
<p className="text-default-500 text-sm text-start">
|
||||||
|
Explora y gestiona tus ofertas de vuelo activas.
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouteComponent
|
||||||
42
src/routes/_authed/register.tsx
Normal file
42
src/routes/_authed/register.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Button, Form, Input, Slider, Textarea } from "@heroui/react"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import type { FormEvent } from "react"
|
||||||
|
import { useProfile } from "@/lib/hooks/user/useCreateUser"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_authed/register")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { errors, isPending, profile } = useProfile()
|
||||||
|
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const formData = new FormData(e.currentTarget)
|
||||||
|
profile({
|
||||||
|
id: "e6472b9d-01a9-4e2e-8bdc-0ddaa9baf5d8",
|
||||||
|
firstName: formData.get("firstName") as string,
|
||||||
|
lastName: formData.get("lastName") as string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form
|
||||||
|
className="grid gap-2 max-w-sm w-full"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validationErrors={errors}
|
||||||
|
>
|
||||||
|
<Input name="name" label="Nombre completo" isRequired />
|
||||||
|
<Input name="company" label="Empresa" />
|
||||||
|
<Input name="role" label="Cargo" />
|
||||||
|
<Textarea name="description" label="Descripción" />
|
||||||
|
<Textarea name="differentiator" label="¿Qué te hace diferente?" />
|
||||||
|
<Slider name="coverage_areas" label="Areas de cobertura" />
|
||||||
|
<Input name="services" label="Servicios" />{" "}
|
||||||
|
{/* Crear varios checkboxes */}
|
||||||
|
<Button isLoading={isPending} type="submit">
|
||||||
|
Crear usuario
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import logo from "@assets/logo.svg"
|
import logo from "@assets/logo.svg"
|
||||||
|
import { Button } from "@heroui/react"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
@ -6,6 +7,7 @@ export const Route = createFileRoute("/")({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const navigate = Route.useNavigate()
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
|
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
|
||||||
@ -17,22 +19,26 @@ function App() {
|
|||||||
<p>
|
<p>
|
||||||
Edit <code>src/routes/index.tsx</code> and save to reload.
|
Edit <code>src/routes/index.tsx</code> and save to reload.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||||
className="text-[#61dafb] hover:underline"
|
<Button
|
||||||
href="https://reactjs.org"
|
onPress={() =>
|
||||||
target="_blank"
|
navigate({
|
||||||
rel="noopener noreferrer"
|
to: "/login"
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Learn React
|
Login
|
||||||
</a>
|
</Button>
|
||||||
<a
|
<Button
|
||||||
className="text-[#61dafb] hover:underline"
|
onPress={() => {
|
||||||
href="https://tanstack.com"
|
navigate({
|
||||||
target="_blank"
|
to: "/signup"
|
||||||
rel="noopener noreferrer"
|
})
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Learn TanStack
|
Signup
|
||||||
</a>
|
</Button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
47
src/routes/login.tsx
Normal file
47
src/routes/login.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Button, Form, Input } from "@heroui/react"
|
||||||
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
||||||
|
import type { FormEvent } from "react"
|
||||||
|
import { useLogin } from "@/lib/hooks/user/useLogin"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/login")({
|
||||||
|
beforeLoad: ({ context }) => {
|
||||||
|
if (!context?.error) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/dashboard"
|
||||||
|
})
|
||||||
|
// TODO: Redirect to login page
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: LoginComp
|
||||||
|
})
|
||||||
|
|
||||||
|
function LoginComp() {
|
||||||
|
const { errors, isPending, login } = useLogin()
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const formData = new FormData(e.currentTarget)
|
||||||
|
|
||||||
|
login({
|
||||||
|
email: formData.get("email") as string,
|
||||||
|
password: formData.get("password") as string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center flex-col h-screen">
|
||||||
|
<p className="font-semibold mb-3">Login</p>
|
||||||
|
<Form
|
||||||
|
validationErrors={errors}
|
||||||
|
className="grid gap-2 max-w-sm w-full"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<Input name="email" label="Email" />
|
||||||
|
<Input name="password" type="password" label="Password" />
|
||||||
|
<Button type="submit" isLoading={isPending}>
|
||||||
|
Entrar
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/routes/logout.tsx
Normal file
7
src/routes/logout.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { logoutUser } from "@/lib/server/user"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/logout")({
|
||||||
|
preload: false,
|
||||||
|
loader: () => logoutUser()
|
||||||
|
})
|
||||||
44
src/routes/signup.tsx
Normal file
44
src/routes/signup.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Button, Form, Input } from "@heroui/react"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import type { FormEvent } from "react"
|
||||||
|
import { useSignup } from "@/lib/hooks/user/useSignup"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/signup")({
|
||||||
|
component: SignupComp
|
||||||
|
})
|
||||||
|
|
||||||
|
function SignupComp() {
|
||||||
|
const { signup, errors, isPending } = useSignup()
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const formData = new FormData(e.currentTarget)
|
||||||
|
|
||||||
|
signup({
|
||||||
|
email: formData.get("email") as string,
|
||||||
|
password: formData.get("password") as string,
|
||||||
|
name: formData.get("nombre") as string,
|
||||||
|
location: formData.get("location") as string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center flex-col h-screen">
|
||||||
|
<p className="font-semibold mb-3">Signup</p>
|
||||||
|
<Form
|
||||||
|
validationErrors={errors}
|
||||||
|
className="grid gap-2 max-w-sm w-full"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<Input name="email" type="email" label="Email" />
|
||||||
|
<Input name="password" type="password" label="Password" />
|
||||||
|
<Input name="nombre" label="Name" />
|
||||||
|
<Input name="location" label="Location" />
|
||||||
|
<Button type="submit" isLoading={isPending}>
|
||||||
|
Enviar
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Luckiest+Guy&family=Outfit:wght@100..900&family=Rubik+Vinyl&display=swap');
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@plugin '../integrations/heroui/heroui.ts';
|
@plugin '../integrations/heroui/heroui.ts';
|
||||||
|
|
||||||
@ -13,7 +14,15 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-outfit: "Outfit", sans-serif;
|
||||||
|
--font-rubik-vinyl: "Rubik Vinyl", cursive;
|
||||||
|
--font-luckiest-guy: "Luckiest Guy", cursive;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family:
|
font-family:
|
||||||
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user