Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/backend/src/schema/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { GlassSize, Prisma, prisma } from '@t09-project-2/db'
import type {
OpeningHours,
OpeningHoursNote,
PriceSighting,
} from '@t09-project-2/db'
import type { OpeningHours, PriceSighting } from '@t09-project-2/db'
import { GraphQLError } from 'graphql'

Expand All @@ -14,6 +19,7 @@ builder.prismaObject('Venue', {
lat: t.exposeFloat('lat'),
lon: t.exposeFloat('lon'),
openingHours: t.relation('openingHours'),
openingHoursNotes: t.relation('openingHoursNotes'),
city: t.relation('city'),
pricesSighted: t.relation('pricesSighted'),
reviews: t.relation('reviews'),
Expand All @@ -28,6 +34,13 @@ const openingHoursRef = builder.prismaObject('OpeningHours', {
}),
})

const openingHoursNoteRef = builder.prismaObject('OpeningHoursNote', {
fields: (t) => ({
days: t.exposeStringList('days'),
note: t.exposeString('note'),
}),
})

builder.prismaObject('City', {
fields: (t) => ({
id: t.exposeString('id'),
Expand Down Expand Up @@ -89,6 +102,7 @@ type SearchResultItem = {
distanceKm: number | null
address: string | null
openingHours: OpeningHours[] | null
openingHoursNotes: OpeningHoursNote[] | null
latestPriceSighting: PriceSighting | null
averageRating: number | null
reviewCount: number
Expand All @@ -110,6 +124,10 @@ const searchResultRef = builder
type: [openingHoursRef],
nullable: { list: true, items: false },
}),
openingHoursNotes: t.expose('openingHoursNotes', {
type: [openingHoursNoteRef],
nullable: { list: true, items: false },
}),
latestPriceSighting: t.expose('latestPriceSighting', {
type: priceSightingRef,
nullable: true,
Expand Down Expand Up @@ -555,6 +573,7 @@ builder.queryType({
where: { id: { in: rows.map((row) => row.id) } },
include: {
openingHours: true,
openingHoursNotes: true,
pricesSighted: true,
},
})
Expand Down Expand Up @@ -585,6 +604,7 @@ builder.queryType({
distanceKm: row.distance_km,
address: venue.address,
openingHours: venue.openingHours,
openingHoursNotes: venue.openingHoursNotes,
latestPriceSighting,
averageRating: row.average_rating,
reviewCount: row.review_count,
Expand All @@ -604,6 +624,7 @@ builder.queryType({
distanceKm: null,
address: null,
openingHours: null,
openingHoursNotes: null,
latestPriceSighting: null,
averageRating: null,
reviewCount: 0,
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/ClientVenueMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function ClientVenueMap(props: VenueMapProps) {
const ready = mounted && LazyVenueMap != null

return (
<div className="relative h-64 w-full">
<div className="relative h-full w-full">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden rounded-xl bg-(--surface-strong)"
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/components/SearchBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const SEARCH_QUERY = `
opening
closing
}
openingHoursNotes {
days
note
}
latestPriceSighting {
price
size
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/components/VenueCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function venueToSearchResult(current: venue): SearchResult {
lon: current.lon,
address: current.address,
openingHours: current.openingHours,
openingHoursNotes: current.openingHoursNotes,
latestPriceSighting:
current.pricesSighted
?.filter((sighting) => sighting.Limitations.length === 0)
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/VenueMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function VenueMap({ lat, lon, name }: VenueMapProps) {
<MapContainer
center={[lat, lon]}
zoom={16}
className="h-64 w-full rounded-xl"
className="h-full w-full rounded-xl"
scrollWheelZoom={false}
dragging={false}
doubleClickZoom={false}
Expand Down
17 changes: 15 additions & 2 deletions apps/frontend/src/components/ViewVenue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ export function ViewVenue({ venue }: { venue: SearchResult }) {
</p>
)}
<h3 className="mt-4 text-sm font-bold">Opening hours</h3>
{venue.openingHours && venue.openingHours.length > 0 ? (
{venue.openingHours?.length || venue.openingHoursNotes?.length ? (
<ul className="mt-1">
{venue.openingHours.map((openingHours, index) => (
{venue.openingHours?.map((openingHours, index) => (
<li
key={index}
className="flex items-baseline justify-between gap-4 py-2 text-sm"
Expand All @@ -55,6 +55,19 @@ export function ViewVenue({ venue }: { venue: SearchResult }) {
</span>
</li>
))}
{venue.openingHoursNotes?.map((openingHoursNote, index) => (
<li
key={`note-${index}`}
className="flex items-baseline justify-between gap-4 py-2 text-sm"
>
<span className="font-semibold">
{formatOpeningDays(openingHoursNote.days)}
</span>
<span className="text-(--sea-ink-soft)">
{openingHoursNote.note}
</span>
</li>
))}
</ul>
) : (
<p className="mt-1 text-sm text-(--sea-ink-soft)">
Expand Down
20 changes: 20 additions & 0 deletions apps/frontend/src/components/__tests__/ViewVenue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,26 @@ describe('ViewVenue', () => {
expect(screen.getByText('No opening hours listed.')).toBeInTheDocument()
})

it('renders an opening-hours note formatted by day', () => {
render(
<ViewVenue
venue={makeSearchResult({
openingHoursNotes: [
{
days: ['SUN'],
note: 'Open during select sports broadcasts.',
},
],
})}
/>,
)

expect(screen.getByText('Sunday')).toBeInTheDocument()
expect(
screen.getByText('Open during select sports broadcasts.'),
).toBeInTheDocument()
})

it('formats a price sighting without a millilitre size', () => {
render(
<ViewVenue
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/graphql/venues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export const GET_VENUE = `
opening
closing
}
openingHoursNotes {
days
note
}
pricesSighted {
price
size
Expand Down
5 changes: 5 additions & 0 deletions apps/frontend/src/lib/format-opening-hours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface OpeningHoursEntry {
closing: string
}

export interface OpeningHoursNoteEntry {
days: string[]
note: string
}

/** Compact one-line summary, e.g. "Weekdays 14:00–02:00 · Weekends 12:00–03:00". */
export function formatOpeningHours(entries: OpeningHoursEntry[]): string {
return entries
Expand Down
5 changes: 5 additions & 0 deletions apps/frontend/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const NEARBY_QUERY = `
opening
closing
}
openingHoursNotes {
days
note
}
pricesSighted {
price
size
Expand Down Expand Up @@ -112,6 +116,7 @@ function toSearchResult(venue: NearbyVenue): SearchResult {
lon: venue.lon,
address: venue.address,
openingHours: venue.openingHours,
openingHoursNotes: venue.openingHoursNotes,
latestPriceSighting: latestPriceSighting(venue.pricesSighted),
averageRating: averageRating(venue.reviews),
reviewCount: venue.reviews.length,
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function makeSearchResult(
lon: 10.3957,
address: 'Elgeseter gate 1',
openingHours: [makeOpeningHours()],
openingHoursNotes: [],
latestPriceSighting: makePriceSighting(),
averageRating: 4.5,
reviewCount: 2,
Expand Down Expand Up @@ -75,6 +76,7 @@ export function makeVenue(overrides: Partial<venue> = {}): venue {
lon: 10.3951,
},
openingHours: [makeOpeningHours()],
openingHoursNotes: [],
pricesSighted: [makePriceSighting()],
reviews: [makeReview()],
...overrides,
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/src/types/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export interface SearchOpeningHours {
closing: string
}

export interface SearchOpeningHoursNote {
days: string[]
note: string
}

export interface SearchPriceSighting {
price: number
size: string
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions apps/frontend/src/types/venue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface VenueOpeningHours {
closing: string
}

export interface VenueOpeningHoursNote {
days: string[]
note: string
}

export interface VenuePriceSighting {
price: number
size: string
Expand All @@ -37,6 +42,7 @@ export interface venue {
lon: number
city: VenueCity
openingHours: VenueOpeningHours[]
openingHoursNotes: VenueOpeningHoursNote[]
pricesSighted?: VenuePriceSighting[]
reviews: VenueReview[]
}
Expand All @@ -49,6 +55,7 @@ export interface NearbyVenue {
lon: number
city: { id: string }
openingHours: VenueOpeningHours[]
openingHoursNotes: VenueOpeningHoursNote[]
pricesSighted: VenuePriceSighting[]
reviews: { rating: number }[]
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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";
28 changes: 20 additions & 8 deletions packages/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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
Expand Down Expand Up @@ -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")
}
Loading
Loading