Compare commits
10 Commits
9d1f35c629
...
login
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf8c592c78 | ||
|
|
08d0a5a099 | ||
|
|
45da7f62fe | ||
|
|
63506d620f | ||
|
|
0b50638583 | ||
| cec96b94da | |||
| 4d4cae2d8b | |||
| bd8bb70b88 | |||
| c04f805936 | |||
| 289941b7d7 |
9
.env.development
Normal file
9
.env.development
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# SUPABASE_URL="https://qsssikzgwomudkwfmgad.supabase.co"
|
||||||
|
# DATABASE_URL="postgresql://postgres.qsssikzgwomudkwfmgad:etrTXNz3ZOwaLJmT@aws-0-eu-north-1.pooler.supabase.com:6543/postgres"
|
||||||
|
# SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFzc3Npa3pnd29tdWRrd2ZtZ2FkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQzMjY1NTQsImV4cCI6MjA2OTkwMjU1NH0.BTSscdTcPP1GVmMB-H5caLpWsfuAw1V6mXiqogF8TjU"
|
||||||
|
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_URL="https://qsssikzgwomudkwfmgad.supabase.co"
|
||||||
|
SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFzc3Npa3pnd29tdWRrd2ZtZ2FkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQzMjY1NTQsImV4cCI6MjA2OTkwMjU1NH0.BTSscdTcPP1GVmMB-H5caLpWsfuAw1V6mXiqogF8TjU"
|
||||||
|
VITE_LOGIN_USER="test@test.com"
|
||||||
|
VITE_PASSWORD_USER="test"
|
||||||
25
.vscode/tasks.json
vendored
Normal file
25
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"label": "pnpm: dev",
|
||||||
|
"command": "pnpm dev",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": {
|
||||||
|
"kind": "test",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"label": "pnpm: build",
|
||||||
|
"command": "pnpm build",
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
AGENTS.md
Normal file
41
AGENTS.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# AGENTS.md — findyourpilot
|
||||||
|
|
||||||
|
Project context for AI coding agents. This file defines which skill files to load
|
||||||
|
depending on the task at hand.
|
||||||
|
|
||||||
|
<!-- intent-skills:start -->
|
||||||
|
# Skill mappings - when working in these areas, load the linked skill file into context.
|
||||||
|
skills:
|
||||||
|
- task: "Building or modifying routes, loaders, search params, or navigation with TanStack Router"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+router-core@1.167.5/node_modules/@tanstack/router-core/skills/router-core/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Working with route search params, type-safe URL state, or Zod-validated search params"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+router-core@1.167.5/node_modules/@tanstack/router-core/skills/router-core/search-params/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Implementing route guards, authentication redirects, or protected routes"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+router-core@1.167.5/node_modules/@tanstack/router-core/skills/router-core/auth-and-guards/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Loading data in routes, using loaders, or integrating with TanStack Query"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+router-core@1.167.5/node_modules/@tanstack/router-core/skills/router-core/data-loading/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Creating server functions, SSR patterns, full-stack logic, or the TanStack Start execution model"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+start-client-core@1.166.13/node_modules/@tanstack/start-client-core/skills/start-core/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Creating or modifying createServerFn, input validation on server functions, or handling server errors"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+start-client-core@1.166.13/node_modules/@tanstack/start-client-core/skills/start-core/server-functions/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Creating middleware, request middleware, or passing context between server functions"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+start-client-core@1.166.13/node_modules/@tanstack/start-client-core/skills/start-core/middleware/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Creating API endpoints or server-only routes (server property on createFileRoute)"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+start-client-core@1.166.13/node_modules/@tanstack/start-client-core/skills/start-core/server-routes/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Deploying the app to Cloudflare, Vercel, Netlify, Node.js/Docker, or configuring SSR/SPA/prerendering"
|
||||||
|
load: "node_modules/.pnpm/@tanstack+start-client-core@1.166.13/node_modules/@tanstack/start-client-core/skills/start-core/deployment/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Writing, reviewing, or optimising Supabase/Postgres queries, schema design, RLS policies, or migrations"
|
||||||
|
load: ".agents/skills/supabase-postgres-best-practices/SKILL.md"
|
||||||
|
|
||||||
|
- task: "Designing or improving UI layouts, dashboards, component composition, or interactive product interfaces with HeroUI"
|
||||||
|
load: ".agents/skills/interface-design/SKILL.md"
|
||||||
|
<!-- intent-skills:end -->
|
||||||
4702
package-lock.json
generated
4702
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -14,42 +14,45 @@
|
|||||||
"machine-translate": "inlang machine translate --project project.inlang"
|
"machine-translate": "inlang machine translate --project project.inlang"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroui/react": "^3.0.0-beta.8",
|
"@heroui/react": "^3.0.0-rc.1",
|
||||||
"@heroui/styles": "^3.0.0-beta.8",
|
"@heroui/styles": "^3.0.0-rc.1",
|
||||||
"@sentry/tanstackstart-react": "^10.42.0",
|
"@sentry/tanstackstart-react": "^10.45.0",
|
||||||
"@supabase/ssr": "^0.9.0",
|
"@supabase/ssr": "^0.9.0",
|
||||||
"@supabase/supabase-js": "^2.98.0",
|
"@supabase/supabase-js": "^2.99.3",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.91.2",
|
||||||
"@tanstack/react-router": "^1.163.3",
|
"@tanstack/react-router": "^1.167.5",
|
||||||
"@tanstack/react-router-ssr-query": "^1.163.3",
|
"@tanstack/react-router-ssr-query": "^1.166.9",
|
||||||
"@tanstack/react-start": "^1.166.1",
|
"@tanstack/react-start": "^1.166.17",
|
||||||
"@tanstack/router-plugin": "^1.164.0",
|
"@tanstack/router-plugin": "^1.166.14",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
"maplibre-gl": "^5.20.2",
|
||||||
"nitro": "^3.0.1-alpha.2",
|
"nitro": "^3.0.1-alpha.2",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tailwindcss": "^4.2.2",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.5",
|
"@biomejs/biome": "^2.4.8",
|
||||||
"@inlang/paraglide-js": "^2.13.1",
|
"@inlang/paraglide-js": "^2.15.0",
|
||||||
"@tanstack/devtools-vite": "^0.5.3",
|
"@tanstack/devtools-vite": "^0.6.0",
|
||||||
"@tanstack/react-devtools": "^0.9.9",
|
"@tanstack/react-devtools": "^0.10.0",
|
||||||
"@tanstack/react-router-devtools": "^1.163.3",
|
|
||||||
"@tanstack/react-query-devtools": "^5.91.3",
|
"@tanstack/react-query-devtools": "^5.91.3",
|
||||||
|
"@tanstack/react-router-devtools": "^1.166.9",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.19.15",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^29.0.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.1",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^4.1.0"
|
||||||
"@inlang/cli": "^3.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4764
pnpm-lock.yaml
generated
4764
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1476
src/components/maps/map.tsx
Normal file
1476
src/components/maps/map.tsx
Normal file
File diff suppressed because it is too large
Load Diff
28
src/integrations/supabase/supabase.ts
Normal file
28
src/integrations/supabase/supabase.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { createServerClient } from "@supabase/ssr"
|
||||||
|
import { createServerOnlyFn } from "@tanstack/react-start"
|
||||||
|
import { getCookies, setCookie } from "@tanstack/react-start/server"
|
||||||
|
|
||||||
|
const supabase_url = createServerOnlyFn(
|
||||||
|
() => process.env.SUPABASE_URL as string
|
||||||
|
)
|
||||||
|
const supabase_key = createServerOnlyFn(
|
||||||
|
() => process.env.SUPABASE_KEY as string
|
||||||
|
)
|
||||||
|
|
||||||
|
export function getSupabaseServerClient() {
|
||||||
|
return createServerClient(supabase_url(), supabase_key(), {
|
||||||
|
cookies: {
|
||||||
|
getAll() {
|
||||||
|
return Object.entries(getCookies()).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
setAll(cookies) {
|
||||||
|
cookies.forEach((cookie) => {
|
||||||
|
setCookie(cookie.name, cookie.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
47
src/lib/hooks/useLogin.tsx
Normal file
47
src/lib/hooks/useLogin.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { toast } from "@heroui/react"
|
||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
import { useNavigate } from "@tanstack/react-router"
|
||||||
|
import type z from "zod"
|
||||||
|
import { user } from "@/lib/server/user"
|
||||||
|
import { loginFormSchema } from "../validation/user"
|
||||||
|
|
||||||
|
type TLoginForm = z.infer<typeof loginFormSchema>
|
||||||
|
|
||||||
|
export const useLogin = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const loginMutation = useMutation({
|
||||||
|
mutationKey: ["login"],
|
||||||
|
mutationFn: async (data: TLoginForm) => {
|
||||||
|
const response = await user.login({ data })
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate({
|
||||||
|
to: "/dashboard"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateLogin = (formData: TLoginForm) => {
|
||||||
|
if (!loginFormSchema.safeParse(formData).success) {
|
||||||
|
toast.danger("Error en el formulario.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const promise = loginMutation.mutateAsync(formData)
|
||||||
|
|
||||||
|
toast.promise(promise, {
|
||||||
|
loading: "Logging in...",
|
||||||
|
success: "Login successful! Redirecting to posts..",
|
||||||
|
error: (error) => error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
login: validateLogin,
|
||||||
|
isPending: loginMutation.isPending
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/lib/hooks/useSignup.tsx
Normal file
44
src/lib/hooks/useSignup.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { toast } from "@heroui/react"
|
||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
import type z from "zod"
|
||||||
|
import { user } from "@/lib/server/user"
|
||||||
|
import type { signupClientFormSchema } from "@/lib/validation/user"
|
||||||
|
|
||||||
|
type TSignupClientForm = z.infer<typeof signupClientFormSchema>
|
||||||
|
|
||||||
|
export const useSignup = () => {
|
||||||
|
const signupMutation = useMutation({
|
||||||
|
mutationKey: ["signup"],
|
||||||
|
mutationFn: async (data: TSignupClientForm) => {
|
||||||
|
const { confirmPassword: _, ...serverData } = data
|
||||||
|
const response = await user.signup({ data: serverData })
|
||||||
|
|
||||||
|
if (response && "error" in response && response.error) {
|
||||||
|
throw new Error(
|
||||||
|
"message" in response
|
||||||
|
? (response.message as string)
|
||||||
|
: "Error desconocido"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateSignup = (formData: TSignupClientForm) => {
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
toast.danger("Las contraseñas no coinciden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = signupMutation.mutateAsync(formData)
|
||||||
|
toast.promise(promise, {
|
||||||
|
loading: "Creando tu cuenta...",
|
||||||
|
success: "¡Cuenta creada! Revisa tu correo para confirmarla.",
|
||||||
|
error: (error: Error) => error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
signup: validateSignup,
|
||||||
|
isPending: signupMutation.isPending
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/lib/server/user.ts
Normal file
142
src/lib/server/user.ts
Normal 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,
|
||||||
|
}
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
35
src/lib/validation/user.ts
Normal file
35
src/lib/validation/user.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import * as z from "zod"
|
||||||
|
|
||||||
|
export const loginFormSchema = z.object({
|
||||||
|
email: z.email("Introduce un correo válido"),
|
||||||
|
password: z.string().min(1, "La contraseña es obligatoria")
|
||||||
|
})
|
||||||
|
|
||||||
|
export const signupFormSchema = z.object({
|
||||||
|
email: z.email("Introduce un correo válido"),
|
||||||
|
password: z.string().min(6, "La contraseña debe tener al menos 6 caracteres"),
|
||||||
|
name: z.string().min(1, "El nombre es obligatorio"),
|
||||||
|
location: z.string().min(1, "La ubicación es obligatoria"),
|
||||||
|
redirectUrl: z.string().optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Schema extendido para el formulario cliente (incluye confirmación de contraseña)
|
||||||
|
export const signupClientFormSchema = signupFormSchema
|
||||||
|
.extend({
|
||||||
|
confirmPassword: z.string().min(6, "Confirma tu contraseña")
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: "Las contraseñas no coinciden",
|
||||||
|
path: ["confirmPassword"]
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
@@ -9,11 +9,37 @@
|
|||||||
// 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 LogoutRouteImport } from './routes/logout'
|
||||||
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
|
import { Route as AccessRouteImport } from './routes/access'
|
||||||
|
import { Route as AuthRouteImport } from './routes/_auth'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
||||||
import { Route as DemoI18nRouteImport } from './routes/demo.i18n'
|
import { Route as DemoI18nRouteImport } from './routes/demo.i18n'
|
||||||
|
import { Route as AccessRegisterRouteImport } from './routes/access.register'
|
||||||
|
import { Route as AccessLoginRouteImport } from './routes/access.login'
|
||||||
|
import { Route as AuthDashboardRouteImport } from './routes/_auth/dashboard'
|
||||||
import { Route as DemoSentryTestingRouteImport } from './routes/demo/sentry.testing'
|
import { Route as DemoSentryTestingRouteImport } from './routes/demo/sentry.testing'
|
||||||
|
|
||||||
|
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 AccessRoute = AccessRouteImport.update({
|
||||||
|
id: '/access',
|
||||||
|
path: '/access',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AuthRoute = AuthRouteImport.update({
|
||||||
|
id: '/_auth',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -29,6 +55,21 @@ const DemoI18nRoute = DemoI18nRouteImport.update({
|
|||||||
path: '/demo/i18n',
|
path: '/demo/i18n',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AccessRegisterRoute = AccessRegisterRouteImport.update({
|
||||||
|
id: '/register',
|
||||||
|
path: '/register',
|
||||||
|
getParentRoute: () => AccessRoute,
|
||||||
|
} as any)
|
||||||
|
const AccessLoginRoute = AccessLoginRouteImport.update({
|
||||||
|
id: '/login',
|
||||||
|
path: '/login',
|
||||||
|
getParentRoute: () => AccessRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthDashboardRoute = AuthDashboardRouteImport.update({
|
||||||
|
id: '/dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
getParentRoute: () => AuthRoute,
|
||||||
|
} as any)
|
||||||
const DemoSentryTestingRoute = DemoSentryTestingRouteImport.update({
|
const DemoSentryTestingRoute = DemoSentryTestingRouteImport.update({
|
||||||
id: '/demo/sentry/testing',
|
id: '/demo/sentry/testing',
|
||||||
path: '/demo/sentry/testing',
|
path: '/demo/sentry/testing',
|
||||||
@@ -37,12 +78,24 @@ const DemoSentryTestingRoute = DemoSentryTestingRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/access': typeof AccessRouteWithChildren
|
||||||
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
|
'/dashboard': typeof AuthDashboardRoute
|
||||||
|
'/access/login': typeof AccessLoginRoute
|
||||||
|
'/access/register': typeof AccessRegisterRoute
|
||||||
'/demo/i18n': typeof DemoI18nRoute
|
'/demo/i18n': typeof DemoI18nRoute
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/access': typeof AccessRouteWithChildren
|
||||||
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
|
'/dashboard': typeof AuthDashboardRoute
|
||||||
|
'/access/login': typeof AccessLoginRoute
|
||||||
|
'/access/register': typeof AccessRegisterRoute
|
||||||
'/demo/i18n': typeof DemoI18nRoute
|
'/demo/i18n': typeof DemoI18nRoute
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||||
@@ -50,6 +103,13 @@ export interface FileRoutesByTo {
|
|||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/_auth': typeof AuthRouteWithChildren
|
||||||
|
'/access': typeof AccessRouteWithChildren
|
||||||
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
|
'/_auth/dashboard': typeof AuthDashboardRoute
|
||||||
|
'/access/login': typeof AccessLoginRoute
|
||||||
|
'/access/register': typeof AccessRegisterRoute
|
||||||
'/demo/i18n': typeof DemoI18nRoute
|
'/demo/i18n': typeof DemoI18nRoute
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
'/demo/sentry/testing': typeof DemoSentryTestingRoute
|
||||||
@@ -58,14 +118,37 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/access'
|
||||||
|
| '/login'
|
||||||
|
| '/logout'
|
||||||
|
| '/dashboard'
|
||||||
|
| '/access/login'
|
||||||
|
| '/access/register'
|
||||||
| '/demo/i18n'
|
| '/demo/i18n'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/demo/sentry/testing'
|
| '/demo/sentry/testing'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/demo/i18n' | '/demo/tanstack-query' | '/demo/sentry/testing'
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/access'
|
||||||
|
| '/login'
|
||||||
|
| '/logout'
|
||||||
|
| '/dashboard'
|
||||||
|
| '/access/login'
|
||||||
|
| '/access/register'
|
||||||
|
| '/demo/i18n'
|
||||||
|
| '/demo/tanstack-query'
|
||||||
|
| '/demo/sentry/testing'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/_auth'
|
||||||
|
| '/access'
|
||||||
|
| '/login'
|
||||||
|
| '/logout'
|
||||||
|
| '/_auth/dashboard'
|
||||||
|
| '/access/login'
|
||||||
|
| '/access/register'
|
||||||
| '/demo/i18n'
|
| '/demo/i18n'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/demo/sentry/testing'
|
| '/demo/sentry/testing'
|
||||||
@@ -73,6 +156,10 @@ export interface FileRouteTypes {
|
|||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
AuthRoute: typeof AuthRouteWithChildren
|
||||||
|
AccessRoute: typeof AccessRouteWithChildren
|
||||||
|
LoginRoute: typeof LoginRoute
|
||||||
|
LogoutRoute: typeof LogoutRoute
|
||||||
DemoI18nRoute: typeof DemoI18nRoute
|
DemoI18nRoute: typeof DemoI18nRoute
|
||||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||||
DemoSentryTestingRoute: typeof DemoSentryTestingRoute
|
DemoSentryTestingRoute: typeof DemoSentryTestingRoute
|
||||||
@@ -80,6 +167,34 @@ export interface RootRouteChildren {
|
|||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/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
|
||||||
|
}
|
||||||
|
'/access': {
|
||||||
|
id: '/access'
|
||||||
|
path: '/access'
|
||||||
|
fullPath: '/access'
|
||||||
|
preLoaderRoute: typeof AccessRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/_auth': {
|
||||||
|
id: '/_auth'
|
||||||
|
path: ''
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof AuthRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -101,6 +216,27 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DemoI18nRouteImport
|
preLoaderRoute: typeof DemoI18nRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/access/register': {
|
||||||
|
id: '/access/register'
|
||||||
|
path: '/register'
|
||||||
|
fullPath: '/access/register'
|
||||||
|
preLoaderRoute: typeof AccessRegisterRouteImport
|
||||||
|
parentRoute: typeof AccessRoute
|
||||||
|
}
|
||||||
|
'/access/login': {
|
||||||
|
id: '/access/login'
|
||||||
|
path: '/login'
|
||||||
|
fullPath: '/access/login'
|
||||||
|
preLoaderRoute: typeof AccessLoginRouteImport
|
||||||
|
parentRoute: typeof AccessRoute
|
||||||
|
}
|
||||||
|
'/_auth/dashboard': {
|
||||||
|
id: '/_auth/dashboard'
|
||||||
|
path: '/dashboard'
|
||||||
|
fullPath: '/dashboard'
|
||||||
|
preLoaderRoute: typeof AuthDashboardRouteImport
|
||||||
|
parentRoute: typeof AuthRoute
|
||||||
|
}
|
||||||
'/demo/sentry/testing': {
|
'/demo/sentry/testing': {
|
||||||
id: '/demo/sentry/testing'
|
id: '/demo/sentry/testing'
|
||||||
path: '/demo/sentry/testing'
|
path: '/demo/sentry/testing'
|
||||||
@@ -111,8 +247,35 @@ declare module '@tanstack/react-router' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthRouteChildren {
|
||||||
|
AuthDashboardRoute: typeof AuthDashboardRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthRouteChildren: AuthRouteChildren = {
|
||||||
|
AuthDashboardRoute: AuthDashboardRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||||
|
|
||||||
|
interface AccessRouteChildren {
|
||||||
|
AccessLoginRoute: typeof AccessLoginRoute
|
||||||
|
AccessRegisterRoute: typeof AccessRegisterRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccessRouteChildren: AccessRouteChildren = {
|
||||||
|
AccessLoginRoute: AccessLoginRoute,
|
||||||
|
AccessRegisterRoute: AccessRegisterRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccessRouteWithChildren =
|
||||||
|
AccessRoute._addFileChildren(AccessRouteChildren)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
AuthRoute: AuthRouteWithChildren,
|
||||||
|
AccessRoute: AccessRouteWithChildren,
|
||||||
|
LoginRoute: LoginRoute,
|
||||||
|
LogoutRoute: LogoutRoute,
|
||||||
DemoI18nRoute: DemoI18nRoute,
|
DemoI18nRoute: DemoI18nRoute,
|
||||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||||
DemoSentryTestingRoute: DemoSentryTestingRoute,
|
DemoSentryTestingRoute: DemoSentryTestingRoute,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export function getRouter() {
|
|||||||
const router = createTanStackRouter({
|
const router = createTanStackRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
context: {
|
context: {
|
||||||
queryClient
|
queryClient,
|
||||||
|
user: null
|
||||||
},
|
},
|
||||||
scrollRestoration: true,
|
scrollRestoration: true,
|
||||||
defaultPreload: "intent",
|
defaultPreload: "intent",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToastProvider } from "@heroui/react"
|
||||||
import type { QueryClient } from "@tanstack/react-query"
|
import type { QueryClient } from "@tanstack/react-query"
|
||||||
import {
|
import {
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
@@ -6,11 +7,12 @@ import {
|
|||||||
} from "@tanstack/react-router"
|
} from "@tanstack/react-router"
|
||||||
import { Devtools } from "@/integrations/devtools"
|
import { Devtools } from "@/integrations/devtools"
|
||||||
import { getLocale } from "@/integrations/paraglide/runtime"
|
import { getLocale } from "@/integrations/paraglide/runtime"
|
||||||
|
import { user } from "@/lib/server/user"
|
||||||
import appCss from "@/styles/globals.css?url"
|
import appCss from "@/styles/globals.css?url"
|
||||||
import Header from "../components/Header"
|
|
||||||
|
|
||||||
interface MyRouterContext {
|
interface MyRouterContext {
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
|
user: null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||||
@@ -20,8 +22,10 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.documentElement.setAttribute("lang", getLocale())
|
document.documentElement.setAttribute("lang", getLocale())
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
user: await user.userData()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
@@ -52,7 +56,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
<HeadContent />
|
<HeadContent />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Header />
|
<ToastProvider />
|
||||||
{children}
|
{children}
|
||||||
<Devtools />
|
<Devtools />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
|
|||||||
31
src/routes/_auth.tsx
Normal file
31
src/routes/_auth.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { createFileRoute, Link, Outlet } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth")({
|
||||||
|
beforeLoad: ({ context }) => {
|
||||||
|
if (context.user.error) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorComponent: ({ error }) => {
|
||||||
|
if (error.message === "Not authenticated") {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Not authenticated. Please <Link to="/access/login">login</Link>.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
},
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex gap-2">
|
||||||
|
<div className="p-2"></div>
|
||||||
|
<div className="sm:pl-25 w-full">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
79
src/routes/_auth/dashboard.tsx
Normal file
79
src/routes/_auth/dashboard.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Card } from "@heroui/react"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Drone } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import {
|
||||||
|
Map as MapComponent,
|
||||||
|
MapMarker,
|
||||||
|
type MapViewport,
|
||||||
|
MarkerContent,
|
||||||
|
MarkerPopup,
|
||||||
|
MarkerTooltip
|
||||||
|
} from "@/components/maps/map"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth/dashboard")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const locations = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Location 1",
|
||||||
|
lat: 40.76,
|
||||||
|
lng: -73.98
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Location 2",
|
||||||
|
lat: 40.77,
|
||||||
|
lng: -73.99
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Location 3",
|
||||||
|
lat: 40.5874827,
|
||||||
|
lng: -1.7925343
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const [viewport, setViewport] = useState<MapViewport>({
|
||||||
|
center: [40.5874827, -1.7925343],
|
||||||
|
zoom: 8,
|
||||||
|
bearing: 0,
|
||||||
|
pitch: 0
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card className="h-200 p-0 overflow-hidden">
|
||||||
|
<MapComponent
|
||||||
|
center={[40.5874827, -1.7925343]}
|
||||||
|
zoom={10}
|
||||||
|
viewport={viewport}
|
||||||
|
onViewportChange={setViewport}
|
||||||
|
>
|
||||||
|
{locations.map((location) => (
|
||||||
|
<MapMarker
|
||||||
|
key={location.id}
|
||||||
|
longitude={location.lng}
|
||||||
|
latitude={location.lat}
|
||||||
|
>
|
||||||
|
{/* Prueba para ssl */}
|
||||||
|
<MarkerContent>
|
||||||
|
<Drone size={24} color="green" className="text-green-200" />
|
||||||
|
</MarkerContent>
|
||||||
|
<MarkerTooltip>{location.name}</MarkerTooltip>
|
||||||
|
<MarkerPopup>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-medium text-foreground">{location.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{location.lat.toFixed(4)}, {location.lng.toFixed(4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</MarkerPopup>
|
||||||
|
</MapMarker>
|
||||||
|
))}
|
||||||
|
</MapComponent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
133
src/routes/access.login.tsx
Normal file
133
src/routes/access.login.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FieldError,
|
||||||
|
Fieldset,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Spinner,
|
||||||
|
TextField
|
||||||
|
} from "@heroui/react"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { LogIn } from "lucide-react"
|
||||||
|
import { useLogin } from "@/lib/hooks/useLogin"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/access/login")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { login, isPending } = useLogin()
|
||||||
|
|
||||||
|
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const formData = new FormData(e.currentTarget)
|
||||||
|
const email = formData.get("email") as string
|
||||||
|
const password = formData.get("password") as string
|
||||||
|
|
||||||
|
login({ email, password })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form onSubmit={handleFormSubmit} className="flex flex-col gap-4">
|
||||||
|
<Fieldset>
|
||||||
|
<Fieldset.Group>
|
||||||
|
<TextField
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
variant="secondary"
|
||||||
|
className="py-4 text-lg"
|
||||||
|
isRequired
|
||||||
|
defaultValue={import.meta.env.VITE_LOGIN_USER}
|
||||||
|
>
|
||||||
|
<Label isRequired className="ml-4 text-lg">
|
||||||
|
Correo
|
||||||
|
</Label>
|
||||||
|
<Input placeholder="Introduce tu correo" />
|
||||||
|
<FieldError />
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
variant="secondary"
|
||||||
|
className="py-4 text-lg"
|
||||||
|
isRequired
|
||||||
|
defaultValue={import.meta.env.VITE_PASSWORD_USER}
|
||||||
|
>
|
||||||
|
<Label isRequired className="ml-4 text-lg">
|
||||||
|
Contraseña
|
||||||
|
</Label>
|
||||||
|
<Input placeholder="Introduce tu contraseña" />
|
||||||
|
<FieldError />
|
||||||
|
</TextField>
|
||||||
|
</Fieldset.Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="py-6 px-8 w-full text-white text-lg"
|
||||||
|
size="lg"
|
||||||
|
isPending={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner /> : <LogIn size={18} />}
|
||||||
|
Entrar
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
<div className="flex justify-evenly w-full gap-4 mt-2">
|
||||||
|
<Button size="lg" className="w-full" variant="secondary">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<g fill="none" fillRule="evenodd" clipRule="evenodd">
|
||||||
|
<path
|
||||||
|
fill="#f44336"
|
||||||
|
d="M7.209 1.061c.725-.081 1.154-.081 1.933 0a6.57 6.57 0 0 1 3.65 1.82a100 100 0 0 0-1.986 1.93q-1.876-1.59-4.188-.734q-1.696.78-2.362 2.528a78 78 0 0 1-2.148-1.658a.26.26 0 0 0-.16-.027q1.683-3.245 5.26-3.86"
|
||||||
|
opacity="0.987"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#ffc107"
|
||||||
|
d="M1.946 4.92q.085-.013.161.027a78 78 0 0 0 2.148 1.658A7.6 7.6 0 0 0 4.04 7.99q.037.678.215 1.331L2 11.116Q.527 8.038 1.946 4.92"
|
||||||
|
opacity="0.997"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#448aff"
|
||||||
|
d="M12.685 13.29a26 26 0 0 0-2.202-1.74q1.15-.812 1.396-2.228H8.122V6.713q3.25-.027 6.497.055q.616 3.345-1.423 6.032a7 7 0 0 1-.51.49"
|
||||||
|
opacity="0.999"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#43a047"
|
||||||
|
d="M4.255 9.322q1.23 3.057 4.51 2.854a3.94 3.94 0 0 0 1.718-.626q1.148.812 2.202 1.74a6.62 6.62 0 0 1-4.027 1.684a6.4 6.4 0 0 1-1.02 0Q3.82 14.524 2 11.116z"
|
||||||
|
opacity="0.993"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Google
|
||||||
|
</Button>
|
||||||
|
<Button size="lg" className="w-full" variant="secondary">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1877f2"
|
||||||
|
d="M256 128C256 57.308 198.692 0 128 0S0 57.308 0 128c0 63.888 46.808 116.843 108 126.445V165H75.5v-37H108V99.8c0-32.08 19.11-49.8 48.348-49.8C170.352 50 185 52.5 185 52.5V84h-16.14C152.959 84 148 93.867 148 103.99V128h35.5l-5.675 37H148v89.445c61.192-9.602 108-62.556 108-126.445"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="m177.825 165l5.675-37H148v-24.01C148 93.866 152.959 84 168.86 84H185V52.5S170.352 50 156.347 50C127.11 50 108 67.72 108 99.8V128H75.5v37H108v89.445A129 129 0 0 0 128 256a129 129 0 0 0 20-1.555V165z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Facebook
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
172
src/routes/access.register.tsx
Normal file
172
src/routes/access.register.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FieldError,
|
||||||
|
Fieldset,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Spinner,
|
||||||
|
TextField
|
||||||
|
} from "@heroui/react"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { UserPlus } from "lucide-react"
|
||||||
|
import { useSignup } from "@/lib/hooks/useSignup"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/access/register")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { signup, isPending } = useSignup()
|
||||||
|
|
||||||
|
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const formData = new FormData(e.currentTarget)
|
||||||
|
signup({
|
||||||
|
email: formData.get("email") as string,
|
||||||
|
password: formData.get("password") as string,
|
||||||
|
confirmPassword: formData.get("confirmPassword") as string,
|
||||||
|
location: formData.get("location") as string,
|
||||||
|
name: formData.get("name") as string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form onSubmit={handleFormSubmit} className="flex flex-col gap-4">
|
||||||
|
<Fieldset>
|
||||||
|
<Fieldset.Group>
|
||||||
|
<TextField
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
variant="secondary"
|
||||||
|
className="py-2 text-lg"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Label isRequired className="ml-4 text-lg">
|
||||||
|
Nombre completo
|
||||||
|
</Label>
|
||||||
|
<Input placeholder="Tu nombre y apellidos" />
|
||||||
|
<FieldError />
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
variant="secondary"
|
||||||
|
className="py-2 text-lg"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Label isRequired className="ml-4 text-lg">
|
||||||
|
Correo electrónico
|
||||||
|
</Label>
|
||||||
|
<Input placeholder="tu@correo.com" />
|
||||||
|
<FieldError />
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
type="text"
|
||||||
|
name="location"
|
||||||
|
variant="secondary"
|
||||||
|
className="py-2 text-lg"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Label isRequired className="ml-4 text-lg">
|
||||||
|
Ubicación
|
||||||
|
</Label>
|
||||||
|
<Input placeholder="Ciudad, País" />
|
||||||
|
<FieldError />
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
variant="secondary"
|
||||||
|
className="py-2 text-lg"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Label isRequired className="ml-4 text-lg">
|
||||||
|
Contraseña
|
||||||
|
</Label>
|
||||||
|
<Input placeholder="Mínimo 6 caracteres" />
|
||||||
|
<FieldError />
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
variant="secondary"
|
||||||
|
className="py-2 text-lg"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Label isRequired className="ml-4 text-lg">
|
||||||
|
Confirmar contraseña
|
||||||
|
</Label>
|
||||||
|
<Input placeholder="Repite tu contraseña" />
|
||||||
|
<FieldError />
|
||||||
|
</TextField>
|
||||||
|
</Fieldset.Group>
|
||||||
|
</Fieldset>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="py-6 px-8 w-full text-white text-lg"
|
||||||
|
size="lg"
|
||||||
|
isPending={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner /> : <UserPlus size={18} />}
|
||||||
|
Crear cuenta
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
<div className="flex justify-evenly w-full gap-4 mt-2">
|
||||||
|
<Button size="lg" className="w-full" variant="secondary">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<g fill="none" fillRule="evenodd" clipRule="evenodd">
|
||||||
|
<path
|
||||||
|
fill="#f44336"
|
||||||
|
d="M7.209 1.061c.725-.081 1.154-.081 1.933 0a6.57 6.57 0 0 1 3.65 1.82a100 100 0 0 0-1.986 1.93q-1.876-1.59-4.188-.734q-1.696.78-2.362 2.528a78 78 0 0 1-2.148-1.658a.26.26 0 0 0-.16-.027q1.683-3.245 5.26-3.86"
|
||||||
|
opacity="0.987"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#ffc107"
|
||||||
|
d="M1.946 4.92q.085-.013.161.027a78 78 0 0 0 2.148 1.658A7.6 7.6 0 0 0 4.04 7.99q.037.678.215 1.331L2 11.116Q.527 8.038 1.946 4.92"
|
||||||
|
opacity="0.997"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#448aff"
|
||||||
|
d="M12.685 13.29a26 26 0 0 0-2.202-1.74q1.15-.812 1.396-2.228H8.122V6.713q3.25-.027 6.497.055q.616 3.345-1.423 6.032a7 7 0 0 1-.51.49"
|
||||||
|
opacity="0.999"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#43a047"
|
||||||
|
d="M4.255 9.322q1.23 3.057 4.51 2.854a3.94 3.94 0 0 0 1.718-.626q1.148.812 2.202 1.74a6.62 6.62 0 0 1-4.027 1.684a6.4 6.4 0 0 1-1.02 0Q3.82 14.524 2 11.116z"
|
||||||
|
opacity="0.993"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Google
|
||||||
|
</Button>
|
||||||
|
<Button size="lg" className="w-full" variant="secondary">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1877f2"
|
||||||
|
d="M256 128C256 57.308 198.692 0 128 0S0 57.308 0 128c0 63.888 46.808 116.843 108 126.445V165H75.5v-37H108V99.8c0-32.08 19.11-49.8 48.348-49.8C170.352 50 185 52.5 185 52.5V84h-16.14C152.959 84 148 93.867 148 103.99V128h35.5l-5.675 37H148v89.445c61.192-9.602 108-62.556 108-126.445"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="m177.825 165l5.675-37H148v-24.01C148 93.866 152.959 84 168.86 84H185V52.5S170.352 50 156.347 50C127.11 50 108 67.72 108 99.8V128H75.5v37H108v89.445A129 129 0 0 0 128 256a129 129 0 0 0 20-1.555V165z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Facebook
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
61
src/routes/access.tsx
Normal file
61
src/routes/access.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Card, type Key, Tabs } from "@heroui/react"
|
||||||
|
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/access")({
|
||||||
|
beforeLoad: ({ location }) => {
|
||||||
|
if (location.pathname === "/access") {
|
||||||
|
redirect({
|
||||||
|
to: "/access/login",
|
||||||
|
replace: true,
|
||||||
|
throw: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const navigate = Route.useNavigate()
|
||||||
|
|
||||||
|
const onSelectTab = (tabId: Key) => {
|
||||||
|
navigate({
|
||||||
|
to: `/access/${tabId}`,
|
||||||
|
viewTransition: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid-cols-2 grid min-h-screen">
|
||||||
|
<div className=" p-5 bg-default">
|
||||||
|
<h1 className="text-4xl font-bold text-end ">
|
||||||
|
Find<span className="text-accent">your</span>Pilot
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center justify-center min-h-[90vh]">
|
||||||
|
<Card className="w-full max-w-md bg-white/90 backdrop-blur-2xl border-3 border-accent-soft">
|
||||||
|
<Card.Header>
|
||||||
|
<Tabs className="w-full max-w-md" onSelectionChange={onSelectTab}>
|
||||||
|
<Tabs.ListContainer>
|
||||||
|
<Tabs.List aria-label="Options">
|
||||||
|
<Tabs.Tab id="login" className="text-lg">
|
||||||
|
Acceso
|
||||||
|
<Tabs.Indicator className="bg-accent" />
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab id="register" className="text-lg ">
|
||||||
|
<Tabs.Separator />
|
||||||
|
Registro
|
||||||
|
<Tabs.Indicator className="bg-accent" />
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
</Tabs.ListContainer>
|
||||||
|
</Tabs>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<Outlet />
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-accent bg-[url('https://cdn.pixabay.com/photo/2023/03/22/22/37/mavic-2-7870679_1280.jpg')] bg-cover"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
export const Route = createFileRoute("/")({ component: App })
|
export const Route = createFileRoute("/")({ component: App })
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const navigate = Route.useNavigate()
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
icon: <Zap className="w-12 h-12 text-cyan-400" />,
|
icon: <Zap className="w-12 h-12 text-cyan-400" />,
|
||||||
@@ -53,7 +55,17 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-linear-to-b from-slate-900 via-slate-800 to-slate-900">
|
<div className="min-h-screen bg-linear-to-b from-slate-900 via-slate-800 to-slate-900">
|
||||||
<Button> Hola</Button>
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
navigate({
|
||||||
|
to: "/login",
|
||||||
|
viewTransition: true
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
Hola
|
||||||
|
</Button>
|
||||||
<section className="relative py-20 px-6 text-center overflow-hidden">
|
<section className="relative py-20 px-6 text-center overflow-hidden">
|
||||||
<div className="absolute inset-0 bg-linear-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10"></div>
|
<div className="absolute inset-0 bg-linear-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10"></div>
|
||||||
<div className="relative max-w-5xl mx-auto">
|
<div className="relative max-w-5xl mx-auto">
|
||||||
|
|||||||
11
src/routes/login.tsx
Normal file
11
src/routes/login.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/login")({
|
||||||
|
beforeLoad: () => {
|
||||||
|
redirect({
|
||||||
|
to: "/access/login",
|
||||||
|
replace: true,
|
||||||
|
throw: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
9
src/routes/logout.tsx
Normal file
9
src/routes/logout.tsx
Normal 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
|
||||||
|
})
|
||||||
@@ -18,122 +18,91 @@ code {
|
|||||||
font-family:
|
font-family:
|
||||||
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* HeroUI Theme Customization
|
||||||
|
* Add this to your global.css after importing @heroui/styles
|
||||||
|
* Only includes base variables from variables.css
|
||||||
|
* @see https://v3.heroui.com/docs/react/getting-started/theming
|
||||||
|
*/
|
||||||
|
|
||||||
:root {
|
:root,
|
||||||
--background: oklch(1 0 0);
|
.light,
|
||||||
--foreground: oklch(0.141 0.005 285.823);
|
.default,
|
||||||
--card: oklch(1 0 0);
|
[data-theme="light"],
|
||||||
--card-foreground: oklch(0.141 0.005 285.823);
|
[data-theme="default"] {
|
||||||
--popover: oklch(1 0 0);
|
/* Theme Colors (Light Mode) */
|
||||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
--accent: oklch(66.78% 0.2232 43.72);
|
||||||
--primary: oklch(0.21 0.006 285.885);
|
--accent-foreground: oklch(15% 0.0300 43.72);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--background: oklch(97.02% 0.0064 43.72);
|
||||||
--secondary: oklch(0.967 0.001 286.375);
|
--border: oklch(90.00% 0.0064 43.72);
|
||||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
--danger: oklch(65.32% 0.2358 0.53);
|
||||||
--muted: oklch(0.967 0.001 286.375);
|
--danger-foreground: oklch(99.11% 0 0);
|
||||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
--default: oklch(94.00% 0.0064 43.72);
|
||||||
--accent: oklch(0.967 0.001 286.375);
|
--default-foreground: oklch(21.03% 0.0059 43.72);
|
||||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
--field-background: oklch(100.00% 0.0032 43.72);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--field-foreground: oklch(21.03% 0.0064 43.72);
|
||||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
--field-placeholder: oklch(55.17% 0.0128 43.72);
|
||||||
--border: oklch(0.92 0.004 286.32);
|
--focus: oklch(66.78% 0.2232 43.72);
|
||||||
--input: oklch(0.92 0.004 286.32);
|
--foreground: oklch(21.03% 0.0064 43.72);
|
||||||
--ring: oklch(0.871 0.006 286.286);
|
--muted: oklch(55.17% 0.0128 43.72);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--overlay: oklch(100.00% 0.0019 43.72);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--overlay-foreground: oklch(21.03% 0.0064 43.72);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--scrollbar: oklch(87.10% 0.0064 43.72);
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--segment: oklch(100.00% 0.0064 43.72);
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--segment-foreground: oklch(21.03% 0.0064 43.72);
|
||||||
--radius: 0.625rem;
|
--separator: oklch(92.00% 0.0064 43.72);
|
||||||
--sidebar: oklch(0.985 0 0);
|
--success: oklch(73.29% 0.1960 125.60);
|
||||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
--success-foreground: oklch(21.03% 0.0059 125.60);
|
||||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
--surface: oklch(100.00% 0.0032 43.72);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--surface-foreground: oklch(21.03% 0.0064 43.72);
|
||||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
--surface-secondary: oklch(95.24% 0.0051 43.72);
|
||||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
--surface-secondary-foreground: oklch(21.03% 0.0064 43.72);
|
||||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
--surface-tertiary: oklch(93.73% 0.0051 43.72);
|
||||||
--sidebar-ring: oklch(0.871 0.006 286.286);
|
--surface-tertiary-foreground: oklch(21.03% 0.0064 43.72);
|
||||||
|
--warning: oklch(78.19% 0.1605 47.12);
|
||||||
|
--warning-foreground: oklch(21.03% 0.0059 47.12);
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
--radius: 0.125rem;
|
||||||
|
--field-radius: 0.125rem;
|
||||||
|
|
||||||
|
/* Font Family */
|
||||||
|
/* Make sure to load Inter font in your app */
|
||||||
|
--font-sans: var(--font-inter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark,
|
||||||
--background: oklch(0.141 0.005 285.823);
|
[data-theme="dark"] {
|
||||||
--foreground: oklch(0.985 0 0);
|
color-scheme: dark;
|
||||||
--card: oklch(0.141 0.005 285.823);
|
/* Theme Colors (Dark Mode) */
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--accent: oklch(66.78% 0.2232 43.72);
|
||||||
--popover: oklch(0.141 0.005 285.823);
|
--accent-foreground: oklch(15% 0.0300 43.72);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--background: oklch(12.00% 0.0064 43.72);
|
||||||
--primary: oklch(0.985 0 0);
|
--border: oklch(28.00% 0.0064 43.72);
|
||||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
--danger: oklch(59.40% 0.1992 359.42);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--danger-foreground: oklch(99.11% 0 0);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--default: oklch(27.40% 0.0064 43.72);
|
||||||
--muted: oklch(0.274 0.006 286.033);
|
--default-foreground: oklch(99.11% 0 0);
|
||||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
--field-background: oklch(21.03% 0.0128 43.72);
|
||||||
--accent: oklch(0.274 0.006 286.033);
|
--field-foreground: oklch(99.11% 0.0064 43.72);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--field-placeholder: oklch(70.50% 0.0128 43.72);
|
||||||
--destructive: oklch(0.396 0.141 25.723);
|
--focus: oklch(66.78% 0.2232 43.72);
|
||||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
--foreground: oklch(99.11% 0.0064 43.72);
|
||||||
--border: oklch(0.274 0.006 286.033);
|
--muted: oklch(70.50% 0.0128 43.72);
|
||||||
--input: oklch(0.274 0.006 286.033);
|
--overlay: oklch(21.03% 0.0128 43.72);
|
||||||
--ring: oklch(0.442 0.017 285.786);
|
--overlay-foreground: oklch(99.11% 0.0064 43.72);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--scrollbar: oklch(70.50% 0.0064 43.72);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--segment: oklch(39.64% 0.0064 43.72);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--segment-foreground: oklch(99.11% 0.0064 43.72);
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--separator: oklch(25.00% 0.0064 43.72);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--success: oklch(73.29% 0.1960 125.60);
|
||||||
--sidebar: oklch(0.21 0.006 285.885);
|
--success-foreground: oklch(21.03% 0.0059 125.60);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--surface: oklch(21.03% 0.0128 43.72);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--surface-foreground: oklch(99.11% 0.0064 43.72);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--surface-secondary: oklch(25.70% 0.0096 43.72);
|
||||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
--surface-secondary-foreground: oklch(99.11% 0.0064 43.72);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--surface-tertiary: oklch(27.21% 0.0096 43.72);
|
||||||
--sidebar-border: oklch(0.274 0.006 286.033);
|
--surface-tertiary-foreground: oklch(99.11% 0.0064 43.72);
|
||||||
--sidebar-ring: oklch(0.442 0.017 285.786);
|
--warning: oklch(82.03% 0.1406 51.13);
|
||||||
}
|
--warning-foreground: oklch(21.03% 0.0059 51.13);
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user