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, "when": 1750786732013,
"tag": "0000_noisy_omega_red", "tag": "0000_noisy_omega_red",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1751371464551,
"tag": "0001_magenta_naoko",
"breakpoints": true
} }
] ]
} }

View file

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

View file

@ -6,14 +6,16 @@ export interface Line {
description: string; 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() { export async function getLines() {
const annotation = await fetch( const annotation = await fetch(
"https://amber.festivalfantazie.cz/porady.php", "https://amber.festivalfantazie.cz/porady.php",
{ {
headers: { headers: 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",
},
}, },
); );
@ -68,16 +70,14 @@ export interface Event {
description: string; description: string;
type: string; type: string;
lineId: number; lineId: number;
location?: string; // Optional location field
} }
export async function getScheduleForLine(lineId: number) { export async function getScheduleForLine(lineId: number) {
const response = await fetch( const response = await fetch(
`https://amber.festivalfantazie.cz/program_linie.php?linie=${lineId}`, `https://amber.festivalfantazie.cz/program_linie.php?linie=${lineId}`,
{ {
headers: { headers: 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",
},
}, },
); );
@ -162,8 +162,7 @@ export async function getScheduleForLine(lineId: number) {
"https://amber.festivalfantazie.cz/ajax/anotace.php", "https://amber.festivalfantazie.cz/ajax/anotace.php",
{ {
headers: { headers: {
"User-Agent": ...userAgentHeader,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
referrer: `https://amber.festivalfantazie.cz/program_linie.php?linie=${lineId}`, 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$ = cheerio.load(descriptionHtml);
const description = description$("description").text().trim(); 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({ events.push({
id: eventIdNumber, id: eventIdNumber,
startTime, startTime,
@ -191,6 +209,7 @@ export async function getScheduleForLine(lineId: number) {
description, description,
type: $(td).find("td.program_typ").text().trim(), type: $(td).find("td.program_typ").text().trim(),
lineId, lineId,
location: location || undefined, // Optional location field
}); });
currentTime = new Date(endTime); currentTime = new Date(endTime);

View file

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

View file

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

View file

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