mirror of
https://github.com/catdevnull/preciazo.git
synced 2024-11-22 14:16:19 +00:00
Compare commits
6 commits
b05776a736
...
4db1f0b09e
Author | SHA1 | Date | |
---|---|---|---|
4db1f0b09e | |||
ddb202dc93 | |||
1f9daaa016 | |||
4b2c360e17 | |||
9aff44b3d9 | |||
245dc2eb02 |
12 changed files with 1683 additions and 70 deletions
BIN
sepa/bun.lockb
BIN
sepa/bun.lockb
Binary file not shown.
|
@ -61,6 +61,7 @@ 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
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
@ -73,7 +74,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) 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;
|
datasetId = res[0].id;
|
||||||
|
|
||||||
const comercios: Papa.ParseResult<{ comercio_cuit: string }> = Papa.parse(
|
const comercios: Papa.ParseResult<{ comercio_cuit: string }> = Papa.parse(
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
@ -49,6 +50,7 @@
|
||||||
"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"
|
||||||
|
|
|
@ -1,33 +1,41 @@
|
||||||
<!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"
|
||||||
|
/>
|
||||||
|
|
||||||
<head>
|
<title>Preciazo</title>
|
||||||
<meta charset="utf-8" />
|
<meta
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
name="description"
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
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:image" content="https://preciazo.nulo.in/favicon.png" />
|
||||||
|
<meta property="og:url" content="https://preciazo.nulo.in" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
|
||||||
<title>Preciazo</title>
|
<meta name="twitter:card" content="summary" />
|
||||||
<meta name="description" content="Busca precios de productos en distintas cadenas de supermercados en Argentina." />
|
<meta name="twitter:site" content="@esoesnulo" />
|
||||||
<meta content="index, follow" name="robots" />
|
<meta name="twitter:title" content="Preciazo" />
|
||||||
<meta property="og:title" content="Preciazo" />
|
<meta
|
||||||
<meta property="og:description"
|
name="twitter: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 property="og:url" content={`https://preciazo.nulo.in${data.pathname}`} />
|
<meta name="twitter:image" content="https://preciazo.nulo.in/favicon.png" />
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary" />
|
%sveltekit.head%
|
||||||
<meta name="twitter:site" content="@esoesnulo" />
|
</head>
|
||||||
<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>
|
|
@ -1,14 +1,16 @@
|
||||||
<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 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;
|
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
|
||||||
|
@ -17,17 +19,14 @@
|
||||||
// tap: false
|
// tap: false
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Control panel to display map layers */
|
L.maplibreGL({
|
||||||
// var controlLayers = L.control.layers( null, null, {
|
style: style as any,
|
||||||
// position: "topright",
|
attribution:
|
||||||
// collapsed: false
|
'© <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a>, © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
|
||||||
// }).addTo(map);
|
} as any).addTo(map);
|
||||||
|
|
||||||
// display Carto basemap tiles with light features and labels
|
// display Carto basemap tiles with light features and labels
|
||||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
// L.tileLayer.provider('Stadia.AlidadeSmoothBackground').addTo(map);
|
||||||
attribution:
|
|
||||||
'© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attribution">CARTO</a>'
|
|
||||||
}).addTo(map);
|
|
||||||
mapMap(map, L);
|
mapMap(map, L);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
1539
sepa/sitio2/src/lib/components/map_style.json
Normal file
1539
sepa/sitio2/src/lib/components/map_style.json
Normal file
File diff suppressed because it is too large
Load diff
26
sepa/sitio2/src/routes/+error.svelte
Normal file
26
sepa/sitio2/src/routes/+error.svelte
Normal 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>
|
|
@ -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 () => {
|
export const load: PageServerLoad = async ({ setHeaders }) => {
|
||||||
// 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,6 +9,10 @@ export const load: PageServerLoad = async () => {
|
||||||
WHERE relname = 'precios';
|
WHERE relname = 'precios';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
setHeaders({
|
||||||
|
'Cache-Control': 'public, max-age=600'
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count: count[0].reltuples
|
count: count[0].reltuples
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,8 @@ 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';
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
import { error } from '@sveltejs/kit';
|
||||||
|
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({
|
||||||
|
@ -47,6 +48,14 @@ 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;
|
||||||
|
|
1
sepa/sitio2/src/routes/id_producto/[id]/+page.ts
Normal file
1
sepa/sitio2/src/routes/id_producto/[id]/+page.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const ssr = false;
|
|
@ -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 }) => {
|
export const load: PageServerLoad = async ({ params, setHeaders }) => {
|
||||||
// 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,11 +16,17 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||||
// // '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;
|
productos_marca: string | null;
|
||||||
in_datasets_count: number;
|
in_datasets_count: number;
|
||||||
}>(sql`
|
}>(sql`
|
||||||
SELECT id_producto, productos_descripcion, productos_marca,
|
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);
|
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);
|
||||||
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 = Math.max(
|
||||||
existingProduct.in_datasets_count,
|
existingProduct.in_datasets_count,
|
||||||
producto.in_datasets_count
|
producto.in_datasets_count
|
||||||
|
@ -55,7 +61,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]),
|
marcas: new Set(producto.productos_marca ? [producto.productos_marca] : []),
|
||||||
in_datasets_count: producto.in_datasets_count
|
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',
|
// 'latest_datasets',
|
||||||
// sql`
|
// sql`
|
||||||
// WITH latest_datasets AS (
|
// WITH latest_datasets AS (
|
||||||
|
|
|
@ -8,6 +8,17 @@
|
||||||
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>
|
||||||
|
@ -21,28 +32,31 @@
|
||||||
</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>
|
||||||
{#each data.collapsedProductos as producto}
|
{#if data.collapsedProductos.length === 0}
|
||||||
<a href={`/id_producto/${producto.id_producto}`} class="my-2 block">
|
<p class="my-2 text-gray-600">
|
||||||
<Card.Root class="transition-colors duration-200 hover:bg-gray-100">
|
No se encontraron resultados para "{data.query}". Tené en cuenta que actualmente, el algoritmo
|
||||||
<Card.Header class="block px-3 py-2 pb-0">
|
de búsqueda es muy básico. Probá buscando palabras clave como "alfajor", "ketchup" o
|
||||||
<Badge
|
"lenteja".
|
||||||
>{Array.from(producto.marcas)
|
</p>
|
||||||
.filter((m) => !['sin marca', 'VARIOS'].includes(m))
|
{:else}
|
||||||
.filter((m) => m?.trim().length > 0)
|
{#each data.collapsedProductos as producto}
|
||||||
.join('/')}</Badge
|
<a href={`/id_producto/${producto.id_producto}`} class="my-2 block">
|
||||||
>
|
<Card.Root class="transition-colors duration-200 hover:bg-gray-100">
|
||||||
<Badge variant="outline">en {producto.in_datasets_count} cadenas</Badge>
|
<Card.Header class="block px-3 py-2 pb-0">
|
||||||
<Badge variant="outline">EAN {producto.id_producto}</Badge>
|
<Badge>{parseMarcas(Array.from(producto.marcas)).join('/')}</Badge>
|
||||||
</Card.Header>
|
<Badge variant="outline">en {producto.in_datasets_count} cadenas</Badge>
|
||||||
<Card.Content class="px-3 py-2">
|
<Badge variant="outline">EAN {producto.id_producto}</Badge>
|
||||||
{#each producto.descriptions as description}
|
</Card.Header>
|
||||||
<span>{description}</span>
|
<Card.Content class="px-3 py-2">
|
||||||
{#if description !== producto.descriptions[producto.descriptions.length - 1]}
|
{#each producto.descriptions as description}
|
||||||
<span class="text-gray-500">⋅</span>{' '}
|
<span>{description}</span>
|
||||||
{/if}
|
{#if description !== producto.descriptions[producto.descriptions.length - 1]}
|
||||||
{/each}
|
<span class="text-gray-500">⋅</span>{' '}
|
||||||
</Card.Content>
|
{/if}
|
||||||
</Card.Root>
|
{/each}
|
||||||
</a>
|
</Card.Content>
|
||||||
{/each}
|
</Card.Root>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue