Compare commits

...

29 Commits

Author SHA1 Message Date
Jrodenas
bababbeabb chore: update dependencies and improve database schema handling
- Updated @supabase/ssr to version 0.8.0 and @supabase/supabase-js to version 2.86.0.
- Upgraded various @tanstack packages to their latest versions for improved functionality.
- Refactored database schema exports in schema.ts for consistency and clarity.
- Modified drizzle integration to utilize createServerOnlyFn for database connection.
- Enhanced server functions for drones and places to use updated validation schemas.
- Changed validation schemas to improve naming conventions and clarity.
- Adjusted TypeScript configuration to disable verbatim module syntax for better compatibility.
2025-11-27 15:50:46 +01:00
f951fe6629 feat: implement CRUD operations for drones with validation and server functions 2025-11-25 19:45:34 +01:00
653a9b2dc5 places - feat: implement user place management with CRUD operations and validation 2025-11-20 19:26:04 +01:00
Jrodenas
66c60829ab feat: refactor user authentication and place management functions 2025-11-20 18:11:46 +01:00
b754ee35cc feat: add places route with user place insertion and fetching functionality 2025-11-13 19:19:54 +01:00
b3213045ce remove unnecesary code 2025-11-13 18:25:47 +01:00
Jrodenas
3371def3b9 chore: update dependencies and devDependencies in package.json
fix: refactor drizzle integration to use optional chaining for DATABASE_URL

fix: update Supabase integration to use getCookies instead of parseCookies

feat: implement user profile creation with validation schema

refactor: remove unused user-related functions and adjust signup logic

fix: update user validation schema to include first and last name

chore: enhance route tree with SSR support for router

refactor: rename createRouter to getRouter for clarity

fix: update RootDocument styles for background gradient

refactor: clean up _authed routes by removing unused queries

fix: adjust register form layout for better readability

chore: simplify Vite configuration by removing unnecessary options
2025-11-12 20:09:59 +01:00
1a049d1193 feat: restructure database schema and remove obsolete migrations 2025-11-10 19:25:17 +01:00
99401308d0 Merge branch 'feature/drizzle-schemas' into develop 2025-11-10 18:39:44 +01:00
06f99c0eef feat: add environment variables, update dashboard layout, and enhance routing components 2025-11-08 19:40:44 +01:00
e5c6278c90 fyp - feat: text 2025-08-30 22:45:08 +02:00
juan
e1c0866cdf feat: add dashboard route and update user registration and login flows 2025-08-15 18:54:14 +02:00
juan
974337af2b feat - new changes 2025-08-13 19:29:19 +02:00
96ebac5547 feat - new schemas 2025-08-13 16:31:22 +02:00
970c8b33db feat - New profile table connected to the authenticated user base 2025-08-12 18:26:36 +02:00
juan
c0764fcc84 Merge branch 'feature/add_drizzle_orm' into develop 2025-08-11 20:00:04 +02:00
juan
c8fcdd7f29 Merge branch 'develop' into feature/add_drizzle_orm 2025-08-11 19:59:44 +02:00
juan
6326d0bc06 feat: integrate drizzle ORM with PostgreSQL and set up user management
- Added drizzle configuration for PostgreSQL connection.
- Created user schema for the 'demo' table in drizzle.
- Implemented database connection and user retrieval in drizzle integration.
- Added migration SQL for creating the 'demo' table.
- Updated user hooks to use server functions for login and signup.
- Refactored user-related functions to use drizzle ORM for database interactions.
- Updated routes to utilize new user management functions.
2025-08-11 19:59:06 +02:00
Jrodenas
8489d37979 feat: add @tanstack/react-router-ssr-query dependency and integrate SSR query setup in router 2025-08-11 19:28:42 +02:00
juan
7bc5fcb249 Merge branch 'feature/supabase_auth' into develop 2025-08-11 14:47:25 +02:00
juan
d4d384ba2b fix: update validation logic in useValidation hook and improve error handling in useLogin hook 2025-08-11 14:06:13 +02:00
Jrodenas
a2ae7d5b5a feat: refactor authentication flow, implement user hooks, and add validation schemas for login/signup 2025-08-10 20:16:27 +02:00
juan
e45772e2a9 feat: add validation schemas for login and signup, integrate validation in respective components 2025-08-10 18:30:07 +02:00
juan
51c7b9f86d feat: refactor authentication routes and add toast notifications for login/signup/logout actions 2025-08-10 16:37:53 +02:00
Jrodenas
83b86ab0f0 feat: implement Sonner for toast notifications 2025-08-07 20:12:35 +02:00
juan
5d178709ef fix: update section header from 'Inicio' to 'Supabase' in README 2025-08-07 18:24:46 +02:00
juan
775133281e feat: integrate Supabase for authentication and add routing for login, signup, and logout 2025-08-07 18:23:45 +02:00
juan
d68a0113b2 add readme 2025-08-06 20:00:27 +02:00
juan
05a83e3bcb update gitignore 2025-08-06 19:48:57 +02:00
40 changed files with 4601 additions and 8739 deletions

