feat: show event location

This commit is contained in:
Matej Stieranka 2025-07-01 14:12:48 +02:00
parent 472347a365
commit f727ade539
8 changed files with 191 additions and 25 deletions

View file

@ -0,0 +1 @@
ALTER TABLE "eventsTable" ADD COLUMN "location" text;

View file

@ -0,0 +1,131 @@
{
"id": "f3f30a9d-d72d-4e62-86b4-031282707007",
"prevId": "c0102a7e-5d09-41ca-88d1-c66f2f299e65",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.eventsTable": {
"name": "eventsTable",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"startTime": {
"name": "startTime",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"endTime": {
"name": "endTime",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "text",
"primaryKey": false,
"notNull": false
},
"lineId": {
"name": "lineId",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"eventsTable_lineId_linesTable_id_fk": {
"name": "eventsTable_lineId_linesTable_id_fk",
"tableFrom": "eventsTable",
"tableTo": "linesTable",
"columnsFrom": [
"lineId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.linesTable": {
"name": "linesTable",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -8,6 +8,13 @@
"when": 1750786732013,
"tag": "0000_noisy_omega_red",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1751371464551,
"tag": "0001_magenta_naoko",
"breakpoints": true
}
]
}

View file

@ -8,7 +8,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"push-db": "drizzle-kit push"
"db-generate": "drizzle-kit generate"
},
"dependencies": {
"@electric-sql/pglite": "^0.3.3",

View file

@ -6,14 +6,16 @@ export interface Line {
description: string;
}
const userAgentHeader = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
};
export async function getLines() {
const annotation = await fetch(
"https://amber.festivalfantazie.cz/porady.php",
{
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
},
headers: userAgentHeader,
},
);
@ -68,16 +70,14 @@ export interface Event {
description: string;
type: string;
lineId: number;
location?: string; // Optional location field
}
export async function getScheduleForLine(lineId: number) {
const response = await fetch(
`https://amber.festivalfantazie.cz/program_linie.php?linie=${lineId}`,
{
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
},
headers: userAgentHeader,
},
);
@ -162,8 +162,7 @@ export async function getScheduleForLine(lineId: number) {
"https://amber.festivalfantazie.cz/ajax/anotace.php",
{
headers: {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
...userAgentHeader,
"Content-Type": "application/x-www-form-urlencoded",
},
referrer: `https://amber.festivalfantazie.cz/program_linie.php?linie=${lineId}`,
@ -182,6 +181,25 @@ export async function getScheduleForLine(lineId: number) {
const description$ = cheerio.load(descriptionHtml);
const description = description$("description").text().trim();
const locationRes = await fetch(
`https://app.festivalfantazie.cz/program-detail/${eventIdNumber}`,
{
headers: userAgentHeader,
},
);
if (!locationRes.ok) {
throw new Error(
`Failed to fetch location for event ID ${eventIdNumber}`,
);
}
const locationHtml = await locationRes.text();
const location$ = cheerio.load(locationHtml);
const location = location$("div.row.mb-5 > div.col-6.mb-2:nth-of-type(2)")
.text()
.trim();
events.push({
id: eventIdNumber,
startTime,
@ -191,6 +209,7 @@ export async function getScheduleForLine(lineId: number) {
description,
type: $(td).find("td.program_typ").text().trim(),
lineId,
location: location || undefined, // Optional location field
});
currentTime = new Date(endTime);

View file

@ -2,10 +2,7 @@ import type { Event } from "@/common/parser";
import { isEventFavorite } from "@/common/utils";
import { Button } from "./ui/button";
import { toggleFavoriteEvent } from "@/app/actions";
import {
HeartMinusIcon,
HeartPlusIcon,
} from "lucide-react";
import { HeartMinusIcon, HeartPlusIcon } from "lucide-react";
import { getLineById } from "@/db";
import { Badge } from "./ui/badge";
import Link from "next/link";
@ -24,8 +21,15 @@ export async function EventCard({
return (
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-sm w-full">
<div className="flex items-center justify-between gap-2">
<h3 className="text-lg font-semibold">{event.title}</h3>
<div className="flex items-start justify-between gap-2">
<div>
<h3 className="text-lg font-semibold">{event.title}</h3>
{line && (
<Badge asChild variant="outline" className="my-2">
<Link href={`/line/${line.id}`}>{line.name}</Link>
</Badge>
)}
</div>
<form action={toggleFavoriteEvent}>
<input
type="number"
@ -34,23 +38,21 @@ export async function EventCard({
hidden
readOnly
/>
<Button variant={isFavorite ? "default" : "secondary"} className="cursor-pointer">
<Button
variant={isFavorite ? "default" : "secondary"}
className="cursor-pointer"
>
{isFavorite ? <HeartMinusIcon /> : <HeartPlusIcon />}
</Button>
</form>
</div>
{line && (
<Badge asChild variant="outline" className='my-2'>
<Link href={`/line/${line.id}`}>{line.name}</Link>
</Badge>
)}
<h6 className="text-xs text-gray-600 dark:text-gray-400">{event.name}</h6>
<h5 className="text-md text-gray-500 dark:text-gray-300">
{showDate
? `${event.startTime.toLocaleDateString(["cs-CZ"], {
month: "numeric",
day: "numeric",
weekday: 'short',
weekday: "short",
})} `
: ""}
{event.startTime.toLocaleTimeString(["cs-CZ"], {
@ -65,6 +67,11 @@ export async function EventCard({
hour12: false,
})}
</h5>
{event.location && (
<h6 className="text-xs text-gray-500 dark:text-gray-300">
{event.location}
</h6>
)}
<p className="text-sm text-gray-600 dark:text-gray-400">
{event.description}
</p>

View file

@ -1,4 +1,4 @@
import { Event } from "@/common/parser";
import type { Event } from "@/common/parser";
import { EventCard } from "./EventCard";
export function EventList({

View file

@ -14,6 +14,7 @@ export const eventsTable = pgTable("eventsTable", {
title: text().notNull(),
description: text().notNull(),
type: text().notNull(),
location: text(), // Optional location field
lineId: integer()
.notNull()
.references(() => linesTable.id, {