perf: complete performance audit optimizations

This commit is contained in:
2026-02-26 22:17:56 -05:00
parent c9a260ab23
commit a8eaad7f35
14 changed files with 439 additions and 33 deletions

View File

@ -8,6 +8,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:hardwareAccelerated="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity

View File

@ -8,6 +8,9 @@ const config: CapacitorConfig = {
androidScheme: 'http',
cleartext: true
},
android: {
allowMixedContent: false,
},
plugins: {
SplashScreen: {
launchShowDuration: 2000,
@ -16,6 +19,9 @@ const config: CapacitorConfig = {
androidSplashResourceName: "splash",
androidScaleType: "CENTER_CROP",
showSpinner: false,
},
Geolocation: {
androidUsePreciseLocation: true
}
}
};

View File

@ -31,7 +31,9 @@
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"rollup-plugin-visualizer": "^7.0.0",
"tailwindcss": "^4.2.0",
"terser": "^5.46.0",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vite-plugin-pwa": "^1.2.0",
@ -3773,6 +3775,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -4220,6 +4235,93 @@
"node": ">=18"
}
},
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -5080,6 +5182,29 @@
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
"integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -5530,6 +5655,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-in-ssh": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@ -6618,6 +6756,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/powershell-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
"integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pretty-bytes": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
@ -6981,6 +7132,127 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-visualizer": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-7.0.0.tgz",
"integrity": "sha512-loo4kmhTg7GMO0hqaUv/azvLPUT2B4jXU3gNMG35gm1mWKpOzhV6rspb/Mqmsfg7oOTdkzdmOckCIwGB5Ca1CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"open": "^11.0.0",
"picomatch": "^4.0.2",
"source-map": "^0.7.4",
"yargs": "^18.0.0"
},
"bin": {
"rollup-plugin-visualizer": "dist/bin/cli.js"
},
"engines": {
"node": ">=22"
},
"peerDependencies": {
"rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc",
"rollup": "2.x || 3.x || 4.x"
},
"peerDependenciesMeta": {
"rolldown": {
"optional": true
},
"rollup": {
"optional": true
}
}
},
"node_modules/rollup-plugin-visualizer/node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rollup-plugin-visualizer/node_modules/is-wsl": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
"integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-inside-container": "^1.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rollup-plugin-visualizer/node_modules/open": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz",
"integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"default-browser": "^5.4.0",
"define-lazy-prop": "^3.0.0",
"is-in-ssh": "^1.0.0",
"is-inside-container": "^1.0.0",
"powershell-utils": "^0.1.0",
"wsl-utils": "^0.3.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/rollup-plugin-visualizer/node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 12"
}
},
"node_modules/rollup-plugin-visualizer/node_modules/wsl-utils": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz",
"integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-wsl": "^3.1.0",
"powershell-utils": "^0.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-applescript": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
@ -9134,6 +9406,16 @@
"node": ">=8.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@ -9143,6 +9425,75 @@
"node": ">=18"
}
},
"node_modules/yargs": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^7.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",

View File

@ -32,7 +32,9 @@
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"rollup-plugin-visualizer": "^7.0.0",
"tailwindcss": "^4.2.0",
"terser": "^5.46.0",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vite-plugin-pwa": "^1.2.0",

View File