5
.env Normal file
View 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/postgres"
APIKEY_MAPS="AIzaSyAwfOShBqkBcS46WqmlsIVWQJ8gpdOPk_4"
SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFzc3Npa3pnd29tdWRrd2ZtZ2FkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQzMjY1NTQsImV4cCI6MjA2OTkwMjU1NH0.BTSscdTcPP1GVmMB-H5caLpWsfuAw1V6mXiqogF8TjU"

9
.gitignore vendored
View File

@ -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
View 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": "always"
}
}

View File

@ -1,18 +1,19 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json", "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": { "vcs": {
"enabled": false, "enabled": false,
"clientKind": "git", "clientKind": "git",
"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": {
@ -26,6 +27,11 @@
"recommended": true "recommended": true
} }
}, },
"css": {
"parser": {
"tailwindDirectives": true
}
},
"javascript": { "javascript": {
"formatter": { "formatter": {
"quoteStyle": "double", "quoteStyle": "double",

10
drizzle.config.ts Normal file
View 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!
},
});

11607
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,29 +12,39 @@
"check": "biome check" "check": "biome check"
}, },
"dependencies": { "dependencies": {
"@heroui/react": "^2.8.2", "@heroui/react": "^2.8.5",
"@tailwindcss/vite": "^4.1.11", "@supabase/ssr": "^0.8.0",
"@tanstack/react-query": "^5.84.1", "@supabase/supabase-js": "^2.86.0",
"@tanstack/react-query-devtools": "^5.84.1", "@tailwindcss/vite": "^4.1.17",
"@tanstack/react-router": "^1.130.12", "@tanstack/react-query": "^5.90.11",
"@tanstack/react-router-devtools": "^1.130.13", "@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-router-with-query": "^1.130.12", "@tanstack/react-router": "^1.139.7",
"@tanstack/react-start": "^1.130.15", "@tanstack/react-router-devtools": "^1.139.7",
"@tanstack/router-plugin": "^1.130.15", "@tanstack/react-router-with-query": "^1.130.17",
"@tanstack/react-start": "^1.139.8",
"@tanstack/router-plugin": "^1.139.7",
"@vis.gl/react-google-maps": "^1.7.1",
"drizzle-orm": "^0.44.7",
"framer-motion": "^12.23.12", "framer-motion": "^12.23.12",
"react": "^19.1.1", "postgres": "^3.4.7",
"react-dom": "^19.1.1", "react": "^19.2.0",
"tailwindcss": "^4.1.11", "react-dom": "^19.2.0",
"vite-tsconfig-paths": "^5.1.4" "sonner": "^2.0.7",
"zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.1.3", "@biomejs/biome": "^2.3.8",
"@types/react": "^19.1.9", "vite-tsconfig-paths": "^5.1.4",
"@types/react-dom": "^19.1.7", "@tanstack/react-router-ssr-query": "^1.139.7",
"@vitejs/plugin-react": "^4.7.0", "tailwindcss": "^4.1.17",
"jsdom": "^26.1.0", "@types/google.maps": "^3.58.1",
"typescript": "^5.7.2", "@types/react": "^19.2.7",
"vite": "^6.3.5", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"drizzle-kit": "^0.31.7",
"jsdom": "^27.2.0",
"typescript": "^5.9.3",
"vite": "^7.2.4",
"web-vitals": "^5.1.0" "web-vitals": "^5.1.0"
} }
} }

