diff --git a/.env b/.env new file mode 100644 index 0000000..2d74d2a --- /dev/null +++ b/.env @@ -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" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2d192b5..fc44466 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ dist dist-ssr *.local count.txt -.env .nitro .tanstack .output diff --git a/drizzle.config.ts b/drizzle.config.ts index bce5f38..d94979e 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from 'drizzle-kit'; -console.log(process.env.DATABASE_URL) + export default defineConfig({ schema: './src/integrations/drizzle/db/schema.ts', out: './src/integrations/supabase/migrations', diff --git a/package-lock.json b/package-lock.json index 7f81393..acc6d6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@tanstack/react-router-with-query": "^1.130.12", "@tanstack/react-start": "^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", "postgres": "^3.4.7", @@ -30,6 +31,7 @@ "devDependencies": { "@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-dom": "^19.1.7", "@vitejs/plugin-react": "^4.7.0", @@ -7650,6 +7652,12 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", @@ -7867,6 +7875,20 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@vis.gl/react-google-maps": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.5.5.tgz", + "integrity": "sha512-LgHtK1AtE2/BN4dPoK05oWu0jWmeDdyX0Ffqi+mZc+M4apaHn2sUxxKXAxhPF90O9vcsiou/ntm6/XBWX+gpqw==", + "license": "MIT", + "dependencies": { + "@types/google.maps": "^3.54.10", + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "react": ">=16.8.0 || ^19.0 || ^19.0.0-rc", + "react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -9929,6 +9951,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", diff --git a/package.json b/package.json index d454137..3495b7b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@tanstack/react-router-with-query": "^1.130.12", "@tanstack/react-start": "^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", "postgres": "^3.4.7", @@ -36,6 +37,7 @@ "devDependencies": { "@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-dom": "^19.1.7", "@vitejs/plugin-react": "^4.7.0", diff --git a/public/drone.webp b/public/drone.webp new file mode 100644 index 0000000..ac469e5 Binary files /dev/null and b/public/drone.webp differ diff --git a/public/profile.png b/public/profile.png new file mode 100644 index 0000000..18b960e Binary files /dev/null and b/public/profile.png differ diff --git a/readme.md b/readme.md index 178c352..b853eb9 100644 --- a/readme.md +++ b/readme.md @@ -1,2 +1,4 @@ ## Supabase -Usuarios de prueba demo12 - juan.penalver@outlook.com \ No newline at end of file +Usuarios de prueba demo12 - juan.penalver@outlook.com + +test \ No newline at end of file diff --git a/src/integrations/drizzle/db/schema.ts b/src/integrations/drizzle/db/schema.ts index e10f8ba..9412d91 100644 --- a/src/integrations/drizzle/db/schema.ts +++ b/src/integrations/drizzle/db/schema.ts @@ -1,6 +1,131 @@ -import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core"; -export const users = pgTable('demo', { - id: serial('id').primaryKey(), - fullName: text('full_name'), - phone: varchar('phone', { length: 256 }), -}); \ No newline at end of file +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() diff --git a/src/integrations/drizzle/index.tsx b/src/integrations/drizzle/index.tsx index 9ea9454..1ba290a 100644 --- a/src/integrations/drizzle/index.tsx +++ b/src/integrations/drizzle/index.tsx @@ -1,15 +1,10 @@ import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' -import { users } from './db/schema' 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); - -const allUsers = await db.select().from(users); - -console.log(allUsers); \ No newline at end of file diff --git a/src/integrations/supabase/migrations/0000_talented_doorman.sql b/src/integrations/supabase/migrations/0000_talented_doorman.sql index 0c9036c..36609bf 100644 --- a/src/integrations/supabase/migrations/0000_talented_doorman.sql +++ b/src/integrations/supabase/migrations/0000_talented_doorman.sql @@ -1,4 +1,4 @@ -CREATE TABLE "demo" ( +IF NOT EXISTS CREATE TABLE "demo" ( "id" serial PRIMARY KEY NOT NULL, "full_name" text, "phone" varchar(256) diff --git a/src/integrations/supabase/migrations/0001_nice_gargoyle.sql b/src/integrations/supabase/migrations/0001_nice_gargoyle.sql new file mode 100644 index 0000000..df3af4b --- /dev/null +++ b/src/integrations/supabase/migrations/0001_nice_gargoyle.sql @@ -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()); \ No newline at end of file diff --git a/src/integrations/supabase/migrations/0002_bouncy_apocalypse.sql b/src/integrations/supabase/migrations/0002_bouncy_apocalypse.sql new file mode 100644 index 0000000..eb17788 --- /dev/null +++ b/src/integrations/supabase/migrations/0002_bouncy_apocalypse.sql @@ -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()); \ No newline at end of file diff --git a/src/integrations/supabase/migrations/meta/0001_snapshot.json b/src/integrations/supabase/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..c313063 --- /dev/null +++ b/src/integrations/supabase/migrations/meta/0001_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/src/integrations/supabase/migrations/meta/0002_snapshot.json b/src/integrations/supabase/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..0bf34bf --- /dev/null +++ b/src/integrations/supabase/migrations/meta/0002_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/src/integrations/supabase/migrations/meta/_journal.json b/src/integrations/supabase/migrations/meta/_journal.json index ea38f8f..831c324 100644 --- a/src/integrations/supabase/migrations/meta/_journal.json +++ b/src/integrations/supabase/migrations/meta/_journal.json @@ -8,6 +8,20 @@ "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 } ] } \ No newline at end of file diff --git a/src/lib/hooks/user/useCreateUser.tsx b/src/lib/hooks/user/useCreateUser.tsx new file mode 100644 index 0000000..3c49cdb --- /dev/null +++ b/src/lib/hooks/user/useCreateUser.tsx @@ -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 + +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 + } +} diff --git a/src/lib/hooks/user/useLogin.tsx b/src/lib/hooks/user/useLogin.tsx index f24afbc..369a6b5 100644 --- a/src/lib/hooks/user/useLogin.tsx +++ b/src/lib/hooks/user/useLogin.tsx @@ -30,7 +30,7 @@ export const useLogin = () => { onSuccess: () => { toast.success("Login successful! Redirecting to posts..", { id: "login" }) navigate({ - to: "/post" + to: "/dashboard" }) }, onError: (error) => { diff --git a/src/lib/server/user.ts b/src/lib/server/user.ts index 53611a9..67df985 100644 --- a/src/lib/server/user.ts +++ b/src/lib/server/user.ts @@ -1,9 +1,14 @@ import { redirect } from "@tanstack/react-router" import { createServerFn } from "@tanstack/react-start" +import { eq } from "drizzle-orm" import { db } from "@/integrations/drizzle" -import { users } from "@/integrations/drizzle/db/schema" +import { profiles, users } from "@/integrations/drizzle/db/schema" import { getSupabaseServerClient } from "@/integrations/supabase/supabase" -import { loginFormSchema, signupFormSchema } from "../validation/user" +import { + loginFormSchema, + profileFormSchema, + signupFormSchema +} from "../validation/user" export const getUser = createServerFn().handler(async () => { const supabase = getSupabaseServerClient() @@ -14,12 +19,13 @@ export const getUser = createServerFn().handler(async () => { message: error?.message ?? "Unknown error" } } - + console.log(data) return { user: { id: data.user.id, email: data.user.email, - name: data.user.user_metadata.name || "" + name: data.user.user_metadata.name || "", + location: data.user.user_metadata.location || "" }, error: false } @@ -71,7 +77,13 @@ export const signupUser = createServerFn({ method: "POST" }) const supabase = getSupabaseServerClient() const { error } = await supabase.auth.signUp({ email: data.email, - password: data.password + password: data.password, + options: { + data: { + name: data.name, + location: data.location + } + } }) if (error) { return { @@ -85,8 +97,26 @@ export const signupUser = createServerFn({ method: "POST" }) }) }) - export const getAllUsers = createServerFn().handler(async () => { const response = await db.select().from(users) return response -}) \ No newline at end of file +}) + +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 + }) diff --git a/src/lib/validation/user.ts b/src/lib/validation/user.ts index bf73223..619763e 100644 --- a/src/lib/validation/user.ts +++ b/src/lib/validation/user.ts @@ -8,5 +8,11 @@ export const loginFormSchema = z.object({ 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() +}) \ No newline at end of file diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 6f037e6..ba3c753 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -14,7 +14,8 @@ 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 AuthedPostRouteImport } from './routes/_authed/post' +import { Route as AuthedRegisterRouteImport } from './routes/_authed/register' +import { Route as AuthedDashboardRouteImport } from './routes/_authed/dashboard' const SignupRoute = SignupRouteImport.update({ id: '/signup', @@ -40,9 +41,14 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) -const AuthedPostRoute = AuthedPostRouteImport.update({ - id: '/post', - path: '/post', +const AuthedRegisterRoute = AuthedRegisterRouteImport.update({ + id: '/register', + path: '/register', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedDashboardRoute = AuthedDashboardRouteImport.update({ + id: '/dashboard', + path: '/dashboard', getParentRoute: () => AuthedRoute, } as any) @@ -51,14 +57,16 @@ export interface FileRoutesByFullPath { '/login': typeof LoginRoute '/logout': typeof LogoutRoute '/signup': typeof SignupRoute - '/post': typeof AuthedPostRoute + '/dashboard': typeof AuthedDashboardRoute + '/register': typeof AuthedRegisterRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute '/logout': typeof LogoutRoute '/signup': typeof SignupRoute - '/post': typeof AuthedPostRoute + '/dashboard': typeof AuthedDashboardRoute + '/register': typeof AuthedRegisterRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -67,13 +75,14 @@ export interface FileRoutesById { '/login': typeof LoginRoute '/logout': typeof LogoutRoute '/signup': typeof SignupRoute - '/_authed/post': typeof AuthedPostRoute + '/_authed/dashboard': typeof AuthedDashboardRoute + '/_authed/register': typeof AuthedRegisterRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/login' | '/logout' | '/signup' | '/post' + fullPaths: '/' | '/login' | '/logout' | '/signup' | '/dashboard' | '/register' fileRoutesByTo: FileRoutesByTo - to: '/' | '/login' | '/logout' | '/signup' | '/post' + to: '/' | '/login' | '/logout' | '/signup' | '/dashboard' | '/register' id: | '__root__' | '/' @@ -81,7 +90,8 @@ export interface FileRouteTypes { | '/login' | '/logout' | '/signup' - | '/_authed/post' + | '/_authed/dashboard' + | '/_authed/register' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -129,22 +139,31 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/_authed/post': { - id: '/_authed/post' - path: '/post' - fullPath: '/post' - preLoaderRoute: typeof AuthedPostRouteImport + '/_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 { - AuthedPostRoute: typeof AuthedPostRoute + AuthedDashboardRoute: typeof AuthedDashboardRoute + AuthedRegisterRoute: typeof AuthedRegisterRoute } const AuthedRouteChildren: AuthedRouteChildren = { - AuthedPostRoute: AuthedPostRoute, + AuthedDashboardRoute: AuthedDashboardRoute, + AuthedRegisterRoute: AuthedRegisterRoute, } const AuthedRouteWithChildren = diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index a9f57ab..c3c0ce1 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -53,11 +53,19 @@ function RootDocument({ children }: { children: React.ReactNode }) { - {children} +
+ {children} +
) -} +} \ No newline at end of file diff --git a/src/routes/_authed.tsx b/src/routes/_authed.tsx index ec1f096..87e8469 100644 --- a/src/routes/_authed.tsx +++ b/src/routes/_authed.tsx @@ -1,4 +1,12 @@ -import { createFileRoute, redirect } from "@tanstack/react-router" +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 }) => { @@ -7,6 +15,12 @@ export const Route = createFileRoute("/_authed")({ // 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 ( @@ -16,5 +30,38 @@ export const Route = createFileRoute("/_authed")({ ) } throw error - } + }, + component: RouteComponent }) + +function RouteComponent() { + const navigate = Route.useNavigate() + return ( +
+ + +
+ ) +} diff --git a/src/routes/_authed/dashboard.tsx b/src/routes/_authed/dashboard.tsx new file mode 100644 index 0000000..d75babe --- /dev/null +++ b/src/routes/_authed/dashboard.tsx @@ -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 ( +
+ + + + Inicio {profile?.firstName ?? "demo"} + +
+

🏗️ La web está en mantenimiento

+
+
+ + {/* === GRID PRINCIPAL === */} +
+ {/* === Tarjeta 1: Mis drones === */} + + +
+

+ Mis drones +

+

+ Actualiza tus drones en uso o pilotados. +

+
+ +
+ +

Ver más

+
+
+ + {/* === Tarjeta 2: Datos personales === */} + + +
+

+ Datos personales +

+

+ Actualiza tu información, ubicación y detalles. +

+
+ +
+ +

Ver más

+
+
+ + {/* === Tarjeta 3: Ofertas de vuelo === */} + + +
+

+ Ofertas de vuelo +

+

+ Explora y gestiona tus ofertas de vuelo activas. +

+
+ +
+ +

Próximamente...

+
+ +

Ver más

+
+
+ + {/* === Mapa (ocupa 2 columnas) === */} +
+ {/* Aquí irá el mapa */} +
+
+
+
+
+ ) +} + +export default RouteComponent diff --git a/src/routes/_authed/post.tsx b/src/routes/_authed/post.tsx deleted file mode 100644 index f42d444..0000000 --- a/src/routes/_authed/post.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Button } from "@heroui/react" -import { useQuery } from "@tanstack/react-query" -import { createFileRoute } from "@tanstack/react-router" -import { getAllUsers } from "@/lib/server/user" - -export const Route = createFileRoute("/_authed/post")({ - component: RouteComponent -}) - -function RouteComponent() { - const navigate = Route.useNavigate() - - const { data } = useQuery({ - queryKey: ["users"], - queryFn: async () => { - return getAllUsers() - } - }) - - console.log(data) - - return ( -
- Hello "/_authed/post"!{" "} -
- {" "} - -
-
- ) -} diff --git a/src/routes/_authed/register.tsx b/src/routes/_authed/register.tsx new file mode 100644 index 0000000..28855a2 --- /dev/null +++ b/src/routes/_authed/register.tsx @@ -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) => { + 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 ( +
+
+ + + +