mirror of
https://github.com/catdevnull/preciazo.git
synced 2024-11-25 19:16:19 +00:00
history
This commit is contained in:
parent
4bf1351688
commit
d38b2a8cb0
8 changed files with 89 additions and 25 deletions
1
rust/Cargo.lock
generated
1
rust/Cargo.lock
generated
|
@ -353,6 +353,7 @@ dependencies = [
|
|||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
|
|
@ -9,7 +9,7 @@ edition = "2021"
|
|||
again = "0.1.2"
|
||||
anyhow = "1.0.79"
|
||||
base64 = "0.21.7"
|
||||
chrono = "0.4"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4.15", features = ["derive"] }
|
||||
cron = "0.12.0"
|
||||
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite", "chrono", "json" ] }
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::ValueEnum;
|
||||
use futures::future::join_all;
|
||||
use itertools::Itertools;
|
||||
|
@ -168,6 +175,45 @@ async fn get_best_selling(State(pool): State<SqlitePool>) -> impl IntoResponse {
|
|||
Json(categories_with_products)
|
||||
}
|
||||
|
||||
async fn get_product_history(
|
||||
State(pool): State<SqlitePool>,
|
||||
Path(ean): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
#[derive(sqlx::FromRow, Debug, Serialize)]
|
||||
struct Precio {
|
||||
ean: String,
|
||||
fetched_at: chrono::DateTime<Utc>,
|
||||
precio_centavos: Option<i64>,
|
||||
in_stock: Option<bool>,
|
||||
url: String,
|
||||
name: Option<String>,
|
||||
image_url: Option<String>,
|
||||
}
|
||||
|
||||
let precios = sqlx::query!(
|
||||
"
|
||||
select ean,fetched_at,precio_centavos,in_stock,url,name,image_url from precios
|
||||
where ean = ?
|
||||
order by fetched_at
|
||||
",
|
||||
ean
|
||||
)
|
||||
.map(|r| Precio {
|
||||
ean: r.ean,
|
||||
url: r.url,
|
||||
fetched_at: DateTime::from_timestamp(r.fetched_at, 0).unwrap(),
|
||||
image_url: r.image_url,
|
||||
name: r.name,
|
||||
in_stock: r.in_stock.map(|x| x == 1),
|
||||
precio_centavos: r.precio_centavos,
|
||||
})
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Json(precios)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
@ -205,6 +251,7 @@ async fn main() {
|
|||
.route("/", get(index))
|
||||
.route("/api/healthcheck", get(healthcheck))
|
||||
.route("/api/0/best-selling-products", get(get_best_selling))
|
||||
.route("/api/0/ean/:ean/history", get(get_product_history))
|
||||
.with_state(pool);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
||||
export const API_HOST = import.meta.env.VITE_API_HOST;
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { getDb, schema } from "$lib/server/db";
|
||||
const { precios } = schema;
|
||||
import { z } from "zod";
|
||||
import { zPrecio, type Precio } from "./common";
|
||||
import { API_HOST } from "$lib";
|
||||
|
||||
async function getProductHistory(ean: string) {
|
||||
const res = await fetch(`${API_HOST}/api/0/ean/${ean}/history`);
|
||||
const json = await res.json();
|
||||
return z.array(zPrecio).parse(json);
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const db = await getDb();
|
||||
const q = db
|
||||
.select()
|
||||
.from(precios)
|
||||
.where(eq(precios.ean, params.ean))
|
||||
.orderBy(precios.fetchedAt);
|
||||
const res = await q;
|
||||
const res = await getProductHistory(params.ean);
|
||||
if (res.length === 0) return error(404, "Not Found");
|
||||
|
||||
const meta = res.findLast((p) => p.name);
|
||||
const meta = res.findLast(
|
||||
(p): p is Precio & { name: string; image_url: string } =>
|
||||
!!(p.name && p.image_url),
|
||||
);
|
||||
|
||||
return { precios: res, meta };
|
||||
};
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Supermercado, hosts } from "db-datos/supermercado";
|
||||
import * as schema from "db-datos/schema";
|
||||
import type { PageData } from "./$types";
|
||||
import Chart from "./Chart.svelte";
|
||||
import type { Precio } from "./common";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let urls: Map<Supermercado, schema.Precio>;
|
||||
let urls: Map<Supermercado, Precio>;
|
||||
$: urls = data.precios.reduce((prev, curr) => {
|
||||
const url = new URL(curr.url);
|
||||
const supermercado = hosts[url.hostname];
|
||||
prev.set(supermercado, curr);
|
||||
return prev;
|
||||
}, new Map<Supermercado, schema.Precio>());
|
||||
}, new Map<Supermercado, Precio>());
|
||||
|
||||
const classBySupermercado: { [supermercado in Supermercado]: string } = {
|
||||
[Supermercado.Dia]: "bg-[#d52b1e] focus:ring-[#d52b1e]",
|
||||
|
@ -30,18 +30,18 @@
|
|||
|
||||
{#if data.meta}
|
||||
<h1 class="text-3xl font-bold">{data.meta.name}</h1>
|
||||
<img src={data.meta.imageUrl} alt={data.meta.name} class="max-h-48" />
|
||||
<img src={data.meta.image_url} alt={data.meta.name} class="max-h-48" />
|
||||
<div class="flex gap-2">
|
||||
{#each urls as [supermercado, { url, precioCentavos }]}
|
||||
{#each urls as [supermercado, { url, precio_centavos }]}
|
||||
<a
|
||||
href={url}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
class={`focus:shadow-outline inline-flex flex-col items-center justify-center rounded-md ${classBySupermercado[supermercado]} px-4 py-2 font-medium tracking-wide text-white transition-colors duration-200 hover:bg-opacity-80 focus:outline-none focus:ring-2 focus:ring-offset-2`}
|
||||
>
|
||||
{#if precioCentavos}
|
||||
{#if precio_centavos}
|
||||
<span class="text-lg font-bold"
|
||||
>{formatter.format(precioCentavos / 100)}</span
|
||||
>{formatter.format(precio_centavos / 100)}</span
|
||||
>
|
||||
{/if}
|
||||
<span class="text-sm">{supermercado}</span>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type { Precio } from "db-datos/schema";
|
||||
// import dayjs from "dayjs";
|
||||
import ChartJs from "./ChartJs.svelte";
|
||||
import { hosts, colorBySupermercado } from "db-datos/supermercado";
|
||||
import type { Precio } from "./common";
|
||||
|
||||
export let precios: Precio[];
|
||||
|
||||
|
@ -15,15 +15,15 @@
|
|||
const ps = precios
|
||||
.filter((p) => new URL(p.url!).hostname === host)
|
||||
.filter(
|
||||
(p): p is Precio & { precioCentavos: number } =>
|
||||
p.precioCentavos !== null,
|
||||
(p): p is Precio & { precio_centavos: number } =>
|
||||
p.precio_centavos !== null,
|
||||
);
|
||||
return {
|
||||
label: supermercado,
|
||||
data: [
|
||||
...ps.map((p) => ({
|
||||
x: p.fetchedAt,
|
||||
y: p.precioCentavos / 100,
|
||||
x: p.fetched_at,
|
||||
y: p.precio_centavos / 100,
|
||||
})),
|
||||
// lie
|
||||
// ...ps.map((p) => ({
|
||||
|
|
12
sitio/src/routes/ean/[ean]/common.ts
Normal file
12
sitio/src/routes/ean/[ean]/common.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const zPrecio = z.object({
|
||||
ean: z.string(),
|
||||
fetched_at: z.coerce.date(),
|
||||
precio_centavos: z.number().nullable(),
|
||||
in_stock: z.boolean().nullable(),
|
||||
url: z.string(),
|
||||
name: z.string().nullable(),
|
||||
image_url: z.string().nullable(),
|
||||
});
|
||||
export type Precio = z.infer<typeof zPrecio>;
|
Loading…
Reference in a new issue