BIN
public/drone.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

BIN
public/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

4
readme.md Normal file
View File

@ -0,0 +1,4 @@
## Supabase
Usuarios de prueba demo12 - juan.penalver@outlook.com
test

View File

@ -0,0 +1,133 @@
import { sql } from "drizzle-orm"
import {
boolean,
decimal,
integer,
pgPolicy,
pgTable,
serial,
text,
uuid,
varchar
} from "drizzle-orm/pg-core"
import { authenticatedRole, authUsers } from "drizzle-orm/supabase"
// === drones ===
export const dronesSchema = pgTable("drones", {
id: serial("id").primaryKey().notNull(),
model: varchar("model", { length: 100 }),
brand: varchar("brand", { length: 100 })
// type: jsonb("type")
}).enableRLS()
// === certs ===
export const certsSchema = pgTable("certs", {
id: serial("id").primaryKey().notNull(),
name: varchar("name", { length: 100 }),
link: text("link")
}).enableRLS()
// === places ===
export const placesSchema = pgTable(
"places",
{
id: serial("id").primaryKey().notNull(),
coord_x: decimal("coord_x"),
coord_y: decimal("coord_y"),
description: varchar("description", { length: 255 }),
name: varchar("name", { length: 100 }),
id_user: uuid("id_user")
.notNull()
.references(() => authUsers.id, { onDelete: "cascade" }),
hidden_place: boolean("hidden_place")
},
(table) => [
pgPolicy("select-own-places", {
for: "select",
to: authenticatedRole,
using: sql`${table.id_user} = auth.uid()`
}),
pgPolicy("insert-own-places", {
for: "insert",
to: authenticatedRole,
withCheck: sql`${table.id_user} = auth.uid()`
}),
pgPolicy("update-own-places", {
for: "update",
to: authenticatedRole,
using: sql`${table.id_user} = auth.uid()`,
withCheck: sql`${table.id_user} = auth.uid()`
})
]
).enableRLS()
// === users_drones ===
export const usersDronesSchema = pgTable(
"users_drones",
{
id: serial("id").primaryKey().notNull(),
id_drone: integer("id_drone").references(() => dronesSchema.id),
id_user: uuid("id_user").references(() => authUsers.id)
},
(table) => [
pgPolicy("select-own-user-drones", {
for: "select",
to: authenticatedRole,
using: sql`${table.id_user} = auth.uid()`
}),
pgPolicy("insert-own-user-drones", {
for: "insert",
to: authenticatedRole,
withCheck: sql`${table.id_user} = auth.uid()`
})
]
).enableRLS()
// === users_certs ===
export const usersCertsSchema = pgTable(
"users_certs",
{
id: serial("id").primaryKey().notNull(),
id_cert: integer("id_cert").references(() => certsSchema.id),
id_user: uuid("id_user").references(() => authUsers.id)
},
(table) => [
pgPolicy("select-own-user-certs", {
for: "select",
to: authenticatedRole,
using: sql`${table.id_user} = auth.uid()`
}),
pgPolicy("insert-own-user-certs", {
for: "insert",
to: authenticatedRole,
withCheck: sql`${table.id_user} = auth.uid()`
})
]
).enableRLS()
// === users_places ===
export const usersPlacesSchema = pgTable(
"users_places",
{
id: serial("id").primaryKey().notNull(),
id_place: integer("id_place").references(() => placesSchema.id),
id_user: uuid("id_user").references(() => authUsers.id)
},
(table) => [
pgPolicy("select-own-user-places", {
for: "select",
to: authenticatedRole,
using: sql`${table.id_user} = auth.uid()`
}),
pgPolicy("insert-own-user-places", {
for: "insert",
to: authenticatedRole,
withCheck: sql`${table.id_user} = auth.uid()`
})
]
).enableRLS()
// === equipment ===
export const equipmentSchema = pgTable("equipment", {
id: serial("id").primaryKey().notNull()
}).enableRLS()

