From b2844923a99310d09c3654331cf8c10e72944776 Mon Sep 17 00:00:00 2001
From: RobTrb
Date: Mon, 2 Jun 2025 20:59:19 +0200
Subject: [PATCH 01/11] added validation to confirm that the email is in a
correct format and added a book button
---
src/app/booking/[id]/page.jsx | 7 ++++++-
src/components/BookingMovieInfo.jsx | 14 +++++++++++++-
src/components/TicketDeliveryInfo.jsx | 14 ++++++++++----
src/styles/TicketDeliveryInfo.scss | 10 +++++++++-
src/styles/bookingPage.scss | 18 ++++++++++++++++++
5 files changed, 56 insertions(+), 7 deletions(-)
diff --git a/src/app/booking/[id]/page.jsx b/src/app/booking/[id]/page.jsx
index c5f2a69..25e0516 100644
--- a/src/app/booking/[id]/page.jsx
+++ b/src/app/booking/[id]/page.jsx
@@ -12,6 +12,7 @@ export default function bookingPageId({ params }) {
const [nrOfTickets, setNrOfTickets] = useState(0)
const [customerName, setCustomerName] = useState('')
const [customerEmail, setCustomerEmail] = useState('')
+ const [emailCorrectFormat, setEmailCorrectFormat] = useState(null)
const [selectedSeats, setSelectedSeats] = useState([])
useEffect(() => {
@@ -43,7 +44,7 @@ export default function bookingPageId({ params }) {
return (
Biljettbokning
-
+
+
+
)
}
diff --git a/src/components/BookingMovieInfo.jsx b/src/components/BookingMovieInfo.jsx
index 511b77b..ac4756e 100644
--- a/src/components/BookingMovieInfo.jsx
+++ b/src/components/BookingMovieInfo.jsx
@@ -1,7 +1,16 @@
-export default function BookingMovieInfo({ movie }) {
+export default function BookingMovieInfo({ movie, screening }) {
if (!movie || typeof movie !== 'object') {
return Filminformation saknas
}
+ const date = new Date(screening.date)
+ const formatedDate = date.toLocaleString('sv-SE', {
+ day: 'numeric',
+ month: 'short',
+ })
+ const formatedTime = date.toLocaleString('sv-Se', {
+ hour: '2-digit',
+ minute: '2-digit',
+ })
return (
@@ -26,6 +35,9 @@ export default function BookingMovieInfo({ movie }) {
Handling: {movie.plot}
+
+ Tid för visning: {formatedDate} kl: {formatedTime}
+
)
diff --git a/src/components/TicketDeliveryInfo.jsx b/src/components/TicketDeliveryInfo.jsx
index e138998..13005cb 100644
--- a/src/components/TicketDeliveryInfo.jsx
+++ b/src/components/TicketDeliveryInfo.jsx
@@ -5,6 +5,8 @@ export default function TicketDeliveryInfo({
setCustomerName,
customerEmail,
setCustomerEmail,
+ emailCorrectFormat,
+ setEmailCorrectFormat,
}) {
return (
)
diff --git a/src/styles/TicketDeliveryInfo.scss b/src/styles/TicketDeliveryInfo.scss
index f1794a2..ed4b816 100644
--- a/src/styles/TicketDeliveryInfo.scss
+++ b/src/styles/TicketDeliveryInfo.scss
@@ -24,6 +24,14 @@
padding-left: 0.5rem;
}
+input[type='email']:valid {
+ border-color: green;
+}
+
+input[type='email']:invalid:not(:placeholder-shown) {
+ border-color: red;
+}
+
.mainContainer__ticketSelector {
display: flex;
flex-direction: column;
@@ -33,7 +41,7 @@
gap: 20px;
align-items: center;
border-radius: 10px;
- min-width: 230px;
+ text-align: center;
}
.ticketSelector__nrOfTickets {
diff --git a/src/styles/bookingPage.scss b/src/styles/bookingPage.scss
index 817fb7e..6916fb6 100644
--- a/src/styles/bookingPage.scss
+++ b/src/styles/bookingPage.scss
@@ -4,3 +4,21 @@
align-items: center;
width: 100%;
}
+
+.booking__bookBtn {
+ font-size: x-large;
+ padding: 30px;
+ padding-left: 130px;
+ padding-right: 130px;
+ margin-bottom: 20px;
+ border-radius: 10px;
+ background-color: #03658c;
+ border: 3px solid;
+ color: white;
+
+ &:hover {
+ background-color: #52b3d9;
+ color: black;
+ border-color: #03658c;
+ }
+}
From abe4797851ca5885947aaab3519ac1fbd4223c3f Mon Sep 17 00:00:00 2001
From: RobTrb
Date: Tue, 3 Jun 2025 10:49:15 +0200
Subject: [PATCH 02/11] fixed typo in email validator checker
---
src/app/booking/[id]/page.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/booking/[id]/page.jsx b/src/app/booking/[id]/page.jsx
index 25e0516..5e829f9 100644
--- a/src/app/booking/[id]/page.jsx
+++ b/src/app/booking/[id]/page.jsx
@@ -53,7 +53,7 @@ export default function bookingPageId({ params }) {
customerEmail={customerEmail}
setCustomerEmail={setCustomerEmail}
emailCorrectFormat={emailCorrectFormat}
- SetEmailCorrectFormat={setEmailCorrectFormat}
+ setEmailCorrectFormat={setEmailCorrectFormat}
/>
Date: Tue, 3 Jun 2025 13:26:30 +0200
Subject: [PATCH 03/11] added routes and ability to create bookings so that the
booking page now creates and takes to the confirmation page / booking id
---
src/app/api/bookings/route.js | 17 +++++++
src/app/booking-confirmation/[id]/page.jsx | 7 +++
src/app/booking-confirmation/layout.jsx | 3 ++
src/app/booking/[id]/page.jsx | 11 ++++-
src/components/BookingBookBtn.jsx | 55 ++++++++++++++++++++++
src/lib/db/bookingDbService.js | 13 +++++
6 files changed, 104 insertions(+), 2 deletions(-)
create mode 100644 src/app/booking-confirmation/[id]/page.jsx
create mode 100644 src/app/booking-confirmation/layout.jsx
create mode 100644 src/components/BookingBookBtn.jsx
diff --git a/src/app/api/bookings/route.js b/src/app/api/bookings/route.js
index e69de29..3686520 100644
--- a/src/app/api/bookings/route.js
+++ b/src/app/api/bookings/route.js
@@ -0,0 +1,17 @@
+import connectDB from '@/lib/db/connectDB'
+import { NextResponse } from 'next/server'
+import { createBooking } from '@/lib/db/bookingDbService'
+
+export async function POST(request) {
+ try {
+ await connectDB()
+
+ const data = await request.json()
+ const savedBooking = await createBooking(data)
+
+ return NextResponse.json(savedBooking, { status: 201 })
+ } catch (error) {
+ console.error(error)
+ return NextResponse.json({ error: 'failed to create booking' }, { status: 500 })
+ }
+}
diff --git a/src/app/booking-confirmation/[id]/page.jsx b/src/app/booking-confirmation/[id]/page.jsx
new file mode 100644
index 0000000..c379b1b
--- /dev/null
+++ b/src/app/booking-confirmation/[id]/page.jsx
@@ -0,0 +1,7 @@
+export default function BookingConfirmationPageId() {
+ return (
+ <>
+ Tog dig till rätt plats
+ >
+ )
+}
diff --git a/src/app/booking-confirmation/layout.jsx b/src/app/booking-confirmation/layout.jsx
new file mode 100644
index 0000000..1f17ed5
--- /dev/null
+++ b/src/app/booking-confirmation/layout.jsx
@@ -0,0 +1,3 @@
+export default function BookingConfirmationLayout({ children }) {
+ return children
+}
diff --git a/src/app/booking/[id]/page.jsx b/src/app/booking/[id]/page.jsx
index 5e829f9..05294e9 100644
--- a/src/app/booking/[id]/page.jsx
+++ b/src/app/booking/[id]/page.jsx
@@ -4,6 +4,7 @@ import { use, useState, useEffect } from 'react'
import BookingMovieInfo from '../../../components/BookingMovieInfo'
import TicketDeliveryInfo from '../../../components/TicketDeliveryInfo'
import SeatMap from '../../../components/booking/SeatMap'
+import BookingBookBtn from '../../../components/BookingBookBtn'
export default function bookingPageId({ params }) {
const unwrappedParams = use(params)
@@ -61,8 +62,14 @@ export default function bookingPageId({ params }) {
onSelect={setSelectedSeats}
nrOfTickets={nrOfTickets}
/>
-
-
+
)
}
diff --git a/src/components/BookingBookBtn.jsx b/src/components/BookingBookBtn.jsx
new file mode 100644
index 0000000..1c7c910
--- /dev/null
+++ b/src/components/BookingBookBtn.jsx
@@ -0,0 +1,55 @@
+'use-client'
+import { useRouter } from 'next/navigation'
+
+export default function BookingBookBtn({
+ movie,
+ screening,
+ customerName,
+ customerEmail,
+ selectedSeats,
+ emailCorrectFormat,
+}) {
+ const router = useRouter()
+
+ async function handleBooking() {
+ const bookingData = {
+ screening: screening._id,
+ movieTitle: movie.title,
+ roomName: screening.room.name,
+ screeningTime: screening.date,
+ seats: selectedSeats,
+ name: customerName,
+ }
+
+ try {
+ const response = await fetch('/api/bookings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(bookingData),
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json()
+ throw new Error(errorData.error || 'Något gick fel vid bokningen')
+ }
+ const result = await response.json()
+ console.log('Booking successfull', result)
+ router.push(`/booking-confirmation/${result._id}`)
+ } catch (error) {
+ console.error('Error', error)
+ }
+ }
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/src/lib/db/bookingDbService.js b/src/lib/db/bookingDbService.js
index abb15e1..836a5b1 100644
--- a/src/lib/db/bookingDbService.js
+++ b/src/lib/db/bookingDbService.js
@@ -3,3 +3,16 @@ import Booking from '@/lib/db/models/Booking'
export async function deleteBookingsByScreeningId(screeningId) {
return await Booking.deleteMany({ screening: screeningId })
}
+
+export async function createBooking({ screening, movieTitle, roomName, screeningTime, seats, name }) {
+ const newBooking = new Booking({
+ screening,
+ movieTitle,
+ roomName,
+ screeningTime,
+ seats,
+ name,
+ })
+
+ return await newBooking.save()
+}
From 205279007e36db53264f1c1cda2cb6563293a4ba Mon Sep 17 00:00:00 2001
From: RobTrb
Date: Wed, 4 Jun 2025 10:20:37 +0200
Subject: [PATCH 04/11] Completed booking. Added control to stop booking if
paramaters are not met
---
src/app/booking/[id]/page.jsx | 5 ++++-
src/components/BookingBookBtn.jsx | 10 ++++++++--
src/components/BookingMovieInfo.jsx | 3 ++-
src/styles/bookingPage.scss | 10 ++++++++++
4 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/src/app/booking/[id]/page.jsx b/src/app/booking/[id]/page.jsx
index 05294e9..518c118 100644
--- a/src/app/booking/[id]/page.jsx
+++ b/src/app/booking/[id]/page.jsx
@@ -15,6 +15,7 @@ export default function bookingPageId({ params }) {
const [customerEmail, setCustomerEmail] = useState('')
const [emailCorrectFormat, setEmailCorrectFormat] = useState(null)
const [selectedSeats, setSelectedSeats] = useState([])
+ const [bookingInvalidClass, setBookingInvalidClass] = useState('booking_invalid')
useEffect(() => {
async function fetchData() {
@@ -62,13 +63,15 @@ export default function bookingPageId({ params }) {
onSelect={setSelectedSeats}
nrOfTickets={nrOfTickets}
/>
+ Du måste välja lika många platser som valda biljetter
)
diff --git a/src/components/BookingBookBtn.jsx b/src/components/BookingBookBtn.jsx
index 1c7c910..a409684 100644
--- a/src/components/BookingBookBtn.jsx
+++ b/src/components/BookingBookBtn.jsx
@@ -5,9 +5,10 @@ export default function BookingBookBtn({
movie,
screening,
customerName,
- customerEmail,
+ nrOfTickets,
selectedSeats,
emailCorrectFormat,
+ setBookingInvalidClass,
}) {
const router = useRouter()
@@ -45,7 +46,12 @@ export default function BookingBookBtn({
className="booking__bookBtn"
disabled={!emailCorrectFormat}
onClick={() => {
- handleBooking()
+ if (selectedSeats.length < nrOfTickets) {
+ setBookingInvalidClass('booking_invalid active')
+ return
+ } else {
+ handleBooking()
+ }
}}
>
Boka
diff --git a/src/components/BookingMovieInfo.jsx b/src/components/BookingMovieInfo.jsx
index ac4756e..dbc3387 100644
--- a/src/components/BookingMovieInfo.jsx
+++ b/src/components/BookingMovieInfo.jsx
@@ -36,7 +36,8 @@ export default function BookingMovieInfo({ movie, screening }) {
Handling: {movie.plot}
- Tid för visning: {formatedDate} kl: {formatedTime}
+ Tid för visning: {formatedDate} kl: {formatedTime} Salong:
+ {screening.room.name}
diff --git a/src/styles/bookingPage.scss b/src/styles/bookingPage.scss
index 6916fb6..0ab0418 100644
--- a/src/styles/bookingPage.scss
+++ b/src/styles/bookingPage.scss
@@ -22,3 +22,13 @@
border-color: #03658c;
}
}
+
+.booking_invalid {
+ color: red;
+ opacity: 0;
+ user-select: none;
+ transition: opacity 0.3s ease;
+ &.active {
+ opacity: 100;
+ }
+}
From 0d38312eec3607bee018eedabfd66409073e2ac8 Mon Sep 17 00:00:00 2001
From: RobTrb
Date: Wed, 4 Jun 2025 10:29:28 +0200
Subject: [PATCH 05/11] added api route and db functionality to fetch booking
by id
---
src/app/api/bookings/[id]/route.js | 10 ++++++++++
src/app/api/bookings/route.js | 2 +-
src/lib/db/bookingDbService.js | 4 ++++
3 files changed, 15 insertions(+), 1 deletion(-)
create mode 100644 src/app/api/bookings/[id]/route.js
diff --git a/src/app/api/bookings/[id]/route.js b/src/app/api/bookings/[id]/route.js
new file mode 100644
index 0000000..ae863d6
--- /dev/null
+++ b/src/app/api/bookings/[id]/route.js
@@ -0,0 +1,10 @@
+import connectDB from '@/lib/db/connectDB'
+import { NextResponse } from 'next/server'
+import { getBookingById } from '@/lib/db/bookingDbService'
+
+export async function GET(req, { params }) {
+ const { id } = await params
+ await connectDB()
+ const booking = await getBookingById(id)
+ return NextResponse.json(booking)
+}
diff --git a/src/app/api/bookings/route.js b/src/app/api/bookings/route.js
index 3686520..7f62659 100644
--- a/src/app/api/bookings/route.js
+++ b/src/app/api/bookings/route.js
@@ -1,6 +1,6 @@
import connectDB from '@/lib/db/connectDB'
import { NextResponse } from 'next/server'
-import { createBooking } from '@/lib/db/bookingDbService'
+import { createBooking, getBookingById } from '@/lib/db/bookingDbService'
export async function POST(request) {
try {
diff --git a/src/lib/db/bookingDbService.js b/src/lib/db/bookingDbService.js
index 836a5b1..a3aad56 100644
--- a/src/lib/db/bookingDbService.js
+++ b/src/lib/db/bookingDbService.js
@@ -16,3 +16,7 @@ export async function createBooking({ screening, movieTitle, roomName, screening
return await newBooking.save()
}
+
+export async function getBookingById(id) {
+ return await Booking.findById(id)
+}
From ec005582109b4ee9fb0131669dfa44a460487c3c Mon Sep 17 00:00:00 2001
From: RobTrb
Date: Wed, 4 Jun 2025 13:42:42 +0200
Subject: [PATCH 06/11] booking confirmation works as intended and ready for
further testing.
---
package-lock.json | 236 +++++++++++++++++-
package.json | 2 +
src/app/booking-confirmation/[id]/page.jsx | 56 ++++-
src/app/booking/[id]/page.jsx | 14 +-
src/app/booking/page.jsx | 12 +-
.../ConfirmationDetails.jsx | 60 +++++
.../booking-confirmation/QrCodeGenerator.jsx | 10 +
src/styles/ConfirmationDetails.scss | 14 ++
src/styles/booking-confirmationPage.scss | 94 +++++++
src/styles/main.scss | 2 +
10 files changed, 479 insertions(+), 21 deletions(-)
create mode 100644 src/components/booking-confirmation/ConfirmationDetails.jsx
create mode 100644 src/components/booking-confirmation/QrCodeGenerator.jsx
create mode 100644 src/styles/ConfirmationDetails.scss
create mode 100644 src/styles/booking-confirmationPage.scss
diff --git a/package-lock.json b/package-lock.json
index 523a675..8ee9388 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,8 +10,10 @@
"dependencies": {
"dotenv": "^16.5.0",
"formdata-node": "^6.0.3",
+ "html2pdf.js": "^0.10.3",
"mongoose": "^8.15.0",
"next": "15.3.1",
+ "qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@@ -431,7 +433,6 @@
},
"node_modules/@babel/runtime": {
"version": "7.27.1",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1552,11 +1553,25 @@
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/raf": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
+ "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"license": "MIT"
@@ -2076,6 +2091,18 @@
"node": ">= 0.4"
}
},
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "atob": "bin/atob.js"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"dev": true,
@@ -2223,6 +2250,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"dev": true,
@@ -2289,6 +2325,18 @@
"node": ">=16.20.1"
}
},
+ "node_modules/btoa": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+ "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "btoa": "bin/btoa.js"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"dev": true,
@@ -2381,6 +2429,26 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/canvg": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
+ "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@types/raf": "^3.4.0",
+ "core-js": "^3.8.3",
+ "raf": "^3.4.1",
+ "regenerator-runtime": "^0.13.7",
+ "rgbcolor": "^1.0.1",
+ "stackblur-canvas": "^2.0.0",
+ "svg-pathdata": "^6.0.3"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"dev": true,
@@ -2610,6 +2678,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/core-js": {
+ "version": "3.42.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz",
+ "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/create-jest": {
"version": "29.7.0",
"dev": true,
@@ -2660,6 +2740,15 @@
"node": ">= 8"
}
},
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"dev": true,
@@ -2848,6 +2937,16 @@
"license": "MIT",
"peer": true
},
+ "node_modules/dompurify": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
+ "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optional": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dotenv": {
"version": "16.5.0",
"license": "BSD-2-Clause",
@@ -3069,6 +3168,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+ "license": "MIT"
+ },
"node_modules/escalade": {
"version": "3.2.0",
"dev": true,
@@ -3596,6 +3701,12 @@
"bser": "2.1.1"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"dev": true,
@@ -4001,6 +4112,30 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/html2pdf.js": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.10.3.tgz",
+ "integrity": "sha512-RcB1sh8rs5NT3jgbN5zvvTmkmZrsUrxpZ/RI8TMbvuReNZAdJZG5TMfA2TBP6ZXxpXlWf9NB/ciLXVb6W2LbRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es6-promise": "^4.2.5",
+ "html2canvas": "^1.0.0",
+ "jspdf": "^3.0.0"
+ }
+ },
"node_modules/human-signals": {
"version": "2.1.0",
"dev": true,
@@ -5412,6 +5547,24 @@
"node": ">=6"
}
},
+ "node_modules/jspdf": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
+ "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.7",
+ "atob": "^2.1.2",
+ "btoa": "^1.2.1",
+ "fflate": "^0.8.1"
+ },
+ "optionalDependencies": {
+ "canvg": "^3.0.11",
+ "core-js": "^3.6.0",
+ "dompurify": "^3.2.4",
+ "html2canvas": "^1.0.0-rc.5"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"dev": true,
@@ -6492,6 +6645,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"license": "ISC"
@@ -6716,6 +6876,15 @@
],
"license": "MIT"
},
+ "node_modules/qrcode.react": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
+ "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"dev": true,
@@ -6735,6 +6904,16 @@
],
"license": "MIT"
},
+ "node_modules/raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "performance-now": "^2.1.0"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"dev": true,
@@ -6799,6 +6978,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"dev": true,
@@ -6942,6 +7128,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/rgbcolor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+ "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+ "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.8.15"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"dev": true,
@@ -7365,6 +7561,16 @@
"node": ">=8"
}
},
+ "node_modules/stackblur-canvas": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+ "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.1.14"
+ }
+ },
"node_modules/streamsearch": {
"version": "1.1.0",
"engines": {
@@ -7619,6 +7825,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/svg-pathdata": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+ "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/test-exclude": {
"version": "6.0.0",
"dev": true,
@@ -7632,6 +7848,15 @@
"node": ">=8"
}
},
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.13",
"dev": true,
@@ -7957,6 +8182,15 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"dev": true,
diff --git a/package.json b/package.json
index c64ee64..51f5dd9 100644
--- a/package.json
+++ b/package.json
@@ -16,8 +16,10 @@
"dependencies": {
"dotenv": "^16.5.0",
"formdata-node": "^6.0.3",
+ "html2pdf.js": "^0.10.3",
"mongoose": "^8.15.0",
"next": "15.3.1",
+ "qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/src/app/booking-confirmation/[id]/page.jsx b/src/app/booking-confirmation/[id]/page.jsx
index c379b1b..343d3a5 100644
--- a/src/app/booking-confirmation/[id]/page.jsx
+++ b/src/app/booking-confirmation/[id]/page.jsx
@@ -1,7 +1,59 @@
-export default function BookingConfirmationPageId() {
+'use client'
+import { use, useState, useEffect } from 'react'
+import QrCodeGenerator from '@/components/booking-confirmation/QrCodeGenerator'
+import ConfirmationDetails from '@/components/booking-confirmation/ConfirmationDetails'
+
+export default function BookingConfirmationPageId({ params }) {
+ const unwrappedParams = use(params)
+
+ function handleDownloadPdf() {
+ const element = document.querySelector('.booking-confirmation__ticket-wrapper')
+ if (!element) return
+
+ import('html2pdf.js').then((html2pdf) => {
+ html2pdf.default().from(element).save('bokning.pdf')
+ })
+ }
+
+ const [booking, setBooking] = useState(null)
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ const bookingRes = await fetch(`/api/bookings/${unwrappedParams.id}`)
+ const bookingData = await bookingRes.json()
+
+ setBooking(bookingData)
+ } catch (error) {
+ console.error('Error fetching data from API:', error)
+ }
+ }
+
+ fetchData()
+ }, [unwrappedParams.id])
+
+ if (!booking) return Laddar sidan...
+
return (
<>
- Tog dig till rätt plats
+ Bokning genomförd!
+ Biljetten har skickats till din e-post adress.
+ Nedan kan du välja om du vill skriva ut eller ladda ner biljetten
+
+
+
+
+
>
)
}
diff --git a/src/app/booking/[id]/page.jsx b/src/app/booking/[id]/page.jsx
index 518c118..d20a012 100644
--- a/src/app/booking/[id]/page.jsx
+++ b/src/app/booking/[id]/page.jsx
@@ -1,10 +1,10 @@
'use client'
import { use, useState, useEffect } from 'react'
-import BookingMovieInfo from '../../../components/BookingMovieInfo'
-import TicketDeliveryInfo from '../../../components/TicketDeliveryInfo'
-import SeatMap from '../../../components/booking/SeatMap'
-import BookingBookBtn from '../../../components/BookingBookBtn'
+import BookingMovieInfo from '@/components/BookingMovieInfo'
+import TicketDeliveryInfo from '@/components/TicketDeliveryInfo'
+import SeatMap from '@/components/booking/SeatMap'
+import BookingBookBtn from '@/components/BookingBookBtn'
export default function bookingPageId({ params }) {
const unwrappedParams = use(params)
@@ -20,17 +20,17 @@ export default function bookingPageId({ params }) {
useEffect(() => {
async function fetchData() {
try {
- const screeningRes = await fetch(`../api/screenings/${unwrappedParams.id}`)
+ const screeningRes = await fetch(`/api/screenings/${unwrappedParams.id}`)
const screeningData = await screeningRes.json()
- const roomRes = await fetch(`../api/rooms/${screeningData.room}`)
+ const roomRes = await fetch(`/api/rooms/${screeningData.room}`)
const roomData = await roomRes.json()
screeningData.room = roomData
setScreening(screeningData)
- const movieRes = await fetch(`../api/movies/${screeningData.movie}`)
+ const movieRes = await fetch(`/api/movies/${screeningData.movie}`)
const movieData = await movieRes.json()
setMovie(movieData)
} catch (error) {
diff --git a/src/app/booking/page.jsx b/src/app/booking/page.jsx
index 26f83c4..d193cdc 100644
--- a/src/app/booking/page.jsx
+++ b/src/app/booking/page.jsx
@@ -1,17 +1,7 @@
-'use client'
-
-import { useState, useEffect } from 'react'
-import BookingMovieInfo from '../../components/BookingMovieInfo'
-import TicketDeliveryInfo from '../../components/TicketDeliveryInfo'
-
export default function bookingPage() {
- const [movieData, setMovieData] = useState('')
-
return (
-
Biljettbokning
-
-
+ Error 404: Invalid page
)
}
diff --git a/src/components/booking-confirmation/ConfirmationDetails.jsx b/src/components/booking-confirmation/ConfirmationDetails.jsx
new file mode 100644
index 0000000..858bef1
--- /dev/null
+++ b/src/components/booking-confirmation/ConfirmationDetails.jsx
@@ -0,0 +1,60 @@
+export default function ConfirmationDetails({ booking }) {
+ const date = new Date(booking.screeningTime)
+ const formatedDate = date.toLocaleString('sv-SE', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ })
+ const formatedTime = date.toLocaleString('sv-Se', {
+ hour: '2-digit',
+ minute: '2-digit',
+ })
+ return (
+ <>
+
+
+ Namn: {booking.name}
+
+
+ Film: {booking.movieTitle}
+
+
+ Kostnad: {booking.seats.length * 149} kr
+
+
+ Datum: {formatedDate}
+
+
+ Tid: kl:{formatedTime}
+
+
+ Salong: {booking.roomName}
+
+
+
+
+ {booking.seats &&
+ Object.entries(
+ booking.seats.reduce((grouped, seat) => {
+ const { row, seat: seatNumber } = seat
+ if (!grouped[row]) {
+ grouped[row] = []
+ }
+ grouped[row].push(seatNumber)
+ return grouped
+ }, {})
+ ).map(([row, seatNumbers]) => (
+
+
+ Rad: {row}
+
+
+ Plats: {seatNumbers.join(', ')}
+
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/src/components/booking-confirmation/QrCodeGenerator.jsx b/src/components/booking-confirmation/QrCodeGenerator.jsx
new file mode 100644
index 0000000..c488a64
--- /dev/null
+++ b/src/components/booking-confirmation/QrCodeGenerator.jsx
@@ -0,0 +1,10 @@
+import { QRCodeSVG } from 'qrcode.react'
+
+export default function QrCodeGenerator({ id }) {
+ const recievedId = id
+ return (
+ <>
+
+ >
+ )
+}
diff --git a/src/styles/ConfirmationDetails.scss b/src/styles/ConfirmationDetails.scss
new file mode 100644
index 0000000..ca4c629
--- /dev/null
+++ b/src/styles/ConfirmationDetails.scss
@@ -0,0 +1,14 @@
+.confirmationDetails__booking-info {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+
+ span {
+ background-color: white;
+ flex-direction: row;
+ gap: 5px;
+ padding: 5px;
+ padding-right: 10px;
+ border-radius: 10px;
+ }
+}
diff --git a/src/styles/booking-confirmationPage.scss b/src/styles/booking-confirmationPage.scss
new file mode 100644
index 0000000..53f02e6
--- /dev/null
+++ b/src/styles/booking-confirmationPage.scss
@@ -0,0 +1,94 @@
+.booking-confirmation__ticket-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.booking-confirmation__ticket {
+ display: flex;
+ flex-direction: row;
+ gap: 30px;
+ background-color: #03658c;
+ padding: 30px;
+ border-radius: 10px;
+ margin-top: 0px;
+}
+
+.booking-confirmation__title {
+ margin-top: 20px;
+}
+.confirmationDetails__seat-info {
+ display: flex;
+ flex-direction: row;
+}
+
+.confirmationDetails__seats {
+ background-color: white;
+ padding: 5px;
+ border-radius: 10px;
+ height: fit-content;
+
+ p {
+ margin: 2px;
+ }
+}
+
+.booking-confirmation__actions {
+ display: flex;
+ flex-direction: row;
+ gap: 30px;
+ margin-top: 30px;
+ margin-bottom: 30px;
+
+ button {
+ padding: 15px;
+ background-color: #03658c;
+ border: none;
+ color: white;
+ font-size: large;
+ min-width: 111px;
+ border-radius: 10px;
+ box-shadow: 0px 0px 20px rgb(126, 126, 126);
+
+ &:hover {
+ background-color: #52b3d9;
+ color: black;
+ }
+ }
+}
+
+@media screen and (max-width: 800px) {
+ .actions-print {
+ display: none;
+ }
+}
+
+@media print {
+ .pdf-export-content {
+ width: 210mm;
+ min-height: 297mm;
+ padding: 20mm;
+ background: white;
+ color: black;
+ }
+
+ button {
+ display: none;
+ }
+
+ .header__nav {
+ display: none;
+ }
+
+ .footer {
+ display: none;
+ }
+
+ html,
+ body {
+ margin: 0;
+ padding: 0;
+ height: auto;
+ overflow: visible;
+ }
+}
diff --git a/src/styles/main.scss b/src/styles/main.scss
index f00bdcb..8976e2f 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -11,6 +11,8 @@
@use 'bookingPage.scss';
@use 'TicketDeliveryInfo.scss';
@use './booking/BookingIndex.scss';
+@use 'booking-confirmationPage.scss';
+@use 'ConfirmationDetails.scss';
body {
background-image: url('../../public/img/app_background.webp');
From e973880973ca1c45e640f5e73f5cca8c179e04e7 Mon Sep 17 00:00:00 2001
From: RobTrb
Date: Wed, 4 Jun 2025 21:20:21 +0200
Subject: [PATCH 07/11] finished booking confirmation.
---
src/app/api/bookings/route.js | 2 +-
src/app/api/screenings/[id]/route.js | 20 +++++++++++++++++++-
src/components/BookingBookBtn.jsx | 22 ++++++++++++++++++++++
src/lib/db/screeningDbService.js | 8 ++++++++
4 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/src/app/api/bookings/route.js b/src/app/api/bookings/route.js
index 7f62659..3686520 100644
--- a/src/app/api/bookings/route.js
+++ b/src/app/api/bookings/route.js
@@ -1,6 +1,6 @@
import connectDB from '@/lib/db/connectDB'
import { NextResponse } from 'next/server'
-import { createBooking, getBookingById } from '@/lib/db/bookingDbService'
+import { createBooking } from '@/lib/db/bookingDbService'
export async function POST(request) {
try {
diff --git a/src/app/api/screenings/[id]/route.js b/src/app/api/screenings/[id]/route.js
index e341e0b..a4aafa3 100644
--- a/src/app/api/screenings/[id]/route.js
+++ b/src/app/api/screenings/[id]/route.js
@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server'
import connectDB from '@/lib/db/connectDB'
-import { deleteScreeningById, getScreeningById } from '@/lib/db/screeningDbService'
+import { deleteScreeningById, getScreeningById, updateBookedSeats } from '@/lib/db/screeningDbService'
import { requireAdminAccess } from '@/lib/auth/requireAdminAccess'
export async function DELETE(req, context) {
@@ -25,3 +25,21 @@ export async function GET(req, { params }) {
const screening = await getScreeningById(id)
return NextResponse.json(screening)
}
+
+export async function PATCH(req, { params }) {
+ const { id } = await params
+ const { bookedSeats } = await req.json()
+
+ try {
+ await connectDB()
+
+ const updated = await updateBookedSeats(id, bookedSeats)
+
+ if (!updated) {
+ return NextResponse.json({ error: 'Update to booked seats failed' }, { status: 404 })
+ }
+ return NextResponse.json({ success: 'Updated booked seats' }, { status: 200 })
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 })
+ }
+}
diff --git a/src/components/BookingBookBtn.jsx b/src/components/BookingBookBtn.jsx
index a409684..31025ec 100644
--- a/src/components/BookingBookBtn.jsx
+++ b/src/components/BookingBookBtn.jsx
@@ -40,6 +40,27 @@ export default function BookingBookBtn({
console.error('Error', error)
}
}
+
+ async function updateBookedSeats() {
+ try {
+ const res = await fetch(`/api/screenings/${screening._id}`, {
+ method: 'PATCH',
+ headers: { 'Conten-Type': 'application/json' },
+ body: JSON.stringify({
+ bookedSeats: selectedSeats,
+ }),
+ })
+
+ if (!res.ok) {
+ const errorData = await res.json()
+ throw new Error(errorData.error || 'Failed to update booked seats')
+ }
+ const result = await res.json()
+ } catch (err) {
+ throw err
+ }
+ }
+
return (
<>