sighting.Limitations.length === 0)
diff --git a/apps/frontend/src/components/VenueMap.tsx b/apps/frontend/src/components/VenueMap.tsx
index 4fdbad1..7b10057 100644
--- a/apps/frontend/src/components/VenueMap.tsx
+++ b/apps/frontend/src/components/VenueMap.tsx
@@ -26,7 +26,7 @@ export default function VenueMap({ lat, lon, name }: VenueMapProps) {
)}
Opening hours
- {venue.openingHours && venue.openingHours.length > 0 ? (
+ {venue.openingHours?.length || venue.openingHoursNotes?.length ? (
- {venue.openingHours.map((openingHours, index) => (
+ {venue.openingHours?.map((openingHours, index) => (
-
))}
+ {venue.openingHoursNotes?.map((openingHoursNote, index) => (
+ -
+
+ {formatOpeningDays(openingHoursNote.days)}
+
+
+ {openingHoursNote.note}
+
+
+ ))}
) : (
diff --git a/apps/frontend/src/components/__tests__/ViewVenue.test.tsx b/apps/frontend/src/components/__tests__/ViewVenue.test.tsx
index d344c54..5db8770 100644
--- a/apps/frontend/src/components/__tests__/ViewVenue.test.tsx
+++ b/apps/frontend/src/components/__tests__/ViewVenue.test.tsx
@@ -73,6 +73,26 @@ describe('ViewVenue', () => {
expect(screen.getByText('No opening hours listed.')).toBeInTheDocument()
})
+ it('renders an opening-hours note formatted by day', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByText('Sunday')).toBeInTheDocument()
+ expect(
+ screen.getByText('Open during select sports broadcasts.'),
+ ).toBeInTheDocument()
+ })
+
it('formats a price sighting without a millilitre size', () => {
render(
= {}): venue {
lon: 10.3951,
},
openingHours: [makeOpeningHours()],
+ openingHoursNotes: [],
pricesSighted: [makePriceSighting()],
reviews: [makeReview()],
...overrides,
diff --git a/apps/frontend/src/types/search.ts b/apps/frontend/src/types/search.ts
index e3370f3..b953d64 100644
--- a/apps/frontend/src/types/search.ts
+++ b/apps/frontend/src/types/search.ts
@@ -15,6 +15,11 @@ export interface SearchOpeningHours {
closing: string
}
+export interface SearchOpeningHoursNote {
+ days: string[]
+ note: string
+}
+
export interface SearchPriceSighting {
price: number
size: string
@@ -35,6 +40,7 @@ export interface SearchResult {
distanceKm?: number | null
address: string | null
openingHours: SearchOpeningHours[] | null
+ openingHoursNotes?: SearchOpeningHoursNote[] | null
latestPriceSighting: SearchPriceSighting | null
averageRating?: number | null
reviewCount?: number
diff --git a/apps/frontend/src/types/venue.ts b/apps/frontend/src/types/venue.ts
index 155e1e5..4bae5b0 100644
--- a/apps/frontend/src/types/venue.ts
+++ b/apps/frontend/src/types/venue.ts
@@ -11,6 +11,11 @@ export interface VenueOpeningHours {
closing: string
}
+export interface VenueOpeningHoursNote {
+ days: string[]
+ note: string
+}
+
export interface VenuePriceSighting {
price: number
size: string
@@ -37,6 +42,7 @@ export interface venue {
lon: number
city: VenueCity
openingHours: VenueOpeningHours[]
+ openingHoursNotes: VenueOpeningHoursNote[]
pricesSighted?: VenuePriceSighting[]
reviews: VenueReview[]
}
@@ -49,6 +55,7 @@ export interface NearbyVenue {
lon: number
city: { id: string }
openingHours: VenueOpeningHours[]
+ openingHoursNotes: VenueOpeningHoursNote[]
pricesSighted: VenuePriceSighting[]
reviews: { rating: number }[]
}
diff --git a/packages/db/prisma/migrations/20261109120000_add_opening_hours_note/migration.sql b/packages/db/prisma/migrations/20261109120000_add_opening_hours_note/migration.sql
new file mode 100644
index 0000000..818615a
--- /dev/null
+++ b/packages/db/prisma/migrations/20261109120000_add_opening_hours_note/migration.sql
@@ -0,0 +1,3 @@
+-- Add an optional note for venues whose opening hours vary or cannot be
+-- represented as a fixed weekly schedule.
+ALTER TABLE "venue" ADD COLUMN "opening_hours_note" TEXT;
diff --git a/packages/db/prisma/migrations/20261109130000_opening_hours_note_table/migration.sql b/packages/db/prisma/migrations/20261109130000_opening_hours_note_table/migration.sql
new file mode 100644
index 0000000..92a3a6a
--- /dev/null
+++ b/packages/db/prisma/migrations/20261109130000_opening_hours_note_table/migration.sql
@@ -0,0 +1,16 @@
+-- Move opening-hours notes into their own table so each note can reference
+-- the day(s) it applies to.
+CREATE TABLE "opening_hours_note" (
+ "id" UUID NOT NULL DEFAULT uuidv7(),
+ "days" "day"[],
+ "note" TEXT NOT NULL,
+ "venue_id" UUID NOT NULL,
+
+ CONSTRAINT "opening_hours_note_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "opening_hours_note" ADD CONSTRAINT "opening_hours_note_venue_id_fkey" FOREIGN KEY ("venue_id") REFERENCES "venue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AlterTable
+ALTER TABLE "venue" DROP COLUMN "opening_hours_note";
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 092446d..a7b2964 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -49,13 +49,14 @@ model Venue {
lat Float
lon Float
- openingHours OpeningHours[]
+ openingHours OpeningHours[]
+ openingHoursNotes OpeningHoursNote[]
cityId String @map("city_id") @db.Uuid
city City @relation(fields: [cityId], references: [id])
pricesSighted PriceSighting[]
- reviews Review[]
+ reviews Review[]
@@map("venue")
}
@@ -72,6 +73,17 @@ model OpeningHours {
@@map("opening_hours")
}
+model OpeningHoursNote {
+ id String @id @default(dbgenerated("uuidv7()")) @db.Uuid
+ days Day[]
+ note String
+
+ venueId String @map("venue_id") @db.Uuid
+ venue Venue @relation(fields: [venueId], references: [id])
+
+ @@map("opening_hours_note")
+}
+
model City {
id String @id @default(dbgenerated("uuidv7()")) @db.Uuid
name String
@@ -100,14 +112,14 @@ model PriceSighting {
}
model Review {
- id String @id @default(dbgenerated("uuidv7()")) @db.Uuid
- username String
- body String
- rating Int
- timeCreated DateTime @map("time_created") @db.Timestamptz() @default(dbgenerated("now()"))
+ id String @id @default(dbgenerated("uuidv7()")) @db.Uuid
+ username String
+ body String
+ rating Int
+ timeCreated DateTime @default(dbgenerated("now()")) @map("time_created") @db.Timestamptz()
venueId String @map("venue_id") @db.Uuid
- venue Venue @relation(fields: [venueId], references: [id])
+ venue Venue @relation(fields: [venueId], references: [id])
@@map("review")
}
diff --git a/packages/db/prisma/seed.ts b/packages/db/prisma/seed.ts
index a67b1b6..51bab76 100644
--- a/packages/db/prisma/seed.ts
+++ b/packages/db/prisma/seed.ts
@@ -2,17 +2,23 @@ import { prisma } from '../src/client.js'
import { Day, GlassSize, Limitations } from '../src/generated/prisma/client.js'
const REFERENCE_DATE = '2000-01-01'
+const PRICE_DATE = '2026-11-09'
function at(time: string) {
return `${REFERENCE_DATE}T${time}:00+01:00`
}
+function priceAt(time: string) {
+ return `${PRICE_DATE}T${time}:00+01:00`
+}
+
async function main() {
console.log('Seeding database...')
await prisma.review.deleteMany()
await prisma.priceSighting.deleteMany()
await prisma.openingHours.deleteMany()
+ await prisma.openingHoursNote.deleteMany()
await prisma.venue.deleteMany()
await prisma.city.deleteMany()
@@ -25,44 +31,46 @@ async function main() {
create: [
{
name: 'Den Gode Nabo',
- address: 'Øvre Bakklandet 69',
- lat: 63.4295,
- lon: 10.3966,
+ address: 'Øvre Bakklandet 66',
+ lat: 63.42794,
+ lon: 10.40268,
openingHours: {
create: [
{
- days: [Day.MON, Day.TUE, Day.WED, Day.THU, Day.FRI],
+ days: [Day.MON],
+ opening: at('16:00'),
+ closing: at('00:00'),
+ },
+ {
+ days: [Day.TUE, Day.WED],
+ opening: at('16:00'),
+ closing: at('01:00'),
+ },
+ {
+ days: [Day.THU, Day.FRI, Day.SAT],
opening: at('15:00'),
- closing: at('23:00'),
+ closing: at('01:00'),
},
{
- days: [Day.SAT, Day.SUN],
- opening: at('12:00'),
- closing: at('22:00'),
+ days: [Day.SUN],
+ opening: at('15:00'),
+ closing: at('00:00'),
},
],
},
pricesSighted: {
create: [
{
- price: 165,
- size: GlassSize.PINT,
+ price: 169,
+ size: GlassSize.HALF_LITER,
limitation: [],
- timeSighted: at('18:30'),
- },
- {
- price: 139,
- size: GlassSize.PINT,
- limitation: [Limitations.TIME],
- timeStart: at('16:00'),
- timeEnd: at('18:00'),
- timeSighted: at('17:45'),
+ timeSighted: priceAt('18:30'),
},
{
- price: 149,
- size: GlassSize.HALF_LITER,
- limitation: [Limitations.MEMBERSHIP],
- timeSighted: at('19:15'),
+ price: 85,
+ size: GlassSize.QUARTER_LITER,
+ limitation: [],
+ timeSighted: priceAt('18:35'),
},
],
},
@@ -82,139 +90,334 @@ async function main() {
},
},
{
- name: 'Skjenkestua',
- address: 'Nedre Bakklandet 24',
- lat: 63.4292,
- lon: 10.3959,
+ name: 'Studentersamfundet (Daglighallen)',
+ address: 'Elgeseter gate 1',
+ lat: 63.422339,
+ lon: 10.395522,
openingHours: {
create: [
{
- days: [Day.MON, Day.TUE, Day.WED, Day.THU],
- opening: at('16:00'),
- closing: at('23:30'),
+ days: [Day.MON, Day.TUE],
+ opening: at('18:00'),
+ closing: at('23:00'),
},
{
- days: [Day.FRI, Day.SAT],
- opening: at('14:00'),
- closing: at('23:30'),
+ days: [Day.WED, Day.THU],
+ opening: at('19:00'),
+ closing: at('01:00'),
+ },
+ {
+ days: [Day.FRI],
+ opening: at('19:00'),
+ closing: at('02:00'),
+ },
+ {
+ days: [Day.SAT],
+ opening: at('20:00'),
+ closing: at('02:00'),
+ },
+ ],
+ },
+ openingHoursNotes: {
+ create: [
+ {
+ days: [Day.SUN],
+ note: 'Open during select sports broadcasts.',
},
],
},
pricesSighted: {
create: [
{
- price: 149,
+ price: 129,
size: GlassSize.PINT,
- limitation: [],
- timeSighted: at('18:00'),
+ limitation: [Limitations.MEMBERSHIP],
+ timeSighted: priceAt('19:00'),
},
+ ],
+ },
+ reviews: {
+ create: [
{
- price: 89,
- size: GlassSize.QUARTER_LITER,
- limitation: [],
- timeSighted: at('16:45'),
+ username: 'student_samfund',
+ body: 'The round red building is a Trondheim institution. Cheap member prices and a great atmosphere.',
+ rating: 5,
+ },
+ {
+ username: 'edgar_regular',
+ body: 'Solid spot for a pint before a concert. Can get busy on Wednesdays.',
+ rating: 4,
+ },
+ {
+ username: 'utdannet_ol',
+ body: 'Great place to meet fellow students, though the queue can be long on Fridays.',
+ rating: 4,
+ },
+ ],
+ },
+ },
+ {
+ name: 'Three Lions English Pub',
+ address: 'Brattørgata 12B',
+ lat: 63.43409,
+ lon: 10.40114,
+ openingHours: {
+ create: [
+ {
+ days: [Day.MON],
+ opening: at('13:00'),
+ closing: at('00:00'),
+ },
+ {
+ days: [Day.TUE, Day.WED],
+ opening: at('12:00'),
+ closing: at('01:00'),
+ },
+ {
+ days: [Day.THU, Day.FRI, Day.SAT],
+ opening: at('12:00'),
+ closing: at('02:00'),
+ },
+ {
+ days: [Day.SUN],
+ opening: at('12:00'),
+ closing: at('00:00'),
},
],
},
reviews: {
create: [
{
- username: 'cozy_seeker',
- body: 'Intimate little spot with a fireplace. Perfect for a quiet pint.',
+ username: 'matchday_mate',
+ body: 'Best place in town to watch the football. Proper English pub atmosphere.',
+ rating: 5,
+ },
+ {
+ username: 'bitter_og_burger',
+ body: 'Good selection of beers and solid pub grub. Gets packed when Liverpool play.',
rating: 4,
},
+ {
+ username: 'brattora_local',
+ body: 'Friendly staff, but the prices are a bit steep even for Trondheim.',
+ rating: 3,
+ },
],
},
},
{
- name: 'Baklandet Skydsstation',
- address: 'Øvre Bakklandet 33',
- lat: 63.4291,
- lon: 10.3962,
+ name: 'Cafe Dublin',
+ address: 'Kongens gate 15',
+ lat: 63.43039,
+ lon: 10.39259,
openingHours: {
create: [
{
- days: [Day.TUE, Day.WED, Day.THU, Day.FRI, Day.SAT, Day.SUN],
- opening: at('17:00'),
- closing: at('23:30'),
+ days: [Day.MON],
+ opening: at('15:00'),
+ closing: at('00:00'),
+ },
+ {
+ days: [Day.TUE, Day.WED, Day.THU],
+ opening: at('13:00'),
+ closing: at('00:00'),
+ },
+ {
+ days: [Day.FRI],
+ opening: at('13:00'),
+ closing: at('02:00'),
+ },
+ {
+ days: [Day.SAT],
+ opening: at('12:00'),
+ closing: at('02:00'),
+ },
+ {
+ days: [Day.SUN],
+ opening: at('15:00'),
+ closing: at('00:00'),
},
],
},
- pricesSighted: {
+ reviews: {
create: [
{
- price: 155,
- size: GlassSize.PINT,
- limitation: [],
- timeSighted: at('18:20'),
+ username: 'irish_in_trondheim',
+ body: 'Feels like a proper Irish pub. Live music and a great pint.',
+ rating: 5,
},
{
- price: 135,
- size: GlassSize.HALF_LITER,
+ username: 'kongens_gate_regular',
+ body: 'Cosy atmosphere and friendly staff. A good spot for a quiet pint during the week.',
+ rating: 4,
+ },
+ {
+ username: 'quiz_goer',
+ body: 'Fun quiz nights and a decent Guinness. Can get loud later in the evening.',
+ rating: 4,
+ },
+ ],
+ },
+ },
+ {
+ name: 'Sukkerhuset',
+ address: 'Sverres gate 15',
+ lat: 63.42859,
+ lon: 10.38974,
+ openingHours: {
+ create: [
+ {
+ days: [Day.THU],
+ opening: at('18:00'),
+ closing: at('00:00'),
+ },
+ {
+ days: [Day.FRI],
+ opening: at('19:00'),
+ closing: at('01:30'),
+ },
+ ],
+ },
+ pricesSighted: {
+ create: [
+ {
+ price: 89,
+ size: GlassSize.BOX,
limitation: [],
- timeSighted: at('18:40'),
+ timeSighted: priceAt('20:00'),
},
],
},
reviews: {
create: [
{
- username: 'sunday_sipper',
- body: 'Charming venue, but the prices are on the higher side.',
- rating: 3,
+ username: 'studentpub_fan',
+ body: "Cheap box beer and a great student atmosphere. Can't beat it.",
+ rating: 5,
+ },
+ {
+ username: 'kalvskinnet_kid',
+ body: 'A hidden gem for students. Only open two days a week, so plan ahead.',
+ rating: 4,
+ },
+ {
+ username: 'frivillig_med',
+ body: 'Run by volunteers and it shows in the best way. Good music and cheap drinks.',
+ rating: 4,
+ },
+ ],
+ },
+ },
+ {
+ name: 'Mellomveien',
+ address: 'Mellomveien 16',
+ lat: 63.43795,
+ lon: 10.42824,
+ openingHours: {
+ create: [
+ {
+ days: [Day.MON, Day.TUE, Day.WED, Day.THU],
+ opening: at('18:00'),
+ closing: at('00:00'),
+ },
+ {
+ days: [Day.FRI],
+ opening: at('16:00'),
+ closing: at('01:00'),
+ },
+ {
+ days: [Day.SAT],
+ opening: at('13:00'),
+ closing: at('01:00'),
},
{
- username: 'historic_pub_fan',
- body: 'Oldest pub on Bakklandet! A must-visit for the vibes alone.',
+ days: [Day.SUN],
+ opening: at('13:00'),
+ closing: at('00:00'),
+ },
+ ],
+ },
+ reviews: {
+ create: [
+ {
+ username: 'lademoen_local',
+ body: 'Proper neighbourhood pub with friendly regulars and good prices.',
rating: 5,
},
+ {
+ username: 'football_pint',
+ body: 'Great place to watch a match. Simple, unpretentious and cheap.',
+ rating: 4,
+ },
+ {
+ username: 'ostbyen_wanderer',
+ body: 'A bit off the beaten path, but worth the walk for the atmosphere.',
+ rating: 4,
+ },
],
},
},
{
- name: 'Trondheim Torg Pub',
- address: 'Olav Tryggvasons gate 6',
- lat: 63.4302,
- lon: 10.3948,
+ name: 'Kieglekroa',
+ address: 'Kongens gate 30',
+ lat: 63.43122,
+ lon: 10.39078,
openingHours: {
create: [
{
- days: [
- Day.MON,
- Day.TUE,
- Day.WED,
- Day.THU,
- Day.FRI,
- Day.SAT,
- Day.SUN,
- ],
- opening: at('10:00'),
- closing: at('20:00'),
+ days: [Day.MON, Day.TUE, Day.WED],
+ opening: at('15:00'),
+ closing: at('00:00'),
+ },
+ {
+ days: [Day.THU],
+ opening: at('15:00'),
+ closing: at('01:00'),
+ },
+ {
+ days: [Day.FRI],
+ opening: at('15:00'),
+ closing: at('02:00'),
+ },
+ {
+ days: [Day.SAT],
+ opening: at('13:00'),
+ closing: at('02:00'),
},
],
},
pricesSighted: {
create: [
{
- price: 159,
- size: GlassSize.PINT,
+ price: 138,
+ size: GlassSize.QUARTER_LITER,
limitation: [],
- timeSighted: at('12:30'),
+ timeSighted: priceAt('19:30'),
},
{
- price: 129,
+ price: 179,
size: GlassSize.HALF_LITER,
- limitation: [Limitations.MEMBERSHIP],
- timeSighted: at('13:15'),
+ limitation: [],
+ timeSighted: priceAt('19:35'),
},
],
},
reviews: {
create: [
{
- username: 'city_center_local',
- body: 'Convenient location, but nothing special. Fine for a quick one.',
- rating: 3,
+ username: 'holstveita_historian',
+ body: "Trondheim's oldest pub for a reason. Great atmosphere and a proper pint.",
+ rating: 5,
+ },
+ {
+ username: 'sjoelbak_champion',
+ body: "Good beer, good company and free shuffleboard. What's not to like?",
+ rating: 4,
+ },
+ {
+ username: 'midtbyen_local',
+ body: 'Historic pub with a wide beer selection. Sunday closed, so plan accordingly.',
+ rating: 4,
},
],
},
diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts
index c4e320c..0c68b18 100644
--- a/packages/db/src/index.ts
+++ b/packages/db/src/index.ts
@@ -1,6 +1,10 @@
export { getDatabaseUrl } from './database-url.js'
export { prisma } from './client.js'
export { GlassSize, Prisma, PrismaClient } from './generated/prisma/client.js'
-export type { OpeningHours, PriceSighting } from './generated/prisma/client.js'
+export type {
+ OpeningHours,
+ OpeningHoursNote,
+ PriceSighting,
+} from './generated/prisma/client.js'
export type { default as PrismaTypes } from './generated/pothos-types.js'
export { getDatamodel } from './generated/pothos-types.js'