This commit is contained in:
Cat /dev/Nulo 2024-09-14 11:46:24 -03:00
parent 2f6963e5f5
commit 3c902dbc70
10 changed files with 222 additions and 145 deletions

Binary file not shown.

View file

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

View file

@ -76,3 +76,11 @@
@apply bg-background text-foreground;
}
}
html,
body,
body > div {
min-height: 100vh;
margin: 0;
padding: 0;
}

View file

@ -1,12 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="utf-8" />
<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%
</head>
<body data-sveltekit-preload-data="hover">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</body>
</html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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