View File

@ -0,0 +1,12 @@
import { createServerOnlyFn } from "@tanstack/react-start"
import { drizzle } from "drizzle-orm/postgres-js"
import postgres from "postgres"
export const db = createServerOnlyFn(() => {
const connectionString = process.env.DATABASE_URL ?? ""
const client = postgres(connectionString, { prepare: false })
// Disable prefetch as it is not supported for "Transaction" pool mode
return drizzle(client)
})

View File

@ -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>
} }

View File

@ -0,0 +1,13 @@
import { Toaster } from "sonner"
export const SonnerProvider = () => {
return (
<Toaster
position="top-right"
duration={3000}
closeButton
richColors
visibleToasts={3}
/>
)
}

View File

@ -0,0 +1,68 @@
CREATE TABLE "certs" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(100),
"link" text
);
--> statement-breakpoint
ALTER TABLE "certs" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "drones" (
"id" serial PRIMARY KEY NOT NULL,
"model" varchar(100),
"brand" varchar(100),
"type" jsonb
);
--> statement-breakpoint
ALTER TABLE "drones" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "equipment" (
"id" serial PRIMARY KEY NOT NULL
);
--> statement-breakpoint
ALTER TABLE "equipment" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "places" (
"id" serial PRIMARY KEY NOT NULL,
"coord_x" numeric,
"coord_y" numeric,
"description" varchar(255),
"name" varchar(100),
"id_user" uuid NOT NULL,
"hidden_place" boolean
);
--> statement-breakpoint
ALTER TABLE "places" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "users_certs" (
"id" serial PRIMARY KEY NOT NULL,
"id_cert" integer,
"id_user" uuid
);
--> statement-breakpoint
ALTER TABLE "users_certs" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "users_drones" (
"id" serial PRIMARY KEY NOT NULL,
"id_drone" integer,
"id_user" uuid
);
--> statement-breakpoint
ALTER TABLE "users_drones" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "users_places" (
"id" serial PRIMARY KEY NOT NULL,
"id_place" integer,
"id_user" uuid
);
--> statement-breakpoint
ALTER TABLE "users_places" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "places" ADD CONSTRAINT "places_id_user_users_id_fk" FOREIGN KEY ("id_user") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users_certs" ADD CONSTRAINT "users_certs_id_cert_certs_id_fk" FOREIGN KEY ("id_cert") REFERENCES "public"."certs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users_certs" ADD CONSTRAINT "users_certs_id_user_users_id_fk" FOREIGN KEY ("id_user") REFERENCES "auth"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users_drones" ADD CONSTRAINT "users_drones_id_drone_drones_id_fk" FOREIGN KEY ("id_drone") REFERENCES "public"."drones"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users_drones" ADD CONSTRAINT "users_drones_id_user_users_id_fk" FOREIGN KEY ("id_user") REFERENCES "auth"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users_places" ADD CONSTRAINT "users_places_id_place_places_id_fk" FOREIGN KEY ("id_place") REFERENCES "public"."places"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users_places" ADD CONSTRAINT "users_places_id_user_users_id_fk" FOREIGN KEY ("id_user") REFERENCES "auth"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE POLICY "select-own-places" ON "places" AS PERMISSIVE FOR SELECT TO "authenticated" USING ("places"."id_user" = auth.uid());--> statement-breakpoint
CREATE POLICY "insert-own-places" ON "places" AS PERMISSIVE FOR INSERT TO "authenticated" WITH CHECK ("places"."id_user" = auth.uid());--> statement-breakpoint
CREATE POLICY "update-own-places" ON "places" AS PERMISSIVE FOR UPDATE TO "authenticated" USING ("places"."id_user" = auth.uid()) WITH CHECK ("places"."id_user" = auth.uid());--> statement-breakpoint
CREATE POLICY "select-own-user-certs" ON "users_certs" AS PERMISSIVE FOR SELECT TO "authenticated" USING ("users_certs"."id_user" = auth.uid());--> statement-breakpoint
CREATE POLICY "insert-own-user-certs" ON "users_certs" AS PERMISSIVE FOR INSERT TO "authenticated" WITH CHECK ("users_certs"."id_user" = auth.uid());--> statement-breakpoint
CREATE POLICY "select-own-user-drones" ON "users_drones" AS PERMISSIVE FOR SELECT TO "authenticated" USING ("users_drones"."id_user" = auth.uid());--> statement-breakpoint
CREATE POLICY "insert-own-user-drones" ON "users_drones" AS PERMISSIVE FOR INSERT TO "authenticated" WITH CHECK ("users_drones"."id_user" = auth.uid());--> statement-breakpoint
CREATE POLICY "select-own-user-places" ON "users_places" AS PERMISSIVE FOR SELECT TO "authenticated" USING ("users_places"."id_user" = auth.uid());--> statement-breakpoint
CREATE POLICY "insert-own-user-places" ON "users_places" AS PERMISSIVE FOR INSERT TO "authenticated" WITH CHECK ("users_places"."id_user" = auth.uid());

