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

View file

@ -3,76 +3,84 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%; --muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 240 3.8% 46.1%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 240 10% 3.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 240 10% 3.9%; --card-foreground: 240 10% 3.9%;
--border: 240 5.9% 90%; --border: 240 5.9% 90%;
--input: 240 5.9% 90%; --input: 240 5.9% 90%;
--primary: 240 5.9% 10%; --primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%; --secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 240 5.9% 10%;
--accent: 240 4.8% 95.9%; --accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%; --accent-foreground: 240 5.9% 10%;
--destructive: 0 72.2% 50.6%; --destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--ring: 240 10% 3.9%; --ring: 240 10% 3.9%;
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { .dark {
--background: 240 10% 3.9%; --background: 240 10% 3.9%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%; --muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%; --popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%; --card: 240 10% 3.9%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
--primary: 0 0% 98%; --primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%; --primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%; --secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 0 0% 98%;
--accent: 240 3.7% 15.9%; --accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--ring: 240 4.9% 83.9%; --ring: 240 4.9% 83.9%;
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
}
html,
body,
body > div {
min-height: 100vh;
margin: 0;
padding: 0;
} }

View file

@ -1,12 +1,15 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8" /> <head>
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
%sveltekit.head% <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head> %sveltekit.head%
<body data-sveltekit-preload-data="hover"> </head>
<div style="display: contents">%sveltekit.body%</div>
</body> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html> </html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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