Compare commits

..

No commits in common. "4db1f0b09eebb00a3fcf231eedb6baba5db7782e" and "b05776a736837ac91615e10aa3a56c3a3a5783ea" have entirely different histories.

12 changed files with 70 additions and 1683 deletions

Binary file not shown.

View file

@ -61,7 +61,6 @@ async function importSucursales(
async function importDataset(dir: string) { async function importDataset(dir: string) {
console.log(dir); console.log(dir);
const date = basename(dir).match(/(\d{4}-\d{2}-\d{2})/)![1]; const date = basename(dir).match(/(\d{4}-\d{2}-\d{2})/)![1];
const id_comercio = basename(dir).match(/comercio-sepa-(\d+)/)![1];
// TODO: parsear "Ultima actualizacion" al final del CSV y insertarlo en la tabla datasets // TODO: parsear "Ultima actualizacion" al final del CSV y insertarlo en la tabla datasets
// { // {
@ -74,7 +73,7 @@ async function importDataset(dir: string) {
await sql.begin(async (sql) => { await sql.begin(async (sql) => {
let datasetId: number; let datasetId: number;
const res = const res =
await sql`insert into datasets (name, date, id_comercio) values (${basename(dir)}, ${date}, ${id_comercio}) returning id`; await sql`insert into datasets (name, date) values (${basename(dir)}, ${date}) returning id`;
datasetId = res[0].id; datasetId = res[0].id;
const comercios: Papa.ParseResult<{ comercio_cuit: string }> = Papa.parse( const comercios: Papa.ParseResult<{ comercio_cuit: string }> = Papa.parse(

View file

@ -41,7 +41,6 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@maplibre/maplibre-gl-leaflet": "^0.0.22",
"@sentry/sveltekit": "^8.30.0", "@sentry/sveltekit": "^8.30.0",
"@types/node": "^22.5.0", "@types/node": "^22.5.0",
"bits-ui": "^0.21.13", "bits-ui": "^0.21.13",
@ -50,7 +49,6 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"lucide-svelte": "^0.441.0", "lucide-svelte": "^0.441.0",
"maplibre-gl": "^4.7.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

@ -1,41 +1,33 @@
<!doctype html> <!doctype html>
<html lang="en"> <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.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Preciazo</title> <head>
<meta <meta charset="utf-8" />
name="description" <link rel="icon" href="%sveltekit.assets%/favicon.png" />
content="Busca precios de productos en distintas cadenas de supermercados en Argentina." <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
/>
<meta content="index, follow" name="robots" />
<meta property="og:title" content="Preciazo" />
<meta
property="og:description"
content="Busca precios de productos en distintas cadenas de supermercados en Argentina."
/>
<meta property="og:image" content="https://preciazo.nulo.in/favicon.png" />
<meta property="og:url" content="https://preciazo.nulo.in" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" /> <title>Preciazo</title>
<meta name="twitter:site" content="@esoesnulo" /> <meta name="description" content="Busca precios de productos en distintas cadenas de supermercados en Argentina." />
<meta name="twitter:title" content="Preciazo" /> <meta content="index, follow" name="robots" />
<meta <meta property="og:title" content="Preciazo" />
name="twitter:description" <meta property="og:description"
content="Busca precios de productos en distintas cadenas de supermercados en Argentina." content="Busca precios de productos en distintas cadenas de supermercados en Argentina." />
/> <meta property="og:image" content="https://preciazo.nulo.in/favicon.png" />
<meta name="twitter:image" content="https://preciazo.nulo.in/favicon.png" /> <meta property="og:url" content={`https://preciazo.nulo.in${data.pathname}`} />
<meta property="og:type" content="website" />
%sveltekit.head% <meta name="twitter:card" content="summary" />
</head> <meta name="twitter:site" content="@esoesnulo" />
<meta name="twitter:title" content="Preciazo" />
<meta name="twitter:description"
content="Busca precios de productos en distintas cadenas de supermercados en Argentina." />
<meta name="twitter:image" content="https://preciazo.nulo.in/favicon.png" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<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

@ -1,16 +1,14 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount, tick } from 'svelte'; import { onDestroy, onMount, tick } from 'svelte';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import style from './map_style.json'; import type L from 'leaflet';
import L from 'leaflet';
import 'maplibre-gl';
import '@maplibre/maplibre-gl-leaflet';
let mapEl: HTMLDivElement; let mapEl: HTMLDivElement;
export let mapMap: (map: L.Map, l: typeof L) => void; export let mapMap: (map: L.Map, l: typeof L) => void;
let map: L.Map | undefined; let map: L.Map | undefined;
onMount(async () => { onMount(async () => {
const L = await import('leaflet');
// 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
@ -19,14 +17,17 @@
// tap: false // tap: false
}); });
L.maplibreGL({ /* Control panel to display map layers */
style: style as any, // var controlLayers = L.control.layers( null, null, {
attribution: // position: "topright",
'&copy; <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a>, &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>' // collapsed: false
} as any).addTo(map); // }).addTo(map);
// display Carto basemap tiles with light features and labels // display Carto basemap tiles with light features and labels
// L.tileLayer.provider('Stadia.AlidadeSmoothBackground').addTo(map); L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attribution">CARTO</a>'
}).addTo(map);
mapMap(map, L); mapMap(map, L);
}); });

File diff suppressed because it is too large Load diff

View file

@ -1,26 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import Button from '$lib/components/ui/button/button.svelte';
import { ArrowLeftIcon, HomeIcon } from 'lucide-svelte';
</script>
<div class="mx-auto flex max-w-screen-sm flex-col items-center justify-center gap-4 p-4">
<h1 class="flex text-5xl font-bold">Error {$page.status} :(</h1>
<p>{$page.error?.message}</p>
<div class="flex gap-2">
<Button
on:click={() => {
window.history.back();
}}
class="flex items-center gap-2"
>
<ArrowLeftIcon class="h-4 w-4" />
Volver
</Button>
<Button variant="outline" href="/" class="flex items-center gap-2">
<HomeIcon class="h-4 w-4" />
Ir al inicio
</Button>
</div>
</div>

View file

@ -1,7 +1,7 @@
import { sql } from '$lib/server/db'; import { sql } from '$lib/server/db';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ setHeaders }) => { export const load: PageServerLoad = async () => {
// https://www.cybertec-postgresql.com/en/postgresql-count-made-fast/ // https://www.cybertec-postgresql.com/en/postgresql-count-made-fast/
const count = await sql` const count = await sql`
SELECT reltuples::bigint SELECT reltuples::bigint
@ -9,10 +9,6 @@ export const load: PageServerLoad = async ({ setHeaders }) => {
WHERE relname = 'precios'; WHERE relname = 'precios';
`; `;
setHeaders({
'Cache-Control': 'public, max-age=600'
});
return { return {
count: count[0].reltuples count: count[0].reltuples
}; };

View file

@ -2,8 +2,7 @@ import { db } from '$lib/server/db';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { datasets, precios, sucursales } from '$lib/server/db/schema'; import { datasets, precios, sucursales } from '$lib/server/db/schema';
import { and, eq, sql } from 'drizzle-orm'; import { and, eq, sql } from 'drizzle-orm';
import { error } from '@sveltejs/kit'; export const load: PageServerLoad = async ({ params }) => {
export const load: PageServerLoad = async ({ params, setHeaders }) => {
const id = BigInt(params.id); const id = BigInt(params.id);
const preciosRes = await db const preciosRes = await db
.select({ .select({
@ -48,14 +47,6 @@ ORDER BY d1.id_comercio)
) )
.leftJoin(datasets, eq(datasets.id, precios.id_dataset)); .leftJoin(datasets, eq(datasets.id, precios.id_dataset));
setHeaders({
'Cache-Control': 'public, max-age=600'
});
if (preciosRes.length == 0) {
return error(404, `Producto ${params.id} no encontrado`);
}
// const precios = await sql< // const precios = await sql<
// { // {
// productos_precio_lista: number; // productos_precio_lista: number;

View file

@ -1 +0,0 @@
export const ssr = false;

View file

@ -2,7 +2,7 @@ import { db } from '$lib/server/db';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, setHeaders }) => { export const load: PageServerLoad = async ({ params }) => {
// const latestDatasetsSq = db.$with('latest_datasets').as( // const latestDatasetsSq = db.$with('latest_datasets').as(
// db.select({ // db.select({
// id: datasets.id, // id: datasets.id,
@ -16,17 +16,11 @@ export const load: PageServerLoad = async ({ params, setHeaders }) => {
// // 'datasets.id_comercio = latest_datasets.id_comercio AND datasets.date = latest_datasets.max_date' // // 'datasets.id_comercio = latest_datasets.id_comercio AND datasets.date = latest_datasets.max_date'
// )) // ))
const query = params.query const query = params.query;
.replaceAll(/á/giu, 'a')
.replaceAll(/é/giu, 'e')
.replaceAll(/í/giu, 'i')
.replaceAll(/ó/giu, 'o')
.replaceAll(/ú/giu, 'u')
.replaceAll(/ñ/giu, 'n');
const productos = await db.execute<{ const productos = await db.execute<{
id_producto: string; id_producto: string;
productos_descripcion: string; productos_descripcion: string;
productos_marca: string | null; productos_marca: string;
in_datasets_count: number; in_datasets_count: number;
}>(sql` }>(sql`
SELECT id_producto, productos_descripcion, productos_marca, SELECT id_producto, productos_descripcion, productos_marca,
@ -52,7 +46,7 @@ WHERE p.id_producto = index.id_producto) as in_datasets_count
const existingProduct = acc.find((p) => p.id_producto === producto.id_producto); const existingProduct = acc.find((p) => p.id_producto === producto.id_producto);
if (existingProduct) { if (existingProduct) {
existingProduct.descriptions.push(producto.productos_descripcion); existingProduct.descriptions.push(producto.productos_descripcion);
if (producto.productos_marca) existingProduct.marcas.add(producto.productos_marca); existingProduct.marcas.add(producto.productos_marca);
existingProduct.in_datasets_count = Math.max( existingProduct.in_datasets_count = Math.max(
existingProduct.in_datasets_count, existingProduct.in_datasets_count,
producto.in_datasets_count producto.in_datasets_count
@ -61,7 +55,7 @@ WHERE p.id_producto = index.id_producto) as in_datasets_count
acc.push({ acc.push({
id_producto: producto.id_producto, id_producto: producto.id_producto,
descriptions: [producto.productos_descripcion], descriptions: [producto.productos_descripcion],
marcas: new Set(producto.productos_marca ? [producto.productos_marca] : []), marcas: new Set([producto.productos_marca]),
in_datasets_count: producto.in_datasets_count in_datasets_count: producto.in_datasets_count
}); });
} }
@ -75,10 +69,6 @@ WHERE p.id_producto = index.id_producto) as in_datasets_count
}> }>
); );
setHeaders({
'Cache-Control': 'public, max-age=600'
});
// 'latest_datasets', // 'latest_datasets',
// sql` // sql`
// WITH latest_datasets AS ( // WITH latest_datasets AS (

View file

@ -8,17 +8,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
export let data: PageData; export let data: PageData;
function parseMarcas(marcas: readonly string[]) {
const x = marcas
.map((m) => m.trim().replaceAll(/['`´]/g, ''))
.filter((m) => !['sin marca', 'VARIOS'].includes(m))
.filter((m) => m.length > 0);
if (x.length === 0) {
return ['n/a'];
}
return Array.from(new Set(x));
}
</script> </script>
<svelte:head> <svelte:head>
@ -32,31 +21,28 @@
</Button> </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>
{#if data.collapsedProductos.length === 0} {#each data.collapsedProductos as producto}
<p class="my-2 text-gray-600"> <a href={`/id_producto/${producto.id_producto}`} class="my-2 block">
No se encontraron resultados para "{data.query}". Tené en cuenta que actualmente, el algoritmo <Card.Root class="transition-colors duration-200 hover:bg-gray-100">
de búsqueda es muy básico. Probá buscando palabras clave como "alfajor", "ketchup" o <Card.Header class="block px-3 py-2 pb-0">
"lenteja". <Badge
</p> >{Array.from(producto.marcas)
{:else} .filter((m) => !['sin marca', 'VARIOS'].includes(m))
{#each data.collapsedProductos as producto} .filter((m) => m?.trim().length > 0)
<a href={`/id_producto/${producto.id_producto}`} class="my-2 block"> .join('/')}</Badge
<Card.Root class="transition-colors duration-200 hover:bg-gray-100"> >
<Card.Header class="block px-3 py-2 pb-0"> <Badge variant="outline">en {producto.in_datasets_count} cadenas</Badge>
<Badge>{parseMarcas(Array.from(producto.marcas)).join('/')}</Badge> <Badge variant="outline">EAN {producto.id_producto}</Badge>
<Badge variant="outline">en {producto.in_datasets_count} cadenas</Badge> </Card.Header>
<Badge variant="outline">EAN {producto.id_producto}</Badge> <Card.Content class="px-3 py-2">
</Card.Header> {#each producto.descriptions as description}
<Card.Content class="px-3 py-2"> <span>{description}</span>
{#each producto.descriptions as description} {#if description !== producto.descriptions[producto.descriptions.length - 1]}
<span>{description}</span> <span class="text-gray-500"></span>{' '}
{#if description !== producto.descriptions[producto.descriptions.length - 1]} {/if}
<span class="text-gray-500"></span>{' '} {/each}
{/if} </Card.Content>
{/each} </Card.Root>
</Card.Content> </a>
</Card.Root> {/each}
</a>
{/each}
{/if}
</div> </div>