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 (
@@ -15,9 +17,14 @@ export default function TicketDeliveryInfo({ Ange din e-postadress: { - setCustomerEmail(e.target.value) + const email = e.target.value + setCustomerEmail(email) + const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) + setEmailCorrectFormat(isValid) }} > Ange ditt fullständiga namn: @@ -53,9 +60,8 @@ export default function TicketDeliveryInfo({ Pris: 149kr/st - - Summa: {nrOfTickets}st x 149kr = {nrOfTickets * 149}kr - + Totalt: {nrOfTickets * 149}kr + Betalning sker på plats
) 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 +
+

Biljett

+
+ + +
+
+
+ + +
) } 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 ( <>