View File

@ -0,0 +1,439 @@
{
"id": "595b3231-9696-413e-b55b-5ec61cb86165",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.certs": {
"name": "certs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"link": {
"name": "link",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.drones": {
"name": "drones",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"model": {
"name": "model",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"brand": {
"name": "brand",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.equipment": {
"name": "equipment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.places": {
"name": "places",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"coord_x": {
"name": "coord_x",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"coord_y": {
"name": "coord_y",
"type": "numeric",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"id_user": {
"name": "id_user",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"hidden_place": {
"name": "hidden_place",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"places_id_user_users_id_fk": {
"name": "places_id_user_users_id_fk",
"tableFrom": "places",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id_user"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {
"select-own-places": {
"name": "select-own-places",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"authenticated"
],
"using": "\"places\".\"id_user\" = auth.uid()"
},
"insert-own-places": {
"name": "insert-own-places",
"as": "PERMISSIVE",
"for": "INSERT",
"to": [
"authenticated"
],
"withCheck": "\"places\".\"id_user\" = auth.uid()"
},
"update-own-places": {
"name": "update-own-places",
"as": "PERMISSIVE",
"for": "UPDATE",
"to": [
"authenticated"
],
"using": "\"places\".\"id_user\" = auth.uid()",
"withCheck": "\"places\".\"id_user\" = auth.uid()"
}
},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.users_certs": {
"name": "users_certs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"id_cert": {
"name": "id_cert",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_user": {
"name": "id_user",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"users_certs_id_cert_certs_id_fk": {
"name": "users_certs_id_cert_certs_id_fk",
"tableFrom": "users_certs",
"tableTo": "certs",
"columnsFrom": [
"id_cert"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"users_certs_id_user_users_id_fk": {
"name": "users_certs_id_user_users_id_fk",
"tableFrom": "users_certs",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {
"select-own-user-certs": {
"name": "select-own-user-certs",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"authenticated"
],
"using": "\"users_certs\".\"id_user\" = auth.uid()"
},
"insert-own-user-certs": {
"name": "insert-own-user-certs",
"as": "PERMISSIVE",
"for": "INSERT",
"to": [
"authenticated"
],
"withCheck": "\"users_certs\".\"id_user\" = auth.uid()"
}
},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.users_drones": {
"name": "users_drones",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"id_drone": {
"name": "id_drone",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_user": {
"name": "id_user",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"users_drones_id_drone_drones_id_fk": {
"name": "users_drones_id_drone_drones_id_fk",
"tableFrom": "users_drones",
"tableTo": "drones",
"columnsFrom": [
"id_drone"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"users_drones_id_user_users_id_fk": {
"name": "users_drones_id_user_users_id_fk",
"tableFrom": "users_drones",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {
"select-own-user-drones": {
"name": "select-own-user-drones",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"authenticated"
],
"using": "\"users_drones\".\"id_user\" = auth.uid()"
},
"insert-own-user-drones": {
"name": "insert-own-user-drones",
"as": "PERMISSIVE",
"for": "INSERT",
"to": [
"authenticated"
],
"withCheck": "\"users_drones\".\"id_user\" = auth.uid()"
}
},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.users_places": {
"name": "users_places",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"id_place": {
"name": "id_place",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"id_user": {
"name": "id_user",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"users_places_id_place_places_id_fk": {
"name": "users_places_id_place_places_id_fk",
"tableFrom": "users_places",
"tableTo": "places",
"columnsFrom": [
"id_place"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"users_places_id_user_users_id_fk": {
"name": "users_places_id_user_users_id_fk",
"tableFrom": "users_places",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id_user"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {
"select-own-user-places": {
"name": "select-own-user-places",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"authenticated"
],
"using": "\"users_places\".\"id_user\" = auth.uid()"
},
"insert-own-user-places": {
"name": "insert-own-user-places",
"as": "PERMISSIVE",
"for": "INSERT",
"to": [
"authenticated"
],
"withCheck": "\"users_places\".\"id_user\" = auth.uid()"
}
},
"checkConstraints": {},
"isRLSEnabled": true
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 2,
"version": "7",
"when": 1762798763195,
"tag": "0002_init",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,24 @@
import { createServerClient } from "@supabase/ssr"
import { getCookies, 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(getCookies()).map(([name, value]) => ({
name,
value
}))
},
setAll(cookies) {
cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value)
})
}
}
}
)
}

View 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 }
}

View File

@ -0,0 +1,56 @@
import { useMutation } from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router"
import { toast } from "sonner"
import type z from "zod"
import { user } 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 user.login({ 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
}
}

View 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 { user } 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) => user.signup({ 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
}
}

69
src/lib/server/drones.ts Normal file
View File

@ -0,0 +1,69 @@
import { createServerFn } from "@tanstack/react-start"
import { eq } from "drizzle-orm"
import { db } from "@/integrations/drizzle"
import { dronesSchema } from "@/integrations/drizzle/db/schema"
import {
droneValidationSchema,
paginatedDronesValidationSchema
} from "../validation/drones"
const insertDrones = createServerFn()
.inputValidator(droneValidationSchema)
.handler(async ({ data }) => {
await db()
.insert(dronesSchema)
.values({
model: data.model,
brand: data.brand
})
.returning()
})
const editDrone = createServerFn()
.inputValidator(
droneValidationSchema.pick({
model: true,
brand: true,
id: true
})
)
.handler(async ({ data }) => {
await db()
.update(dronesSchema)
.set({
model: data.model,
brand: data.brand
})
.where(eq(dronesSchema.id, data.id))
})
const deleteDrones = createServerFn({
method: "POST"
})
.inputValidator(
droneValidationSchema.pick({
id: true
})
)
.handler(async ({ data }) => {
return await db().delete(dronesSchema).where(eq(dronesSchema.id, data.id))
})
const getAllDrones = createServerFn({
method: "POST"
})
.inputValidator(paginatedDronesValidationSchema)
.handler(async ({ data }) => {
return db()
.select()
.from(dronesSchema)
.limit(data.limit)
.offset((data.page - 1) * data.limit)
})
export const serverDrones = {
insertDrones,
editDrone,
getAllDrones,
deleteDrones
}

47
src/lib/server/places.ts Normal file
View File

@ -0,0 +1,47 @@
import { createServerFn } from "@tanstack/react-start"
import { eq } from "drizzle-orm"
import { db } from "@/integrations/drizzle"
import { placesSchema } from "@/integrations/drizzle/db/schema"
import {
paginatedPlacesValidationSchema,
placeValidationSchema
} from "../validation/places"
export const insertUserPlace = createServerFn()
.inputValidator(placeValidationSchema)
.handler(async ({ data }) => {
await db().insert(placesSchema).values(data).returning()
})
export const editUserPlace = createServerFn()
.inputValidator(
placeValidationSchema.pick({
hidden_place: true,
name: true,
description: true,
id_user: true
})
)
.handler(async ({ data }) => {
await db()
.update(placesSchema)
.set(data)
.where(eq(placesSchema.id_user, data.id_user))
})
export const getUserPlacesById = createServerFn()
.inputValidator(paginatedPlacesValidationSchema)
.handler(async ({ data }) => {
return await db()
.select()
.from(placesSchema)
.where(eq(placesSchema.id_user, data.id_user))
.limit(data.limit)
.offset((data.page - 1) * data.limit)
})
export const places = {
insertUserPlace,
editUserPlace,
getUserPlacesById
}

142
src/lib/server/user.ts Normal file
View File

@ -0,0 +1,142 @@
import { redirect } from "@tanstack/react-router"
import { createServerFn } from "@tanstack/react-start"
import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
import {
loginFormSchema,
signupFormSchema,
userListParamsSchema
} from "../validation/user"
const login = createServerFn({ method: "POST" })
.inputValidator(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
}
})
const logout = 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
})
})
const signup = createServerFn({ method: "POST" })
.inputValidator(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 || "/"
})
})
const userData = 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"
}
}
return {
user: {
id: data.user.id,
email: data.user.email,
name: data.user.user_metadata.name || "",
location: data.user.user_metadata.location || ""
},
error: false
}
})
const resendConfirmationEmail = createServerFn({ method: "POST" })
.inputValidator(signupFormSchema.pick({ email: true }))
.handler(async ({ data }) => {
const supabase = getSupabaseServerClient()
const { error } = await supabase.auth.resetPasswordForEmail(data.email)
if (error) {
return {
error: true,
message: error.message
}
}
return {
error: false
}
})
const userList = createServerFn()
.inputValidator(userListParamsSchema)
.handler(async ({ data }) => {
const supabase = getSupabaseServerClient()
const users = await supabase.auth.admin.listUsers({
page: data.page,
perPage: data.limit
})
if (users.error) {
return {
error: true,
message: users.error.message
}
}
return {
users: users.data,
error: false
}
})
export const user = {
login,
logout,
signup,
userData,
resendConfirmationEmail,
userList
}

