mirror of
https://github.com/catdevnull/preciazo.git
synced 2024-11-26 03:26:19 +00:00
mejorar
This commit is contained in:
parent
2f6963e5f5
commit
3c902dbc70
10 changed files with 222 additions and 145 deletions
BIN
sepa/bun.lockb
BIN
sepa/bun.lockb
Binary file not shown.
|
@ -18,7 +18,7 @@
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
|
||||||
"@tailwindcss/typography": "^0.5.14",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.0",
|
||||||
"@types/leaflet": "^1.9.12",
|
"@types/leaflet": "^1.9.12",
|
||||||
"@types/leaflet.markercluster": "^1.5.4",
|
"@types/leaflet.markercluster": "^1.5.4",
|
||||||
|
@ -46,6 +46,7 @@
|
||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.33.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet.markercluster": "^1.5.3",
|
"leaflet.markercluster": "^1.5.3",
|
||||||
|
"lucide-svelte": "^0.441.0",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwind-variants": "^0.2.1"
|
"tailwind-variants": "^0.2.1"
|
||||||
|
|
|
@ -76,3 +76,11 @@
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
body > div {
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -9,13 +9,12 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const L = await import('leaflet');
|
const L = await import('leaflet');
|
||||||
const L1 = await import('leaflet.markercluster');
|
|
||||||
// Set up initial map center and zoom level
|
// Set up initial map center and zoom level
|
||||||
map = L.map(mapEl, {
|
map = L.map(mapEl, {
|
||||||
center: [-34.599722222222, -58.381944444444], // EDIT latitude, longitude to re-center map
|
center: [-34.599722222222, -58.381944444444], // EDIT latitude, longitude to re-center map
|
||||||
zoom: 9, // EDIT from 1 to 18 -- decrease to zoom out, increase to zoom in
|
zoom: 9, // EDIT from 1 to 18 -- decrease to zoom out, increase to zoom in
|
||||||
scrollWheelZoom: true, // Changed to true to enable zoom with scrollwheel
|
scrollWheelZoom: true // Changed to true to enable zoom with scrollwheel
|
||||||
tap: false
|
// tap: false
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Control panel to display map layers */
|
/* Control panel to display map layers */
|
||||||
|
@ -38,12 +37,19 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="map" bind:this={mapEl}></div>
|
<div class="wrapper flex-auto">
|
||||||
|
<div class="map" bind:this={mapEl}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.map {
|
.map {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 500px;
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,9 +10,22 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-screen-sm p-4">
|
<div class="mx-auto max-w-screen-sm p-4">
|
||||||
<h1 class="flex text-3xl font-bold">
|
<h1 class="my-4 flex text-5xl font-bold">
|
||||||
preciazo<small class="text-xs">alpha</small>
|
preciazo<small class="text-sm">alpha</small>
|
||||||
</h1>
|
</h1>
|
||||||
<h2>¡actualmente tengo {numberFormat.format(data.count)} registros de precios!</h2>
|
<h2 class="my-4 text-lg font-medium text-gray-700">
|
||||||
|
¡actualmente tengo {numberFormat.format(data.count)} registros de precios!
|
||||||
|
</h2>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
|
<footer class="prose my-4 text-sm text-gray-700">
|
||||||
|
preciazo es una iniciativa de <a
|
||||||
|
href="https://nulo.in/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">Nulo</a
|
||||||
|
>. usa la base de datos abierta
|
||||||
|
<a href="https://datos.produccion.gob.ar/dataset/sepa-precios">Precios Claros - Base SEPA</a>
|
||||||
|
del Ministerio de Economía. podes contactarme en
|
||||||
|
<a href="mailto:hola@nulo.in">hola@nulo.in</a>. twitter:
|
||||||
|
<a href="https://x.com/esoesnulo" target="_blank" rel="noopener noreferrer">@esoesnulo</a>.
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { PageServerLoad } from './$types';
|
||||||
import { precios, sucursales } from '$lib/server/db/schema';
|
import { precios, sucursales } from '$lib/server/db/schema';
|
||||||
import { and, eq, sql } from 'drizzle-orm';
|
import { and, eq, sql } from 'drizzle-orm';
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const id = BigInt(params.id);
|
||||||
const preciosRes = await db
|
const preciosRes = await db
|
||||||
.select({
|
.select({
|
||||||
id_comercio: precios.id_comercio,
|
id_comercio: precios.id_comercio,
|
||||||
|
@ -13,7 +14,7 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||||
productos_descripcion: precios.productos_descripcion,
|
productos_descripcion: precios.productos_descripcion,
|
||||||
sucursales_latitud: sucursales.sucursales_latitud,
|
sucursales_latitud: sucursales.sucursales_latitud,
|
||||||
sucursales_longitud: sucursales.sucursales_longitud,
|
sucursales_longitud: sucursales.sucursales_longitud,
|
||||||
sucursales_nombre: sucursales.sucursales_nombre,
|
sucursales_nombre: sucursales.sucursales_nombre
|
||||||
})
|
})
|
||||||
.from(precios)
|
.from(precios)
|
||||||
.where(
|
.where(
|
||||||
|
@ -33,7 +34,14 @@ ORDER BY d1.id_comercio)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.leftJoin(sucursales, and(eq(sucursales.id_dataset, precios.id_dataset), eq(sucursales.id_sucursal, precios.id_sucursal), eq(sucursales.id_comercio, precios.id_comercio)));
|
.leftJoin(
|
||||||
|
sucursales,
|
||||||
|
and(
|
||||||
|
eq(sucursales.id_dataset, precios.id_dataset),
|
||||||
|
eq(sucursales.id_sucursal, precios.id_sucursal),
|
||||||
|
eq(sucursales.id_comercio, precios.id_comercio)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// const precios = await sql<
|
// const precios = await sql<
|
||||||
// {
|
// {
|
||||||
|
@ -84,11 +92,12 @@ ORDER BY d1.id_comercio)
|
||||||
// `;
|
// `;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
precios:preciosRes.map(p => ({
|
precios: preciosRes.map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
productos_precio_lista: parseFloat(p.productos_precio_lista ?? '0'),
|
productos_precio_lista: parseFloat(p.productos_precio_lista ?? '0'),
|
||||||
sucursales_latitud: parseFloat(p.sucursales_latitud ?? '0'),
|
sucursales_latitud: parseFloat(p.sucursales_latitud ?? '0'),
|
||||||
sucursales_longitud: parseFloat(p.sucursales_longitud ?? '0'),
|
sucursales_longitud: parseFloat(p.sucursales_longitud ?? '0')
|
||||||
}))
|
})),
|
||||||
|
id_producto: id
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,19 +1,33 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { ArrowLeft } from 'lucide-svelte';
|
||||||
import Map from '$lib/components/Map.svelte';
|
import Map from '$lib/components/Map.svelte';
|
||||||
|
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||||
|
import {} from '$app/navigation';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
const pesosFormatter = new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS'
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Producto {data.precios[0].productos_descripcion}</h1>
|
<div class="flex min-h-screen flex-col">
|
||||||
|
<div class="flex items-stretch gap-3 px-2">
|
||||||
|
<button on:click={() => window.history.back()}>
|
||||||
|
<ArrowLeft class="size-8" />
|
||||||
|
</button>
|
||||||
|
<h1 class="flex items-center gap-2 py-1 text-2xl font-bold">
|
||||||
|
{data.precios[0].productos_descripcion}
|
||||||
|
<Badge>mostrando {data.precios.length} precios</Badge>
|
||||||
|
<Badge variant="outline">EAN {data.id_producto}</Badge>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>cantidad de precios: {data.precios.length}</h2>
|
|
||||||
|
|
||||||
<div class="h-[80vh]">
|
|
||||||
<Map
|
<Map
|
||||||
mapMap={(map, L) => {
|
mapMap={(map, L) => {
|
||||||
// var markers = L.MarkerClusterGroup();
|
// var markers = L.MarkerClusterGroup();
|
||||||
|
|
||||||
const myRenderer = L.canvas({ padding: 0.5 });
|
const myRenderer = L.canvas({ padding: 0.5 });
|
||||||
const prices = data.precios.map((p) => p.productos_precio_lista);
|
const prices = data.precios.map((p) => p.productos_precio_lista);
|
||||||
const sortedPrices = prices.sort((a, b) => a - b);
|
const sortedPrices = prices.sort((a, b) => a - b);
|
||||||
|
@ -31,31 +45,42 @@
|
||||||
// For each row, columns `Latitude`, `Longitude`, and `Title` are required
|
// For each row, columns `Latitude`, `Longitude`, and `Title` are required
|
||||||
for (const precio of data.precios) {
|
for (const precio of data.precios) {
|
||||||
const normalizedPrice = (precio.productos_precio_lista - min) / (max - min);
|
const normalizedPrice = (precio.productos_precio_lista - min) / (max - min);
|
||||||
// const l = 0.8 - normalizedPrice * 0.8; // Lightness decreases as price increases
|
// Safari doesn't support color-mix, so we'll use a fallback
|
||||||
// const a = -0.2 + normalizedPrice * 0.4; // Green to red
|
const color = getSafeColor(normalizedPrice);
|
||||||
// const b = 0.2 - normalizedPrice * 0.4; // Yellow to blue
|
|
||||||
const color = `color-mix(in lab, yellow, red ${normalizedPrice * 100}%)`;
|
const createElement = () => {
|
||||||
// const color = `oklch(${l} ${Math.sqrt(a * a + b * b)} ${Math.atan2(b, a)})`;
|
const div = document.createElement('div');
|
||||||
// console.log(row)
|
|
||||||
|
[
|
||||||
|
`precio: ${pesosFormatter.format(precio.productos_precio_lista)}`,
|
||||||
|
`sucursal: ${precio.sucursales_nombre}`,
|
||||||
|
`descripcion del producto segun el comercio: ${precio.productos_descripcion}`
|
||||||
|
].forEach((text) => {
|
||||||
|
div.append(text);
|
||||||
|
div.append(document.createElement('br'));
|
||||||
|
});
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
|
||||||
var marker = L.circleMarker([precio.sucursales_latitud, precio.sucursales_longitud], {
|
var marker = L.circleMarker([precio.sucursales_latitud, precio.sucursales_longitud], {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
renderer: myRenderer,
|
renderer: myRenderer,
|
||||||
color,
|
color,
|
||||||
radius: 5
|
radius: 5
|
||||||
// riseOnHover: false,
|
|
||||||
// riseOffset: 0
|
|
||||||
})
|
})
|
||||||
.bindPopup(
|
.bindPopup(createElement)
|
||||||
`precio: ${precio.productos_precio_lista}<br>sucursal: ${precio.sucursales_nombre}<br>descripcion: ${precio.productos_descripcion}`
|
|
||||||
)
|
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
marker.on('click', function(this: L.CircleMarker) {
|
marker.on('click', function (this: L.CircleMarker) {
|
||||||
this.openPopup();
|
this.openPopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
// markers.addLayer(marker);
|
|
||||||
}
|
}
|
||||||
// map.addLayer(markers);
|
|
||||||
|
// Helper function to get a color that works in Safari
|
||||||
|
function getSafeColor(normalizedPrice: number) {
|
||||||
|
const r = Math.round(255 * normalizedPrice);
|
||||||
|
const g = Math.round(255 * (1 - normalizedPrice));
|
||||||
|
return `rgb(${r}, ${g}, 0)`;
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SearchBar from '$lib/components/SearchBar.svelte';
|
import SearchBar from '$lib/components/SearchBar.svelte';
|
||||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||||
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
import * as Card from '$lib/components/ui/card/index.js';
|
import * as Card from '$lib/components/ui/card/index.js';
|
||||||
|
import { ArrowLeft } from 'lucide-svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-screen-sm p-4">
|
<div class="mx-auto max-w-screen-sm p-4">
|
||||||
|
<Button on:click={() => goto('/')} class="mb-2 gap-1" variant="outline">
|
||||||
|
<ArrowLeft />
|
||||||
|
Volver al inicio
|
||||||
|
</Button>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<h1 class="my-2 text-2xl font-bold">Resultados para "{data.query}"</h1>
|
<h1 class="my-2 text-2xl font-bold">Resultados para "{data.query}"</h1>
|
||||||
{#each data.collapsedProductos as producto}
|
{#each data.collapsedProductos as producto}
|
||||||
|
|
|
@ -1,64 +1,69 @@
|
||||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
import { fontFamily } from 'tailwindcss/defaultTheme';
|
||||||
import type { Config } from "tailwindcss";
|
import type { Config } from 'tailwindcss';
|
||||||
|
import typography from '@tailwindcss/typography';
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
darkMode: ["class"],
|
darkMode: ['class'],
|
||||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
safelist: ["dark"],
|
safelist: ['dark'],
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: "2rem",
|
padding: '2rem',
|
||||||
screens: {
|
screens: {
|
||||||
"2xl": "1400px"
|
'2xl': '1400px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border) / <alpha-value>)",
|
border: 'hsl(var(--border) / <alpha-value>)',
|
||||||
input: "hsl(var(--input) / <alpha-value>)",
|
input: 'hsl(var(--input) / <alpha-value>)',
|
||||||
ring: "hsl(var(--ring) / <alpha-value>)",
|
ring: 'hsl(var(--ring) / <alpha-value>)',
|
||||||
background: "hsl(var(--background) / <alpha-value>)",
|
background: 'hsl(var(--background) / <alpha-value>)',
|
||||||
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
foreground: 'hsl(var(--foreground) / <alpha-value>)',
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
|
||||||
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
|
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
|
||||||
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
|
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
|
||||||
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
|
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
|
||||||
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
|
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
|
||||||
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
|
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
|
||||||
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
|
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
|
||||||
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
|
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: 'var(--radius)',
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: "calc(var(--radius) - 4px)"
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: [...fontFamily.sans]
|
sans: [...fontFamily.sans]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
typography
|
||||||
|
// ...
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
Loading…
Reference in a new issue