@ -5,14 +5,14 @@ import type { BusStop, Route } from '@/types'
export const busStopsService = {
/** Get all bus stops */
async getAllBusStops(): Promise<BusStop[]> {
const { data, error } = await supabase.from('bus_stops').select('*')
const { data, error } = await supabase.from('bus_stops').select('id, name, latitude, longitude, city, address, parent_id, side, stop_type, has_shelter, has_seating, is_accessible, created_at, updated_at')
if (error) throw new Error(error.message)
return data as BusStop[]
},
/** Get a single bus stop by ID */
async getBusStopById(id: string): Promise<BusStop> {
const { data, error } = await supabase.from('bus_stops').select('*').eq('id', id).single()
const { data, error } = await supabase.from('bus_stops').select('id, name, latitude, longitude, city, address, parent_id, side, stop_type, has_shelter, has_seating, is_accessible, created_at, updated_at').eq('id', id).single()
if (error) throw new Error(error.message)
return data as BusStop
},

View File

@ -18,14 +18,14 @@ export const businessService = {
/** Get all businesses */
async getAllBusinesses(): Promise<Business[]> {
const { data, error } = await supabase.from('businesses').select('*')
const { data, error } = await supabase.from('businesses').select('id, name, address, phone, image_url, social_media, category, latitude, longitude, area, updated_at')
if (error) throw new Error(error.message)
return data as Business[]
},
/** Get a single business by ID */
async getBusiness(id: string): Promise<Business> {
const { data, error } = await supabase.from('businesses').select('*').eq('id', id).single()
const { data, error } = await supabase.from('businesses').select('id, name, address, phone, image_url, social_media, category, latitude, longitude, area, updated_at').eq('id', id).single()
if (error) throw new Error(error.message)
return data as Business
},

View File

@ -5,7 +5,7 @@ import type { Route, BusStop } from '@/types'
export const routesService = {
/** Get all routes with optional filtering */
async getAllRoutes(filters?: { originCity?: string, destinationCity?: string }): Promise<Route[]> {
let query = supabase.from('routes').select('*')
let query = supabase.from('routes').select('id, name, description, color, direction, origin_city, destination_city, distance_km, estimated_duration_minutes, average_speed_kmh, status, created_at, updated_at')
if (filters?.originCity) {
query = query.eq('origin_city', filters.originCity)
@ -21,7 +21,7 @@ export const routesService = {
/** Get a single route by ID */
async getRouteById(id: string): Promise<Route> {
const { data, error } = await supabase.from('routes').select('*').eq('id', id).single()
const { data, error } = await supabase.from('routes').select('id, name, description, color, direction, origin_city, destination_city, distance_km, estimated_duration_minutes, average_speed_kmh, status, created_at, updated_at').eq('id', id).single()
if (error) throw new Error(error.message)
return data as Route
},

View File

@ -32,7 +32,16 @@ export const useBusStopStore = defineStore('busStop', () => {
}
}
async function loadBusStopById(id: string) {
async function loadBusStopById(id: string, force = false) {
// Buscar en cache primero
if (!force && busStops.value.length > 0) {
const cachedStop = busStops.value.find(s => s.id === id);
if (cachedStop) {
selectedStop.value = cachedStop;
return;
}
}
isLoading.value = true
error.value = null
try {

View File

@ -12,7 +12,8 @@ export const useRouteStore = defineStore('route', () => {
const isLoadingRoutes = ref(false)
const isLoadingStops = ref(false)
const error = ref<string | null>(null)
const lastFetched = ref<number>(0) // ⚡ CACHÉ ESTÁTICO
const lastFetched = ref<number>(0) // ⚡ CACHÉ ESTÁTICO RUTAS
const stopsCache = ref<Map<string, { fetchedAt: number, stops: BusStop[] }>>(new Map()) // ⚡ CACHÉ ESTÁTICO PARADAS
const hasSelectedRoute = computed(() => selectedRouteId.value !== null && selectedRouteName.value !== null)
@ -38,11 +39,24 @@ export const useRouteStore = defineStore('route', () => {
}
}
async function loadRouteStops(routeId: string) {
async function loadRouteStops(routeId: string, force = false) {
const CACHE_TIME = 1000 * 60 * 15; // 15 minutos
const now = Date.now();
if (!force && stopsCache.value.has(routeId)) {
const cacheEntry = stopsCache.value.get(routeId)!;
if (now - cacheEntry.fetchedAt < CACHE_TIME) {
selectedRouteStops.value = cacheEntry.stops;
return;
}
}
isLoadingStops.value = true
error.value = null
try {
selectedRouteStops.value = await routesService.getRouteStops(routeId)
const stops = await routesService.getRouteStops(routeId)
selectedRouteStops.value = stops
stopsCache.value.set(routeId, { fetchedAt: now, stops })
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load route stops'
console.error('Error loading route stops:', e)

View File

@ -51,7 +51,8 @@ import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { busStopsService } from '@/services/busStopsService'
import type { BusStop } from '@/types'
import BusStopEditor from '@/components/BusStopEditor.vue'
import { defineAsyncComponent } from 'vue'
const BusStopEditor = defineAsyncComponent(() => import('@/components/BusStopEditor.vue'))
const router = useRouter()
const stops = ref<BusStop[]>([])

View File

@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import LoginForm from '@/components/auth/LoginForm.vue'
import RegisterForm from '@/components/auth/RegisterForm.vue'
import { defineAsyncComponent } from 'vue'
const LoginForm = defineAsyncComponent(() => import('@/components/auth/LoginForm.vue'))
const RegisterForm = defineAsyncComponent(() => import('@/components/auth/RegisterForm.vue'))
const isLogin = ref(true)
const toggleAuth = () => { isLogin.value = !isLogin.value }

View File

@ -122,11 +122,12 @@ function getCategoryIcon(category?: string | null) {
<div
v-for="coupon in filteredCoupons"
:key="coupon.id"
v-memo="[coupon.id]"
class="offer-card-new"
@click="openCoupon(coupon)"
>
<div class="offer-image-wrapper">
<img :src="getImageUrl(coupon.image_url)" :alt="coupon.title" class="offer-img">
<img :src="getImageUrl(coupon.image_url)" :alt="coupon.title" loading="lazy" decoding="async" class="offer-img">
<div class="status-badge" :class="{ 'tmr': coupon.title.toLowerCase().includes('mañana') || (coupon.description?.toLowerCase().includes('mañana') ?? false) }">
<span class="material-icons">schedule</span>
{{ coupon.title.toLowerCase().includes('mañana') ? t('coupons.tomorrow') : t('coupons.active') }}

View File

@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'
import { useTaxiStore } from '@/stores/taxi'
import { useShuttleStore } from '@/stores/shuttle'
import { analyticsService } from '@/services/analyticsService'
import type { Taxi, Shuttle } from '@/types'
import type { Taxi } from '@/types'
import FavoriteButton from '@/components/FavoriteButton.vue'
import { getImageUrl } from '@/utils/imageUrl'

View File

@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
// https://vite.dev/config/
export default defineConfig(() => {
@ -10,6 +11,12 @@ export default defineConfig(() => {
plugins: [
vue(),
tailwindcss(),
visualizer({
open: false,
filename: 'dist/stats.html',
gzipSize: true,
brotliSize: true
}),
VitePWA({
selfDestroying: true,
registerType: 'autoUpdate',
@ -48,36 +55,39 @@ export default defineConfig(() => {
navigateFallbackDenylist: [/^\/api/, /^\/rest\/v1/],
runtimeCaching: [
{
// ASSETS EXTERNOS E IMÁGENES SUPERBASE
urlPattern: /^https:\/\/(.*\.(png|jpg|jpeg|svg|webp|woff2|css))/,
urlPattern: /^https:\/\/maps\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'assets-estaticos-sibu',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 }, // 30 días
cacheableResponse: { statuses: [0, 200] },
},
cacheName: 'google-maps-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 604800 }
}
},
{
// LLAMADAS API SEMI-ESTÁTICAS (Supabase listas que no mutan tan rápido)
urlPattern: /^https:\/\/.*\.supabase\.co\/rest\/v1\/(routes|bus_stops)/,
urlPattern: /^https:\/\/.*\.supabase\.co\/rest\/v1\/(routes|bus_stops|businesses).*/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'api-estatica-sibu',
cacheableResponse: { statuses: [0, 200] },
},
cacheName: 'supabase-static-cache',
expiration: { maxEntries: 30, maxAgeSeconds: 300 }
}
},
{
// LLAMADAS API REALTIME / DELUXE
urlPattern: /^https:\/\/.*\.supabase\.co\/rest\/v1\/(shuttles|locations|users)/,
urlPattern: /^https:\/\/.*\.supabase\.co\/rest\/v1\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-dinamica',
networkTimeoutSeconds: 5, // Vital en zonas rurales: si el 3G no responde en 5s, muestra la caché
cacheableResponse: { statuses: [0, 200] },
},
cacheName: 'supabase-dynamic-cache',
networkTimeoutSeconds: 5,
expiration: { maxEntries: 50, maxAgeSeconds: 60 }
}
},
{
urlPattern: /^https:\/\/.*\.supabase\.co\/storage\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'supabase-images-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 2592000 }
}
},
{
// FONT CACHE (Google)
urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
handler: 'CacheFirst',
options: {
@ -126,9 +136,19 @@ export default defineConfig(() => {
if (id.includes('node_modules/@googlemaps')) {
return 'vendor-maps'
}
if (id.includes('node_modules/@supabase/supabase-js')) {
return 'vendor-supabase'
}
},
},
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
},
}
})