View File

@ -0,0 +1,12 @@
import * as z from "zod"
export const droneValidationSchema = z.object({
model: z.string(),
brand: z.string(),
id: z.number()
})
export const paginatedDronesValidationSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(10)
})

View File

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

View File

@ -0,0 +1,25 @@
import * as 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(),
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required")
})
export const userListParamsSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(10)
})

View File

@ -9,38 +9,137 @@
// 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 PlacesRouteImport } from './routes/places'
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 AuthedDashboardRouteImport } from './routes/_authed/dashboard'
const SignupRoute = SignupRouteImport.update({
id: '/signup',
path: '/signup',
getParentRoute: () => rootRouteImport,
} as any)
const PlacesRoute = PlacesRouteImport.update({
id: '/places',
path: '/places',
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 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
'/places': typeof PlacesRoute
'/signup': typeof SignupRoute
'/dashboard': typeof AuthedDashboardRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute
'/logout': typeof LogoutRoute
'/places': typeof PlacesRoute
'/signup': typeof SignupRoute
'/dashboard': typeof AuthedDashboardRoute
} }
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
'/places': typeof PlacesRoute
'/signup': typeof SignupRoute
'/_authed/dashboard': typeof AuthedDashboardRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' fullPaths: '/' | '/login' | '/logout' | '/places' | '/signup' | '/dashboard'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' to: '/' | '/login' | '/logout' | '/places' | '/signup' | '/dashboard'
id: '__root__' | '/' id:
| '__root__'
| '/'
| '/_authed'
| '/login'
| '/logout'
| '/places'
| '/signup'
| '/_authed/dashboard'
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
PlacesRoute: typeof PlacesRoute
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
}
'/places': {
id: '/places'
path: '/places'
fullPath: '/places'
preLoaderRoute: typeof PlacesRouteImport
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,12 +147,44 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_authed/dashboard': {
id: '/_authed/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof AuthedDashboardRouteImport
parentRoute: typeof AuthedRoute
}
} }
} }
interface AuthedRouteChildren {
AuthedDashboardRoute: typeof AuthedDashboardRoute
}
const AuthedRouteChildren: AuthedRouteChildren = {
AuthedDashboardRoute: AuthedDashboardRoute,
}
const AuthedRouteWithChildren =
AuthedRoute._addFileChildren(AuthedRouteChildren)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AuthedRoute: AuthedRouteWithChildren,
LoginRoute: LoginRoute,
LogoutRoute: LogoutRoute,
PlacesRoute: PlacesRoute,
SignupRoute: SignupRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>() ._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

