Compare commits

..

6 commits

Author SHA1 Message Date
4db1f0b09e mejores errores 2024-09-14 20:36:08 -03:00
ddb202dc93 importer: idcomercio 2024-09-14 18:18:07 -03:00
1f9daaa016 caché 2024-09-14 18:12:47 -03:00
4b2c360e17 arreglar mapa 2024-09-14 18:04:43 -03:00
9aff44b3d9 malvinas
fixes las malvinas son argentinas #48
2024-09-14 17:58:13 -03:00
245dc2eb02 mejorar búsqueda 2024-09-14 17:23:33 -03:00
12 changed files with 1683 additions and 70 deletions

Binary file not shown.

View file

@ -61,6 +61,7 @@ async function importSucursales(
async function importDataset(dir: string) {
console.log(dir);
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
// {
@ -73,7 +74,7 @@ async function importDataset(dir: string) {
await sql.begin(async (sql) => {
let datasetId: number;
const res =
await sql`insert into datasets (name, date) values (${basename(dir)}, ${date}) returning id`;
await sql`insert into datasets (name, date, id_comercio) values (${basename(dir)}, ${date}, ${id_comercio}) returning id`;
datasetId = res[0].id;
const comercios: Papa.ParseResult<{ comercio_cuit: string }> = Papa.parse(

View file

@ -41,6 +41,7 @@
},
"type": "module",
"dependencies": {
"@maplibre/maplibre-gl-leaflet": "^0.0.22",
"@sentry/sveltekit": "^8.30.0",
"@types/node": "^22.5.0",
"bits-ui": "^0.21.13",
@ -49,6 +50,7 @@
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"lucide-svelte": "^0.441.0",
"maplibre-gl": "^4.7.0",
"postgres": "^3.4.4",
"tailwind-merge": "^2.5.2",
"tailwind-variants": "^0.2.1"

View file

@ -1,33 +1,41 @@
<!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.0, maximum-scale=1.0, user-scalable=no" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Preciazo</title>
<meta name="description" content="Busca precios de productos en distintas cadenas de supermercados en Argentina." />
<meta
name="description"
content="Busca precios de productos en distintas cadenas de supermercados en Argentina."
/>
<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: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${data.pathname}`} />
<meta property="og:url" content="https://preciazo.nulo.in" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<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: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>
</head>
<body data-sveltekit-preload-data="hover">
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</body>
</html>

View file

@ -1,14 +1,16 @@
<script lang="ts">
import { onDestroy, onMount, tick } from 'svelte';
import 'leaflet/dist/leaflet.css';
import type L from 'leaflet';
import style from './map_style.json';
import L from 'leaflet';
import 'maplibre-gl';
import '@maplibre/maplibre-gl-leaflet';
let mapEl: HTMLDivElement;
export let mapMap: (map: L.Map, l: typeof L) => void;
let map: L.Map | undefined;
onMount(async () => {
const L = await import('leaflet');
// Set up initial map center and zoom level
map = L.map(mapEl, {
center: [-34.599722222222, -58.381944444444], // EDIT latitude, longitude to re-center map
@ -17,17 +19,14 @@
// tap: false
});
/* Control panel to display map layers */
// var controlLayers = L.control.layers( null, null, {
// position: "topright",
// collapsed: false
// }).addTo(map);
L.maplibreGL({
style: style as any,
attribution:
'&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>'
} as any).addTo(map);
// display Carto basemap tiles with light features and labels
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);
// L.tileLayer.provider('Stadia.AlidadeSmoothBackground').addTo(map);
mapMap(map, L);
});

File diff suppressed because it is too large Load diff

View file

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

View file

@ -2,7 +2,8 @@ import { db } from '$lib/server/db';
import type { PageServerLoad } from './$types';
import { datasets, precios, sucursales } from '$lib/server/db/schema';
import { and, eq, sql } from 'drizzle-orm';
export const load: PageServerLoad = async ({ params }) => {
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, setHeaders }) => {
const id = BigInt(params.id);
const preciosRes = await db
.select({
@ -47,6 +48,14 @@ ORDER BY d1.id_comercio)
)
.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<
// {
// productos_precio_lista: number;

View file

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

View file

@ -2,7 +2,7 @@ import { db } from '$lib/server/db';
import { sql } from 'drizzle-orm';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
export const load: PageServerLoad = async ({ params, setHeaders }) => {
// const latestDatasetsSq = db.$with('latest_datasets').as(
// db.select({
// id: datasets.id,
@ -16,11 +16,17 @@ export const load: PageServerLoad = async ({ params }) => {
// // '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<{
id_producto: string;
productos_descripcion: string;
productos_marca: string;
productos_marca: string | null;
in_datasets_count: number;
}>(sql`
SELECT id_producto, productos_descripcion, productos_marca,
@ -46,7 +52,7 @@ WHERE p.id_producto = index.id_producto) as in_datasets_count
const existingProduct = acc.find((p) => p.id_producto === producto.id_producto);
if (existingProduct) {
existingProduct.descriptions.push(producto.productos_descripcion);
existingProduct.marcas.add(producto.productos_marca);
if (producto.productos_marca) existingProduct.marcas.add(producto.productos_marca);
existingProduct.in_datasets_count = Math.max(
existingProduct.in_datasets_count,
producto.in_datasets_count
@ -55,7 +61,7 @@ WHERE p.id_producto = index.id_producto) as in_datasets_count
acc.push({
id_producto: producto.id_producto,
descriptions: [producto.productos_descripcion],
marcas: new Set([producto.productos_marca]),
marcas: new Set(producto.productos_marca ? [producto.productos_marca] : []),
in_datasets_count: producto.in_datasets_count
});
}
@ -69,6 +75,10 @@ WHERE p.id_producto = index.id_producto) as in_datasets_count
}>
);
setHeaders({
'Cache-Control': 'public, max-age=600'
});
// 'latest_datasets',
// sql`
// WITH latest_datasets AS (

View file

@ -8,6 +8,17 @@
import { goto } from '$app/navigation';
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>
<svelte:head>
@ -21,16 +32,18 @@
</Button>
<SearchBar />
<h1 class="my-2 text-2xl font-bold">Resultados para "{data.query}"</h1>
{#if data.collapsedProductos.length === 0}
<p class="my-2 text-gray-600">
No se encontraron resultados para "{data.query}". Tené en cuenta que actualmente, el algoritmo
de búsqueda es muy básico. Probá buscando palabras clave como "alfajor", "ketchup" o
"lenteja".
</p>
{:else}
{#each data.collapsedProductos as producto}
<a href={`/id_producto/${producto.id_producto}`} class="my-2 block">
<Card.Root class="transition-colors duration-200 hover:bg-gray-100">
<Card.Header class="block px-3 py-2 pb-0">
<Badge
>{Array.from(producto.marcas)
.filter((m) => !['sin marca', 'VARIOS'].includes(m))
.filter((m) => m?.trim().length > 0)
.join('/')}</Badge
>
<Badge>{parseMarcas(Array.from(producto.marcas)).join('/')}</Badge>
<Badge variant="outline">en {producto.in_datasets_count} cadenas</Badge>
<Badge variant="outline">EAN {producto.id_producto}</Badge>
</Card.Header>
@ -45,4 +58,5 @@
</Card.Root>
</a>
{/each}
{/if}
</div>