mirror of
https://github.com/catdevnull/preciazo.git
synced 2024-11-25 19:16: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/kit": "^2.0.0",
|
||||
"@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/leaflet": "^1.9.12",
|
||||
"@types/leaflet.markercluster": "^1.5.4",
|
||||
|
@ -46,6 +46,7 @@
|
|||
"drizzle-orm": "^0.33.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"lucide-svelte": "^0.441.0",
|
||||
"postgres": "^3.4.4",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-variants": "^0.2.1"
|
||||
|
|
|
@ -1,78 +1,86 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 240 10% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 240 4.9% 83.9%;
|
||||
}
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 240 10% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 240 4.9% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
body > div {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -9,13 +9,12 @@
|
|||
|
||||
onMount(async () => {
|
||||
const L = await import('leaflet');
|
||||
const L1 = await import('leaflet.markercluster');
|
||||
// Set up initial map center and zoom level
|
||||
map = L.map(mapEl, {
|
||||
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
|
||||
scrollWheelZoom: true, // Changed to true to enable zoom with scrollwheel
|
||||
tap: false
|
||||
scrollWheelZoom: true // Changed to true to enable zoom with scrollwheel
|
||||
// tap: false
|
||||
});
|
||||
|
||||
/* Control panel to display map layers */
|
||||
|
@ -38,12 +37,19 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="map" bind:this={mapEl}></div>
|
||||
<div class="wrapper flex-auto">
|
||||
<div class="map" bind:this={mapEl}></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
position: absolute !important;
|
||||
}
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,9 +10,22 @@
|
|||
</script>
|
||||
|
||||
<div class="mx-auto max-w-screen-sm p-4">
|
||||
<h1 class="flex text-3xl font-bold">
|
||||
preciazo<small class="text-xs">alpha</small>
|
||||
<h1 class="my-4 flex text-5xl font-bold">
|
||||
preciazo<small class="text-sm">alpha</small>
|
||||
</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 />
|
||||
<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>
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { PageServerLoad } from './$types';
|
|||
import { precios, sucursales } from '$lib/server/db/schema';
|
||||
import { and, eq, sql } from 'drizzle-orm';
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const id = BigInt(params.id);
|
||||
const preciosRes = await db
|
||||
.select({
|
||||
id_comercio: precios.id_comercio,
|
||||
|
@ -13,7 +14,7 @@ export const load: PageServerLoad = async ({ params }) => {
|
|||
productos_descripcion: precios.productos_descripcion,
|
||||
sucursales_latitud: sucursales.sucursales_latitud,
|
||||
sucursales_longitud: sucursales.sucursales_longitud,
|
||||
sucursales_nombre: sucursales.sucursales_nombre,
|
||||
sucursales_nombre: sucursales.sucursales_nombre
|
||||
})
|
||||
.from(precios)
|
||||
.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<
|
||||
// {
|
||||
|
@ -84,11 +92,12 @@ ORDER BY d1.id_comercio)
|
|||
// `;
|
||||
|
||||
return {
|
||||
precios:preciosRes.map(p => ({
|
||||
precios: preciosRes.map((p) => ({
|
||||
...p,
|
||||
productos_precio_lista: parseFloat(p.productos_precio_lista ?? '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">
|
||||
import type { PageData } from './$types';
|
||||
import { ArrowLeft } from 'lucide-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;
|
||||
|
||||
const pesosFormatter = new Intl.NumberFormat('es-AR', {
|
||||
style: 'currency',
|
||||
currency: 'ARS'
|
||||
});
|
||||
</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
|
||||
mapMap={(map, L) => {
|
||||
// var markers = L.MarkerClusterGroup();
|
||||
|
||||
const myRenderer = L.canvas({ padding: 0.5 });
|
||||
const prices = data.precios.map((p) => p.productos_precio_lista);
|
||||
const sortedPrices = prices.sort((a, b) => a - b);
|
||||
|
@ -31,31 +45,42 @@
|
|||
// For each row, columns `Latitude`, `Longitude`, and `Title` are required
|
||||
for (const precio of data.precios) {
|
||||
const normalizedPrice = (precio.productos_precio_lista - min) / (max - min);
|
||||
// const l = 0.8 - normalizedPrice * 0.8; // Lightness decreases as price increases
|
||||
// const a = -0.2 + normalizedPrice * 0.4; // Green to red
|
||||
// const b = 0.2 - normalizedPrice * 0.4; // Yellow to blue
|
||||
const color = `color-mix(in lab, yellow, red ${normalizedPrice * 100}%)`;
|
||||
// const color = `oklch(${l} ${Math.sqrt(a * a + b * b)} ${Math.atan2(b, a)})`;
|
||||
// console.log(row)
|
||||
// Safari doesn't support color-mix, so we'll use a fallback
|
||||
const color = getSafeColor(normalizedPrice);
|
||||
|
||||
const createElement = () => {
|
||||
const div = document.createElement('div');
|
||||
|
||||
[
|
||||
`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], {
|
||||
opacity: 1,
|
||||
renderer: myRenderer,
|
||||
color,
|
||||
radius: 5
|
||||
// riseOnHover: false,
|
||||
// riseOffset: 0
|
||||
})
|
||||
.bindPopup(
|
||||
`precio: ${precio.productos_precio_lista}<br>sucursal: ${precio.sucursales_nombre}<br>descripcion: ${precio.productos_descripcion}`
|
||||
)
|
||||
.bindPopup(createElement)
|
||||
.addTo(map);
|
||||
marker.on('click', function(this: L.CircleMarker) {
|
||||
marker.on('click', function (this: L.CircleMarker) {
|
||||
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>
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
<script lang="ts">
|
||||
import SearchBar from '$lib/components/SearchBar.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 { ArrowLeft } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<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 />
|
||||
<h1 class="my-2 text-2xl font-bold">Resultados para "{data.query}"</h1>
|
||||
{#each data.collapsedProductos as producto}
|
||||
|
|
|
@ -1,64 +1,69 @@
|
|||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
import type { Config } from "tailwindcss";
|
||||
import { fontFamily } from 'tailwindcss/defaultTheme';
|
||||
import type { Config } from 'tailwindcss';
|
||||
import typography from '@tailwindcss/typography';
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
safelist: ["dark"],
|
||||
darkMode: ['class'],
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
safelist: ['dark'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
"2xl": "1400px"
|
||||
'2xl': '1400px'
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border) / <alpha-value>)",
|
||||
input: "hsl(var(--input) / <alpha-value>)",
|
||||
ring: "hsl(var(--ring) / <alpha-value>)",
|
||||
background: "hsl(var(--background) / <alpha-value>)",
|
||||
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
||||
border: 'hsl(var(--border) / <alpha-value>)',
|
||||
input: 'hsl(var(--input) / <alpha-value>)',
|
||||
ring: 'hsl(var(--ring) / <alpha-value>)',
|
||||
background: 'hsl(var(--background) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--foreground) / <alpha-value>)',
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
||||
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
||||
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
||||
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
||||
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
||||
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)"
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [...fontFamily.sans]
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
typography
|
||||
// ...
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
Loading…
Reference in a new issue