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
236 changes: 235 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
9 changes: 9 additions & 0 deletions src/app/api/bookings/[id]/route.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import connectDB from '@/lib/db/connectDB'
import { NextResponse } from 'next/server'
import { getBookingById } from '@/lib/db/bookingDbService'
import { updateBookingSeats, deleteBookingById } from '@/lib/db/bookingDbService'
import { requireAdminAccess } from '@/lib/auth/requireAdminAccess'

export async function GET(req, { params }) {
const { id } = await params
await connectDB()
const booking = await getBookingById(id)
return NextResponse.json(booking)
}

export async function PATCH(req, context) {
if (requireAdminAccess()) {
return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 })
Expand Down
17 changes: 16 additions & 1 deletion src/app/api/bookings/route.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import connectDB from '@/lib/db/connectDB'
import { NextResponse } from 'next/server'
import { getAllBookings } from '@/lib/db/bookingDbService'
import { getAllBookings, createBooking } from '@/lib/db/bookingDbService'
import { requireAdminAccess } from '@/lib/auth/requireAdminAccess'

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

export async function GET() {
if (requireAdminAccess()) {
return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 })
Expand Down
20 changes: 19 additions & 1 deletion src/app/api/screenings/[id]/route.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 })
}
}
59 changes: 59 additions & 0 deletions src/app/booking-confirmation/[id]/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'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 <h1>Laddar sidan...</h1>

return (
<>
<h1 className="booking-confirmation__title">Bokning genomförd!</h1>
<a>Biljetten har skickats till din e-post adress.</a>
<a>Nedan kan du välja om du vill skriva ut eller ladda ner biljetten</a>
<div className="booking-confirmation__ticket-wrapper">
<h1>Biljett</h1>
<div className="booking-confirmation__ticket">
<QrCodeGenerator id={unwrappedParams.id} />
<ConfirmationDetails booking={booking} />
</div>
</div>
<div className="booking-confirmation__actions">
<button className="actions-print" onClick={() => window.print()}>
Skriv ut
</button>
<button className="actions-download" onClick={handleDownloadPdf}>
Ladda ner
</button>
</div>
</>
)
}
3 changes: 3 additions & 0 deletions src/app/booking-confirmation/layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function BookingConfirmationLayout({ children }) {
return children
}
37 changes: 27 additions & 10 deletions src/app/booking/[id]/page.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +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 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)
Expand All @@ -12,22 +13,24 @@ 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([])
const [bookingInvalidClass, setBookingInvalidClass] = useState('booking_invalid')

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) {
Expand All @@ -43,20 +46,34 @@ export default function bookingPageId({ params }) {
return (
<div className="booking__pageContainer">
<h1>Biljettbokning</h1>
<BookingMovieInfo movie={movie} />
<BookingMovieInfo movie={movie} screening={screening} />
<TicketDeliveryInfo
nrOfTickets={nrOfTickets}
setNrOfTickets={setNrOfTickets}
customerName={customerName}
setCustomerName={setCustomerName}
customerEmail={customerEmail}
setCustomerEmail={setCustomerEmail}
emailCorrectFormat={emailCorrectFormat}
setEmailCorrectFormat={setEmailCorrectFormat}
/>
<SeatMap
<div className="seat-map-scroll-wrapper">
<SeatMap
screening={screening}
selectedSeats={selectedSeats}
onSelect={setSelectedSeats}
nrOfTickets={nrOfTickets}
/>
</div>
<h4 className={bookingInvalidClass}>Du måste välja lika många platser som valda biljetter</h4>
<BookingBookBtn
movie={movie}
screening={screening}
selectedSeats={selectedSeats}
onSelect={setSelectedSeats}
customerName={customerName}
nrOfTickets={nrOfTickets}
selectedSeats={selectedSeats}
emailCorrectFormat={emailCorrectFormat}
setBookingInvalidClass={setBookingInvalidClass}
/>
</div>
)
Expand Down
12 changes: 1 addition & 11 deletions src/app/booking/page.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="booking__pageContainer">
<h1>Biljettbokning</h1>
<BookingMovieInfo />
<TicketDeliveryInfo />
<h1>Error 404: Invalid page</h1>
</div>
)
}
83 changes: 83 additions & 0 deletions src/components/BookingBookBtn.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use-client'
import { useRouter } from 'next/navigation'

export default function BookingBookBtn({
movie,
screening,
customerName,
nrOfTickets,
selectedSeats,
emailCorrectFormat,
setBookingInvalidClass,
}) {
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)
}
}

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 (
<>
<button
className="booking__bookBtn"
disabled={!emailCorrectFormat}
onClick={() => {
if (selectedSeats.length < nrOfTickets) {
setBookingInvalidClass('booking_invalid active')
return
} else {
updateBookedSeats()
handleBooking()
}
}}
>
Boka
</button>
</>
)
}
15 changes: 14 additions & 1 deletion src/components/BookingMovieInfo.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
export default function BookingMovieInfo({ movie }) {
export default function BookingMovieInfo({ movie, screening }) {
if (!movie || typeof movie !== 'object') {
return <div className="booking__movieInfoWrapper">Filminformation saknas</div>
}
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 (
<div className="booking__movieInfoWrapper">
<div className="movieInfoWrapper__posterWrapper">
Expand All @@ -26,6 +35,10 @@ export default function BookingMovieInfo({ movie }) {
<p>
<strong>Handling:</strong> {movie.plot}
</p>
<p>
<strong>Tid för visning:</strong> {formatedDate} kl: {formatedTime} <strong>Salong: </strong>
{screening.room.name}
</p>
</div>
</div>
)
Expand Down
14 changes: 10 additions & 4 deletions src/components/TicketDeliveryInfo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export default function TicketDeliveryInfo({
setCustomerName,
customerEmail,
setCustomerEmail,
emailCorrectFormat,
setEmailCorrectFormat,
}) {
return (
<div className="TicketDeliveryInfo__mainContainer">
Expand All @@ -15,9 +17,14 @@ export default function TicketDeliveryInfo({
<a>Ange din e-postadress:</a>
<input
placeholder="exempel@mail.com"
type="email"
required
value={customerEmail}
onChange={(e) => {
setCustomerEmail(e.target.value)
const email = e.target.value
setCustomerEmail(email)
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
setEmailCorrectFormat(isValid)
}}
></input>
<a>Ange ditt fullständiga namn:</a>
Expand Down Expand Up @@ -53,9 +60,8 @@ export default function TicketDeliveryInfo({
<a>
<strong>Pris: 149kr/st</strong>
</a>
<a>
Summa: {nrOfTickets}st x 149kr = {nrOfTickets * 149}kr
</a>
<a>Totalt: {nrOfTickets * 149}kr</a>
<a>Betalning sker på plats</a>
</div>
</div>
)
Expand Down
Loading