View File

@ -1,27 +1,35 @@
import { createRouter as createTanstackRouter } from "@tanstack/react-router" import { createRouter } 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 getRouter = () => {
const rqContext = createQueryContext() const { queryClient } = createQueryContext()
return routerWithQueryClient( const router = createRouter({
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 {
router: ReturnType<typeof createRouter> router: ReturnType<typeof getRouter>
} }
} }

View File

@ -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 { user } 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 userData = await user.userData()
return {
...userData
}
},
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-linear-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>

58
src/routes/_authed.tsx Normal file
View File

@ -0,0 +1,58 @@
import {
Avatar,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger
} from "@heroui/react"
import { createFileRoute, Outlet } from "@tanstack/react-router"
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
if (context.error) {
throw new Error("Not authenticated")
// TODO: Redirect to login page
}
},
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">
<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>
)
}

View File

@ -0,0 +1,11 @@
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/_authed/dashboard")({
component: RouteComponent
})
function RouteComponent() {
const { user } = Route.useRouteContext()
console.log(user)
return <div>Dashboard</div>
}

View File

@ -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
View 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>
)
}

9
src/routes/logout.tsx Normal file
View File

@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router"
import { user } from "@/lib/server/user"
export const Route = createFileRoute("/logout")({
beforeLoad: async () => {
await user.logout()
},
preload: false
})

35
src/routes/places.tsx Normal file
View File

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

44
src/routes/signup.tsx Normal file
View 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>
)
}

View File

@ -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;
} }

View File

@ -10,7 +10,7 @@
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": false,
"noEmit": true, "noEmit": true,
/* Linting */ /* Linting */

View File

@ -10,9 +10,7 @@ const config = defineConfig({
projects: ['./tsconfig.json'], projects: ['./tsconfig.json'],
}), }),
tailwindcss(), tailwindcss(),
tanstackStart({ tanstackStart(),
customViteReactPlugin: true,
}),
viteReact(), viteReact(),
], ],
}) })