From 8fd49781f7c0a200a0ef6fda11dfe10ba8617a53 Mon Sep 17 00:00:00 2001 From: hkarimn Date: Tue, 5 Nov 2024 14:55:50 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=C3=A4ndrat=20fr=C3=A5n=20SEK=20till=20KR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front-end/src/components/BookingPage/BookingPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/src/components/BookingPage/BookingPage.tsx b/front-end/src/components/BookingPage/BookingPage.tsx index 3ef99d8..d42d7ef 100644 --- a/front-end/src/components/BookingPage/BookingPage.tsx +++ b/front-end/src/components/BookingPage/BookingPage.tsx @@ -450,7 +450,7 @@ const BookingPage: React.FC = ({ showtimeId }) => {

Att betala: - {totalAmount} SEK + {totalAmount} KR

From 670d35aab511872c13233fb880e2d1846a56c061 Mon Sep 17 00:00:00 2001 From: hkarimn Date: Fri, 8 Nov 2024 15:01:51 +0100 Subject: [PATCH 2/2] fixed the img in bookingpage --- .gitignore | 1 - .vscode/settings.json | 8 + back-end/controllers/authController.js | 7 +- back-end/controllers/userController.js | 32 +- back-end/index.js | 70 +- front-end/src/App.tsx | 22 +- front-end/src/UserContext.tsx | 55 +- .../components/BookingPage/BookingPage.scss | 689 +++++++++--------- .../components/BookingPage/BookingPage.tsx | 508 ++++++++----- .../ScheduleSection/ScheduleSection.scss | 35 +- front-end/src/layout/Header/Header.scss | 86 ++- front-end/src/layout/Header/Header.tsx | 143 +++- front-end/src/views/modals/LoginModal.scss | 132 ++-- front-end/src/views/modals/LoginModal.tsx | 231 ++++-- front-end/vite.config.ts | 16 +- package.json | 5 +- 16 files changed, 1251 insertions(+), 789 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 64177a1..b5fd6b9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ dist-ssr *.local # Editor directories and files -.vscode/* !.vscode/extensions.json .idea .DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e7625e6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.tabSize": 2, + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.insertSpaces": true +} diff --git a/back-end/controllers/authController.js b/back-end/controllers/authController.js index fc85b4c..fd0d67b 100644 --- a/back-end/controllers/authController.js +++ b/back-end/controllers/authController.js @@ -34,7 +34,12 @@ export const userLogin = async (req, res) => { if (!token) { return res.status(500).json({ error: "Failed to generate token" }); } - res.cookie("token", token, { httpOnly: true }); + res.cookie("token", token, { + httpOnly: true, // Prevents JavaScript access + secure: false, // Set to true in production (over HTTPS) + sameSite: 'Lax', // CSRF protection + maxAge: 60 * 60 * 1000 // 1 hour + }); const { password: _, ...userWithoutPassword } = user.toObject(); res.status(200).json({ message: "User logged in successfully", user: userWithoutPassword }); } catch (error) { diff --git a/back-end/controllers/userController.js b/back-end/controllers/userController.js index 71d606b..98d433c 100644 --- a/back-end/controllers/userController.js +++ b/back-end/controllers/userController.js @@ -179,22 +179,24 @@ export const removeTicket = async (req, res) => { } } -export const getUserInfo = async (req, res) => { - try { - const user = await User.findById(req.user._id).populate("bookings").exec(); - if (!user) { - return res.status(404).json({ error: 'User not found' }); + export const getUserInfo = async (req, res) => { + try { + const user = await User.findById(req.user._id).populate("bookings").exec(); + console.log("User info endpoint hit"); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + // Remove the password field from the user object + const { password, ...userWithoutPassword } = user.toObject(); + + res.status(200).json({user:userWithoutPassword}); + console.log("User info sent"); + } catch (error) { + console.error('Error fetching user info:', error); // Log the error for debugging + res.status(500).json({ error: 'Server error' }); } - - // Remove the password field from the user object - const { password, ...userWithoutPassword } = user.toObject(); - - res.status(200).json(userWithoutPassword); - } catch (error) { - console.error('Error fetching user info:', error); // Log the error for debugging - res.status(500).json({ error: 'Server error' }); - } -}; + }; export const updateProfile = async (req, res) => { try { diff --git a/back-end/index.js b/back-end/index.js index a21f0f3..64f4d17 100644 --- a/back-end/index.js +++ b/back-end/index.js @@ -9,9 +9,14 @@ import movierouter from "./routes/movie.js"; import userRouter from "./routes/user.js"; import showtimeRouter from "./routes/showtime.js"; import ticketRouter from "./routes/ticket.js"; +import { Server } from "socket.io"; +import http from "http"; +import Showtime from "./models/Showtime.js"; + dotenv.config(); const app = express(); + app.use(bodyParser.json()); app.use(cookieParser()); app.use(express.json()); @@ -22,10 +27,71 @@ app.use("/api/movie", movierouter); app.use("/api/user", userRouter); app.use("/api/showtime", showtimeRouter); app.use("/api/ticket", ticketRouter); -app.listen(process.env.PORT || 5001, () => { + +// Skapa HTTP-server och Socket.io-server +const server = http.createServer(app); +const io = new Server(server); + +// Socket.io-anslutningar +io.on("connection", (socket) => { + console.log("A user connected"); + + socket.on("book-seat", async (seatId, showtimeId) => { + try { + // Uppdatera databasen och signalera till alla anslutna klienter + await updateSeatStatus(seatId, showtimeId); + io.emit("seat-booked", seatId); + } catch (error) { + console.error("Error booking seat:", error); + socket.emit("booking-error", { message: "Failed to book seat" }); + } + }); + + socket.on("disconnect", () => { + console.log("A user disconnected"); + }); +}); + +async function updateSeatStatus(seatId, showtimeId) { + try { + // Uppdatera platsens status i Showtime-dokumentet + const updatedShowtime = await Showtime.findOneAndUpdate( + { + _id: showtimeId, + "seats.seat": seatId, + }, + { + $set: { + "seats.$.isBooked": true, + }, + }, + { new: true } + ); + + if (!updatedShowtime) { + throw new Error("Showtime or seat not found"); + } + + // Find the updated seat object + const updatedSeat = updatedShowtime.seats.find( + (seat) => seat.seat.toString() === seatId + ); + + // Emit a 'seat-status-updated' event with the updated seat object + io.emit("seat-status-updated", updatedSeat); + + return updatedSeat; + } catch (error) { + console.error("Error updating seat status:", error); + throw error; + } +} + +// Starta servern +server.listen(process.env.PORT, () => { try { connectDB(); - console.log("Server started at", process.env.PORT || 5001); + console.log("Server started at", process.env.PORT); } catch (error) { console.error("Server failed to start"); process.exit(1); diff --git a/front-end/src/App.tsx b/front-end/src/App.tsx index dbd7455..37626d0 100644 --- a/front-end/src/App.tsx +++ b/front-end/src/App.tsx @@ -1,9 +1,9 @@ -import React, { useRef, useState } from 'react' -import { BrowserRouter as Router } from 'react-router-dom' -import './App.css' -import Footer from './layout/Footer/Footer' -import Header from './layout/Header/Header' -import Main from './layout/Main/Main' +import React, { useRef, useState } from "react"; +import { BrowserRouter as Router } from "react-router-dom"; +import "./App.css"; +import Footer from "./layout/Footer/Footer"; +import Header from "./layout/Header/Header"; +import Main from "./layout/Main/Main"; const App: React.FC = () => { const scheduleRef = useRef(null); @@ -13,7 +13,7 @@ const App: React.FC = () => { const newDate = new Date(); newDate.setDate(newDate.getDate() + daysAhead); setSelectedDate(newDate); - scheduleRef.current?.scrollIntoView({ behavior: 'smooth' }); + scheduleRef.current?.scrollIntoView({ behavior: "smooth" }); }; return ( @@ -21,18 +21,18 @@ const App: React.FC = () => {
-
+
-
+
-
+
); }; -export default App +export default App; diff --git a/front-end/src/UserContext.tsx b/front-end/src/UserContext.tsx index 330004e..cb94c36 100644 --- a/front-end/src/UserContext.tsx +++ b/front-end/src/UserContext.tsx @@ -1,29 +1,52 @@ -import { createContext, useState, Dispatch, SetStateAction, ReactNode } from 'react'; - +import { + createContext, + useState, + Dispatch, + SetStateAction, + ReactNode, + useEffect, +} from "react"; +import { useCookies } from "react-cookie"; interface User { - name: string; - email: string; - // Add other user properties here + name: string; + email: string; + // Add other user properties here } interface UserContextType { - user: User | null; - setUser: Dispatch>; + user: User | null; + setUser: Dispatch>; } - export const UserContext = createContext({ - user: null, - setUser: () => null // No-op function with the correct type + user: null, + setUser: () => null, // No-op function with the correct type }); -const UserProvider = ({children}: {children: ReactNode}) => { - const [user, setUser] = useState(null); +const UserProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState(null); + const [cookies] = useCookies(["token"]); + useEffect(() => { + if (cookies.token) { + fetch("/api/user/info", { + method: "GET", + credentials: "include", + }) + .then((res) => res.json()) + .then((data) => { + if (data.user) { + setUser(data.user); + } else if (data.error) { + console.log(data.error); + } + }); + } + }, [cookies.token]); return ( - + {children} - ) -} + ); +}; -export default UserProvider \ No newline at end of file +export default UserProvider; diff --git a/front-end/src/components/BookingPage/BookingPage.scss b/front-end/src/components/BookingPage/BookingPage.scss index 7dc5b41..158c956 100644 --- a/front-end/src/components/BookingPage/BookingPage.scss +++ b/front-end/src/components/BookingPage/BookingPage.scss @@ -1,446 +1,447 @@ .container { - max-width: 1234px; - margin: 0 auto; - padding: 20px; - display: flex; - flex-direction: row; - position: relative; + max-width: 1234px; + margin: 0 auto; + padding: 20px; + display: flex; + flex-direction: row; + position: relative; } .booking-information { - margin-bottom: 20px; - border-radius: 10px; - @media (max-width: 991.98px) { - margin-bottom: 260px; - } + margin-bottom: 20px; + border-radius: 10px; + @media (max-width: 991.98px) { + margin-bottom: 260px; + } } .booking-information-header { - position: relative; - border-radius: 10px 10px 0 0; - background: $bg-gradient-info; - height: 350px; - padding: 20px; + position: relative; + border-radius: 10px 10px 0 0; + background: $bg-gradient-info; + height: 350px; + padding: 20px; - h1 { - font-size: 28px; - font-weight: bold; - color: $color-text-white; - margin-bottom: 20px; - } + h1 { + font-size: 28px; + font-weight: bold; + color: $color-text-white; + margin-bottom: 20px; + } - p { - color: $color-text-white; - } + p { + color: $color-text-white; + } } .booking-information-header__top { - position: relative; - margin-bottom: 20px; + position: relative; + margin-bottom: 20px; - p { - font-size: 14px; + p { + font-size: 14px; - &:nth-child(2) { - font-size: 13px; - margin-bottom: 5px - } + &:nth-child(2) { + font-size: 13px; + margin-bottom: 5px; + } - &:nth-child(3) { - font-size: 13px; - padding: 0; - margin: 0; - } - } + &:nth-child(3) { + font-size: 13px; + padding: 0; + margin: 0; + } + } } .booking-information-header__poster { - position: absolute; - right: 0; - margin-right: 20px; - border-radius: 5px; - width: fit-content; - height: fit-content; - display: flex; - justify-content: end; - - .booking-information-header__poster-image { - background: $border-image-poster; - padding: 3px; - border-radius: 5px; - width: fit-content; - height: fit-content; - - img { - width: 100%; - height: 100%; - max-width: 200px; - object-fit: cover; - border-radius: 5px; - } - } + position: absolute; + right: 0; + margin-right: 20px; + border-radius: 5px; + width: fit-content; + height: fit-content; + display: flex; + justify-content: end; + + .booking-information-header__poster-image { + background: $border-image-poster; + padding: 3px; + border-radius: 5px; + width: fit-content; + height: fit-content; + + img { + width: 100%; + height: 100%; + max-width: 200px; + object-fit: cover; + border-radius: 5px; + } + } } .booking-information-header__bottom { - padding-top: 20px; - - p { - display: flex; - align-items: center; - font-size: 24px; - margin-bottom: 20px; - } - - img { - width: 38px; - height: 38px; - margin-right: 20px; - } - - @media (max-width: 576px) { - padding-top: 10px; - text-overflow: nowrap; - img { - width: 30px; - height: 30px; - margin-right: 10px; - margin-bottom: 0px; - margin-top: 0px; - padding: 0px; - } - p { - font-size: 0.9rem; - text-overflow: nowrap; - margin-bottom: 0px; - } - } + padding-top: 20px; + + p { + display: flex; + align-items: center; + font-size: 24px; + margin-bottom: 20px; + } + + img { + width: 38px; + height: 38px; + margin-right: 20px; + } + + @media (max-width: 576px) { + padding-top: 10px; + text-overflow: nowrap; + img { + width: 30px; + height: 30px; + margin-right: 10px; + margin-bottom: 0px; + margin-top: 0px; + padding: 0px; + } + p { + font-size: 0.9rem; + text-overflow: nowrap; + margin-bottom: 0px; + } + } } .booking-information-content { - background: $color-primary-dark; + background: $color-primary-dark; } .email-input { - padding: 8px; - border: 1px solid $color-email-input-border; - border-radius: 4px; - width: 100%; - max-width: 400px; + padding: 8px; + border: 1px solid $color-email-input-border; + border-radius: 4px; + width: 100%; + max-width: 400px; } .book-button { - padding: -20px 20px; - background-color: $color-secondary; - color: $color-text-dark; - border: 2px solid $color-primary-dark; - cursor: pointer; - width: 50%; - border-radius: 10px; + padding: -20px 20px; + background-color: $color-secondary; + color: $color-text-dark; + border: 2px solid $color-primary-dark; + cursor: pointer; + width: 50%; + border-radius: 10px; - &:disabled { - background-color: $color-border-light; - } + &:disabled { + background-color: $color-border-light; + } - &:hover { - background: $color-select; - } + &:hover { + background: $color-select; + } } .alert { - padding: 10px; - margin-top: 20px; - border-radius: 4px; + padding: 10px; + margin-top: 20px; + border-radius: 4px; } .alert-success { - background-color: $color-alert-success-bg; - color: $color-alert-success-text; + background-color: $color-alert-success-bg; + color: $color-alert-success-text; } .alert-error { - background-color: $color-alert-error-bg; - color: $color-alert-error-text; + background-color: $color-alert-error-bg; + color: $color-alert-error-text; } .seat-row { - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 0px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0px; } .seat-grid { - display: flex; - flex-direction: column; - align-items: center; - gap: 5px; - margin: 10px 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + margin: 10px 0; } .seat-button { - padding: 2px; - background: $color-secondary; - border: 2px solid $color-primary-dark; - cursor: pointer; - margin: 2px; - font-size: 12px; - color: $color-text-dark; - width: 40px; - height: 40px; - - &:hover { - border: 2px solid $color-select; - box-shadow: 0 0 5px $color-select; - } + padding: 2px; + background: $color-secondary; + border: 2px solid $color-primary-dark; + cursor: pointer; + margin: 2px; + font-size: 12px; + color: $color-text-dark; + width: 40px; + height: 40px; + + &:hover { + border: 2px solid $color-select; + box-shadow: 0 0 5px $color-select; + } } .seat-button.selected { - background: $color-select; - color: $color-text-dark; + background: $color-select; + color: $color-text-dark; } .seat-button.unavailable { - background: $color-unavailable-seat; - cursor: not-allowed; - &:hover { - border: 2px solid $color-primary-dark; - box-shadow: 0 0 0px $color-select; - } + background: $color-unavailable-seat; + cursor: not-allowed; + &:hover { + border: 2px solid $color-primary-dark; + box-shadow: 0 0 0px $color-select; + } } .contact-info { - margin: 0; - background: $bg-gradient-contact-info; - color: $color-secondary; - border: 2px solid $color-secondary; - padding: 20px; + margin: 0; + background: $bg-gradient-contact-info; + color: $color-secondary; + border: 2px solid $color-secondary; + padding: 20px; } .ticket-counts { - margin: 0; - background: $bg-gradient-ticket-counts; - color: $color-secondary; - border: 2px solid $color-secondary; - padding: 20px; + margin: 0; + background: $bg-gradient-ticket-counts; + color: $color-secondary; + border: 2px solid $color-secondary; + padding: 20px; } .screen-container { - display: flex; - justify-content: center; - align-items: center; - margin-top: 1rem; + display: flex; + justify-content: center; + align-items: center; + margin-top: 1rem; } .screen { - width: 25rem; - height: 4rem; - background: linear-gradient(to bottom, #2CDF8C, #045685,); - color: black; // Text color - display: flex; - justify-content: center; - align-items: center; - position: relative; - clip-path: polygon(0% 0%, - 10% 100%, - 90% 100%, - 100% 0%); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); - z-index: 1; - font-size: 1.5rem; + width: 25rem; + height: 4rem; + background: linear-gradient(to bottom, #2cdf8c, #045685); + color: black; // Text color + display: flex; + justify-content: center; + align-items: center; + position: relative; + clip-path: polygon(0% 0%, 10% 100%, 90% 100%, 100% 0%); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + z-index: 1; + font-size: 1.5rem; } .age-confirmation { - margin: 0; - background: #2d2d2d; - color: $color-white; - padding: 20px; - .age-confirmation-inner-box { - border: 1px solid $color-age-box-border; - border-radius: 4px; - padding: 10px; - position: relative; - .checkbox-container { - width: 25%; - display: flex; - align-items: center; - justify-content: center; - - } - input[type="checkbox"] { - // margin: 20px; - transform: scale(1.6); - text-align: left; - accent-color: $color-select; - } - - label { - text-align: left; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - } - } + margin: 0; + background: #2d2d2d; + color: $color-white; + padding: 20px; + .age-confirmation-inner-box { + border: 1px solid $color-age-box-border; + border-radius: 4px; + padding: 10px; + position: relative; + .checkbox-container { + width: 25%; + display: flex; + align-items: center; + justify-content: center; + } + input[type="checkbox"] { + // margin: 20px; + transform: scale(1.6); + text-align: left; + accent-color: $color-select; + } + + label { + text-align: left; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + } } .ticket-counts__tickets { - display: flex; - justify-content: space-between; - flex-direction: column; - margin: 0; + display: flex; + justify-content: space-between; + flex-direction: column; + margin: 0; } .ticket-counts__tickets__ticket { - display: flex; - align-items: center; - justify-content: space-between; + display: flex; + align-items: center; + justify-content: space-between; - label { - @include text-stroke($color-primary-dark); - font-size: 28px; - } + label { + @include text-stroke($color-primary-dark); + font-size: 28px; + } } .ticket-counts__tickets__ticket__button-container { - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; - - span { - @include text-stroke($color-primary-dark); - font-size: 1.4rem; - min-width: 30px; - text-align: center; - transition: color 0.3s ease-in-out; - color: $color-select; - &.ticket-count-zero { - color: $color-secondary; - } - &.ticket-count-nonzero { - color: $color-select; - } - } - button { - // display: flex; - // justify-content: center; - // align-items: center; - text-align:center; - background: $color-secondary; - border: 2px solid $color-primary-dark; - width: 40px; - height: 40px; - border-radius: 50%; - transition: background 0.5s ease-in-out; - cursor: pointer; - font-weight: bolder; - font-size: 18px; - // padding-top: 0.15rem; - &:hover { - transition: border 0.1s ease-in-out, box-shadow 0.1s ease-in-out; - border: 2px solid $color-select; - box-shadow: 0 0 5px $color-select; - } - &:active { - transition: background 0.1s ease-in-out, transform 0.1s ease-in-out; - background: $color-select; - transform: scale(0.9); - } - } + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + + span { + @include text-stroke($color-primary-dark); + font-size: 1.4rem; + min-width: 30px; + text-align: center; + transition: color 0.3s ease-in-out; + color: $color-select; + &.ticket-count-zero { + color: $color-secondary; + } + &.ticket-count-nonzero { + color: $color-select; + } + } + button { + // display: flex; + // justify-content: center; + // align-items: center; + text-align: center; + background: $color-secondary; + border: 2px solid $color-primary-dark; + width: 40px; + height: 40px; + border-radius: 50%; + transition: background 0.5s ease-in-out; + cursor: pointer; + font-weight: bolder; + font-size: 18px; + // padding-top: 0.15rem; + &:hover { + transition: border 0.1s ease-in-out, box-shadow 0.1s ease-in-out; + border: 2px solid $color-select; + box-shadow: 0 0 5px $color-select; + } + &:active { + transition: background 0.1s ease-in-out, transform 0.1s ease-in-out; + background: $color-select; + transform: scale(0.9); + } + } } - .booking-modal { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: $bg-gradient-modal; - color: $color-secondary; - border: 2px solid $color-secondary; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - z-index: 9999; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: $bg-gradient-modal; + color: $color-secondary; + border: 2px solid $color-secondary; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; } .modal-content { - background: $color-white; - padding: 20px; - border-radius: 5px; - text-align: center; + background: $color-white; + padding: 20px; + border-radius: 5px; + text-align: center; } .book-button-container { - width: 100%; - display: flex; - justify-content: center; - background: #2d2d2d; - padding: 0 0 20px 0; - border-radius: 0 0 10px 10px; + width: 100%; + display: flex; + justify-content: center; + background: #2d2d2d; + padding: 0 0 20px 0; + border-radius: 0 0 10px 10px; } .total-amount-aside { - position: relative; - height: fit-content; - z-index: 3; - @media (max-width: 991.98px) { - position: fixed; - bottom: 0; - left: 0; - z-index: 1050; - padding: 1rem; - text-align: center; - background: transparent; - .total-amount { - position: sticky; - // margin-left: 12px; - } - } + position: relative; + height: fit-content; + z-index: 3; + @media (max-width: 991.98px) { + position: fixed; + bottom: 0; + left: 0; + z-index: 999; + padding: 1rem; + text-align: center; + background: transparent; + .total-amount { + position: sticky; + // margin-left: 12px; + } + } } .total-amount { - background: $bg-gradient-total-amount; - color: $color-primary-dark; - border: 2px solid $color-primary-dark; - padding: 20px; - margin-top: 0; - border-radius: 10px; - position: fixed; - width: 25%; - - h3, h2 { - margin-top: 10px; - display: flex; - justify-content: space-between; - text-align: left; - font-size: 20px; - - - &:last-child { - font-size: 25px; - font-weight: bold; - } - - span:first-child { - flex-grow: 1; - text-align: left; - } - - span:last-child { - text-align: right; - white-space: nowrap; - } - } + background: $bg-gradient-total-amount; + color: $color-primary-dark; + border: 2px solid $color-primary-dark; + padding: 20px; + margin-top: 0; + border-radius: 10px; + position: fixed; + width: 25%; + + h3, + h2 { + margin-top: 10px; + display: flex; + justify-content: space-between; + text-align: left; + font-size: 20px; + + &:last-child { + font-size: 25px; + font-weight: bold; + } + + span:first-child { + flex-grow: 1; + text-align: left; + } + + span:last-child { + text-align: right; + white-space: nowrap; + } + } } .book-button h1 { - font-size: 25px; + font-size: 25px; } body.hide-footer { - footer { - display: none; - } + footer { + display: none; + } +} + +.booking-information-header__poster-image img { + max-height: 300px; + width: auto; + object-fit: cover; } diff --git a/front-end/src/components/BookingPage/BookingPage.tsx b/front-end/src/components/BookingPage/BookingPage.tsx index d42d7ef..aedc5ad 100644 --- a/front-end/src/components/BookingPage/BookingPage.tsx +++ b/front-end/src/components/BookingPage/BookingPage.tsx @@ -1,10 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import './BookingPage.scss'; -import dateIcon from '../../assets/icons/calendar_today_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png'; -import timeIcon from '../../assets/icons/schedule_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png'; -import hallIcon from '../../assets/icons/icon-cinema-fatter.png'; -import { Container, Row } from 'react-bootstrap'; +import React, { useState, useEffect, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import "./BookingPage.scss"; +import dateIcon from "../../assets/icons/calendar_today_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png"; +import timeIcon from "../../assets/icons/schedule_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png"; +import hallIcon from "../../assets/icons/icon-cinema-fatter.png"; +import { Container, Row } from "react-bootstrap"; +import { io, Socket } from "socket.io-client"; interface Seat { seat: { @@ -61,14 +62,49 @@ interface BookingPageProps { showtimeId: string | undefined; } +//Static price for ordinary tickets +const ORDINARY_PRICE = 140; + const calculateEndTime = (startTime: string, length: number): string => { - const [hours, minutes] = startTime.split(':').map(Number); + const [hours, minutes] = startTime.split(":").map(Number); const startDateTime = new Date(); startDateTime.setHours(hours, minutes, 0); const endDateTime = new Date(startDateTime.getTime() + length * 60000); - return endDateTime.toTimeString().slice(0, 5); + return endDateTime.toTimeString().slice(0, 5); }; +const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); + +const formatDate = (dateStr: string): string => { + const date = new Date(dateStr); + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + + const options = { day: "numeric", month: "long" } as const; + const weekdayOptions = { + weekday: "long", + day: "numeric", + month: "long", + } as const; + + if ( + date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + ) { + return `Idag ${date.toLocaleDateString("sv-SE", options)}`; + } else if ( + date.getDate() === tomorrow.getDate() && + date.getMonth() === tomorrow.getMonth() && + date.getFullYear() === tomorrow.getFullYear() + ) { + return `Imorgon ${date.toLocaleDateString("sv-SE", options)}`; + } else { + const formattedDate = date.toLocaleDateString("sv-SE", weekdayOptions); + return capitalize(formattedDate); + } +}; const BookingPage: React.FC = ({ showtimeId }) => { const navigate = useNavigate(); @@ -78,20 +114,45 @@ const BookingPage: React.FC = ({ showtimeId }) => { const [movie, setMovie] = useState(null); const [seats, setSeats] = useState([]); const [selectedSeats, setSelectedSeats] = useState([]); - const [email, setEmail] = useState(''); + const [email, setEmail] = useState(""); const [ageConfirmation, setAgeConfirmation] = useState(false); const [showModal, setShowModal] = useState(false); - const [bookingStatus, setBookingStatus] = useState<{ success: boolean; message?: string; bookingNumber?: string } | null>(null); + const [bookingStatus, setBookingStatus] = useState<{ + success: boolean; + message?: string; + bookingNumber?: string; + } | null>(null); const [totalAmount, setTotalAmount] = useState(0); const [ticketTypes, setTicketTypes] = useState([]); const [ticketCounts, setTicketCounts] = useState>({}); + const socketRef = useRef(null); - const ORDINARY_PRICE = 140; + useEffect(() => { + // Initiera socket-anslutningen + socketRef.current = io("/"); // eller din specifika backend URL + + // Lyssna på anslutningshändelser + socketRef.current.on("connect", () => { + console.log("Connected to server"); + }); + + // Lyssna på seat-status-updated händelser + socketRef.current.on("seat-status-updated", (updatedSeat) => { + updateSeatStatus(updatedSeat._id, updatedSeat.isBooked); + }); + + // Cleanup funktion som körs när komponenten unmountas + return () => { + if (socketRef.current) { + socketRef.current.disconnect(); + } + }; + }, []); useEffect(() => { - document.body.classList.add('hide-footer'); + document.body.classList.add("hide-footer"); return () => { - document.body.classList.remove('hide-footer'); + document.body.classList.remove("hide-footer"); }; }, []); @@ -117,13 +178,13 @@ const BookingPage: React.FC = ({ showtimeId }) => { const response = await fetch(`/api/showtime/${showtimeId}`); const data = await response.json(); if (!response.ok) { - throw new Error(data.message || 'Failed to fetch showtime details'); + throw new Error(data.message || "Failed to fetch showtime details"); } setShowtime(data); await fetchMovieDetails(data.movie._id); await fetchAvailableSeats(); } catch (err: any) { - console.error('Error fetching showtime details:', err); + console.error("Error fetching showtime details:", err); setError(err.message); } finally { setLoading(false); @@ -136,11 +197,11 @@ const BookingPage: React.FC = ({ showtimeId }) => { const response = await fetch(`/api/movie/${movieId}`); const data = await response.json(); if (!response.ok) { - throw new Error(data.message || 'Failed to fetch movie details'); + throw new Error(data.message || "Failed to fetch movie details"); } setMovie(data); } catch (err: any) { - console.error('Error fetching movie details:', err); + console.error("Error fetching movie details:", err); setError(err.message); } }; @@ -150,27 +211,30 @@ const BookingPage: React.FC = ({ showtimeId }) => { const response = await fetch(`/api/showtime/${showtimeId}/seats`); const data = await response.json(); if (!response.ok) { - throw new Error(data.message || 'Failed to fetch available seats'); + throw new Error(data.message || "Failed to fetch available seats"); } setSeats(data.seats); } catch (err: any) { - console.error('Error fetching available seats:', err); + console.error("Error fetching available seats:", err); setError(err.message); } }; const handleSeatClick = (seatId: string) => { - const totalTickets = Object.values(ticketCounts).reduce((sum, count) => sum + count, 0); - if (selectedSeats.includes(seatId)) { - // If the seat is already selected, remove it - setSelectedSeats((prev) => prev.filter((id) => id !== seatId)); - } else if (selectedSeats.length < totalTickets) { - // Add the seat only if the number of selected seats is less than the total ticket count - setSelectedSeats((prev) => [...prev, seatId]); - } else { - alert('You have selected the maximum number of seats allowed.'); - } -}; + const totalTickets = Object.values(ticketCounts).reduce( + (sum, count) => sum + count, + 0 + ); + if (selectedSeats.includes(seatId)) { + // If the seat is already selected, remove it + setSelectedSeats((prev) => prev.filter((id) => id !== seatId)); + } else if (selectedSeats.length < totalTickets) { + // Add the seat only if the number of selected seats is less than the total ticket count + setSelectedSeats((prev) => [...prev, seatId]); + } else { + alert("You have selected the maximum number of seats allowed."); + } + }; const groupSeatsByRow = (seats: Seat[]) => { return seats.reduce((acc, seat) => { @@ -183,12 +247,20 @@ const BookingPage: React.FC = ({ showtimeId }) => { }, {} as Record); }; + const updateSeatStatus = (seatId: string, isBooked: boolean) => { + setSeats((prevSeats) => + prevSeats.map((seat) => + seat._id === seatId ? { ...seat, isBooked } : seat + ) + ); + }; + const fetchTicketTypes = async () => { try { - const response = await fetch('/api/ticket'); + const response = await fetch("/api/ticket"); const data = await response.json(); if (!response.ok) { - throw new Error(data.message || 'Failed to fetch ticket types'); + throw new Error(data.message || "Failed to fetch ticket types"); } setTicketTypes(data); // Initialize ticket counts @@ -198,80 +270,92 @@ const BookingPage: React.FC = ({ showtimeId }) => { }); setTicketCounts(initialCounts); } catch (err: any) { - console.error('Error fetching ticket types:', err); + console.error("Error fetching ticket types:", err); setError(err.message); } }; const handleTicketCountChange = (ticketType: string, increment: boolean) => { - setTicketCounts(prev => { + setTicketCounts((prev) => { const currentCount = prev[ticketType] || 0; - const newCount = increment ? currentCount + 1 : Math.max(0, currentCount - 1); + const newCount = increment + ? currentCount + 1 + : Math.max(0, currentCount - 1); const updatedCounts = { ...prev, [ticketType]: newCount }; - + // Beräkna totala antalet biljetter efter ändringen - const totalNewTickets = Object.values(updatedCounts).reduce((sum, count) => sum + count, 0); - + const totalNewTickets = Object.values(updatedCounts).reduce( + (sum, count) => sum + count, + 0 + ); + // Om vi minskar antalet biljetter, ta bort det senast valda sätet if (!increment && totalNewTickets < selectedSeats.length) { - setSelectedSeats(prev => prev.slice(0, -1)); // Ta bort det sista elementet i arrayen + setSelectedSeats((prev) => prev.slice(0, -1)); // Ta bort det sista elementet i arrayen } - + return updatedCounts; }); }; const handleBooking = async () => { - if (!email || selectedSeats.length === 0 || !ageConfirmation) { - setError('Please select seats, enter your email, and confirm age'); - return; - } + if (!email || selectedSeats.length === 0 || !ageConfirmation) { + setError("Please select seats, enter your email, and confirm age"); + return; + } - try { - const tickets = ticketTypes.map(ticketType => ({ - type: ticketType.type, - quantity: ticketCounts[ticketType.type] || 0 - })); - - // Filter out only the selected seats - const selectedSeatObjects = seats.filter(seat => selectedSeats.includes(seat._id)); - - const response = await fetch('/api/user/bookings', { - - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - showtimeId, - selectedSeats: selectedSeatObjects.map(seat => seat.seat._id), - email, - tickets, - totalAmount, - }), - }); + try { + const tickets = ticketTypes.map((ticketType) => ({ + type: ticketType.type, + quantity: ticketCounts[ticketType.type] || 0, + })); + + // Filter out only the selected seats + const selectedSeatObjects = seats.filter((seat) => + selectedSeats.includes(seat._id) + ); + + const response = await fetch("/api/user/bookings", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + showtimeId, + selectedSeats: selectedSeatObjects.map((seat) => seat.seat._id), + email, + tickets, + totalAmount, + }), + }); - const data = await response.json(); + const data = await response.json(); - if (!response.ok) { - throw new Error(data.error || 'Failed to create booking'); - } + if (!response.ok) { + throw new Error(data.error || "Failed to create booking"); + } - setBookingStatus({ - success: true, - bookingNumber: data.booking.bookingNumber, - }); + // Emittera book-seat händelser med socket + if (socketRef.current) { + selectedSeatObjects.forEach((seat) => { + socketRef.current?.emit("book-seat", seat.seat._id, showtimeId); + }); + } - setShowModal(true); // Visa modalen med bokningsinformation - } catch (err: any) { - console.error('Error creating booking:', err); - setBookingStatus({ - success: false, - message: err.message, - }); - } -}; + setBookingStatus({ + success: true, + bookingNumber: data.booking.bookingNumber, + }); + setShowModal(true); // Visa modalen med bokningsinformation + } catch (err: any) { + console.error("Error creating booking:", err); + setBookingStatus({ + success: false, + message: err.message, + }); + } + }; const closeModal = () => { setShowModal(false); @@ -290,26 +374,39 @@ const BookingPage: React.FC = ({ showtimeId }) => {
- {/* Section 1: Showtime Info */}
{movie?.title} -
+
-

{movie?.title}

-

Tal: {movie?.language}, Undertexter: {movie?.subtitles}

-

Genre: {movie?.genre.join(', ')}

+

{movie?.title}

+

+ Tal: {movie?.language}, Undertexter: {movie?.subtitles} +

+

Genre: {movie?.genre.join(", ")}

Speltid: {movie?.length} minuter

-

dateDatum: {showtime && new Date(showtime.date).toLocaleDateString()}

-

timekl {showtime?.time} - {showtime && movie && calculateEndTime(showtime.time, movie.length)}

-

hallSalong: {showtime?.hall.hallName}

-
+

+ date + {showtime && formatDate(showtime.date)} +

+

+ time + kl {showtime?.time} -{" "} + {showtime && + movie && + calculateEndTime(showtime.time, movie.length)} +

+

+ hall + Salong: {showtime?.hall.hallName} +

+
@@ -317,18 +414,35 @@ const BookingPage: React.FC = ({ showtimeId }) => {
{ticketTypes.map((ticketType) => ( -
+
- - + + {ticketCounts[ticketType.type] || 0} - +
))} @@ -338,103 +452,122 @@ const BookingPage: React.FC = ({ showtimeId }) => { {/* Section 3: Seat Selection */}
-
Bioduk
+
Bioduk
- {Object.entries(groupSeatsByRow(seats)).map(([rowNumber, rowSeats]) => ( -
- {rowSeats - .sort((a, b) => b.seat.seatNumber - a.seat.seatNumber) // Sort seats in descending order - .map((seat) => ( - - ))} -
- ))} -
- - {/* Section 4: Contact Information */} -
-

Biljettleverans

-

För att boka biljetter, ange din e-postadress.

- setEmail(e.target.value)} - className="email-input" - /> -
+ {Object.entries(groupSeatsByRow(seats)).map( + ([rowNumber, rowSeats]) => ( +
+ {rowSeats + .sort((a, b) => b.seat.seatNumber - a.seat.seatNumber) // Sort seats in descending order + .map((seat) => ( + + ))} +
+ ) + )} +
- {/* Section 5: Age Confirmation */} -
-
-
-
- - {showModal && ( -
-
-

Bokningsbekräftelse

- {bookingStatus?.success ? ( - <> -

Bokningen genomfördes

-

Ditt bokningsnummer: {bookingStatus.bookingNumber}

-

Information har skickats till angiven e-postadress

- - - ) : ( - <> -

{bookingStatus?.message}

- - - )} + + {showModal && ( +
+
+

Bokningsbekräftelse

+ {bookingStatus?.success ? ( + <> +

Bokningen genomfördes

+

Ditt bokningsnummer: {bookingStatus.bookingNumber}

+

Information har skickats till angiven e-postadress

+ + + ) : ( + <> +

{bookingStatus?.message}

+ + + )} +
-
)} - - {/* Section 6: Total Amount - Aside */} -
-
- {ticketTypes.map((ticketType) => ( -

- {ticketType.type}: {ticketCounts[ticketType.type] || 0} st - {(ticketCounts[ticketType.type] || 0) * ticketType.price} kr -

- ))} + + {/* Section 6: Total Amount - Aside */} +
+
+ {ticketTypes.map((ticketType) => ( +

+ + {ticketType.type}: {ticketCounts[ticketType.type] || 0} st + + + {(ticketCounts[ticketType.type] || 0) * ticketType.price} kr + +

+ ))}

Ordinarie pris: {Object.values(ticketCounts).reduce( - (sum, count) => sum + (count || 0) * ORDINARY_PRICE, - 0 - )} kr + (sum, count) => sum + (count || 0) * ORDINARY_PRICE, + 0 + )}{" "} + kr

@@ -442,9 +575,10 @@ const BookingPage: React.FC = ({ showtimeId }) => { Totalt prisavdrag: {Object.values(ticketCounts).reduce( - (sum, count) => sum + (count || 0) * ORDINARY_PRICE, - 0 - ) - totalAmount} kr + (sum, count) => sum + (count || 0) * ORDINARY_PRICE, + 0 + ) - totalAmount}{" "} + kr diff --git a/front-end/src/components/ScheduleSection/ScheduleSection.scss b/front-end/src/components/ScheduleSection/ScheduleSection.scss index d11b7d6..e424905 100644 --- a/front-end/src/components/ScheduleSection/ScheduleSection.scss +++ b/front-end/src/components/ScheduleSection/ScheduleSection.scss @@ -24,8 +24,8 @@ color: $color-secondary; margin: 0; &::first-letter { - text-transform: uppercase; - } + text-transform: uppercase; + } } } @@ -48,21 +48,27 @@ color: $color-primary-dark; border-radius: 5px; cursor: pointer; - transition: background-color 0.3s, transform 0.2s; + transition: background-color 0.4s, outline 0.1s, border 0.2s; text-align: center; + border: 1px solid $color-primary-dark; + margin-top: 1px; + margin-bottom: 1px; &:hover { - outline: 3px solid $color-select; + outline: 2px solid $color-select; } &.selected { background-color: $color-select; - border: 1px solid $color-primary-dark; + border: 1px solid $color-select; } &.no-showtime { background-color: $color-unavailable-seat; cursor: not-allowed; + &:hover { + outline: none; + } } p { margin: 0; @@ -148,16 +154,20 @@ display: inline-block; position: relative; border-radius: 5px; // Samma som bilden för att matcha hörnen - background: linear-gradient(to bottom, $color-primary-dark, $color-primary-light); + background: linear-gradient( + to bottom, + $color-primary-dark, + $color-primary-light + ); padding: 2px; // Justera efter behov för att skapa avstånd mellan bilden och gradienten margin-right: 15px; img { - width: 80px; - height: 118px; - border-radius: 5px; - // border: 1px solid $color-primary-dark; - } + width: 80px; + height: 118px; + border-radius: 5px; + // border: 1px solid $color-primary-dark; + } } .schedule-section-showtime-info__text { @@ -176,7 +186,6 @@ text-align: left; font-size: 14px; } - } .schedule-section-showtime-info__text__age { position: absolute; @@ -203,4 +212,4 @@ font-size: 16px; color: #999; } -} \ No newline at end of file +} diff --git a/front-end/src/layout/Header/Header.scss b/front-end/src/layout/Header/Header.scss index 326e223..e04a3be 100644 --- a/front-end/src/layout/Header/Header.scss +++ b/front-end/src/layout/Header/Header.scss @@ -7,8 +7,6 @@ header { justify-content: space-between; flex-direction: row !important; height: 80px; - // width: 80%; - // margin-left: 10vw; overflow: hidden; .schedule-button-container { @@ -16,9 +14,11 @@ header { flex-wrap: nowrap; align-items: center; justify-content: flex-start; + button { - border: none; - } + border: none; + } + .schedule-button-container__button { display: flex; align-items: center; @@ -41,16 +41,19 @@ header { transition: background 0.3s ease, color 0.3s ease; } } + .schedule-button-container__button:first-child { - padding-left: 0px; + padding-left: 0px; } + @media (max-width: 576px) { opacity: 0; font-size: 0.9rem; padding: 0 5px; + .schedule-button-container__button:first-child { - padding-left: 5px; - } + padding-left: 5px; + } } } @@ -59,15 +62,17 @@ header { justify-content: center; align-items: center; height: 76px; + .logo-img-wrapper { display: flex; justify-content: center; align-items: center; + .logo-img { - max-height: 76px; - max-width: 300px; - } - } + max-height: 76px; + max-width: 300px; + } + } } .search-login-container { @@ -76,27 +81,48 @@ header { display: flex; justify-content: flex-end; height: 76px; + .search-login-container__icon { - display: flex; - justify-content: center; - align-items: center; - max-width: 60px; - border-left: 2px solid $color-secondary; - img { - height: 40px; - width: auto; - } - &:hover { - background: $color-secondary; - transition: background 0.3s ease; - & img { - filter:brightness(1.2) invert(1) saturate(0.1); - transition: filter 0.3s ease; - } + display: flex; + justify-content: center; + align-items: center; + width: 20%; + max-width: 60px; + min-width: 48px; + border-left: 2px solid $color-secondary; + + button { + border: none; + background: transparent; + } + + img { + height: 40px; + width: auto; + } + + &:hover { + background: $color-secondary; + transition: background 0.3s ease; + + & img { + filter: brightness(1.2) invert(1) saturate(0.1); + transition: filter 0.3s ease; } - &:last-child { - padding-right: 0px; + } + + &:last-child { + padding-right: 0px; + } + } + @media (max-width: 576px) { + .search-login-container__icon { + min-width: 40px; + img { + height: 28px; + width: auto; } } + } } -} \ No newline at end of file +} diff --git a/front-end/src/layout/Header/Header.tsx b/front-end/src/layout/Header/Header.tsx index f2c471e..5622b72 100644 --- a/front-end/src/layout/Header/Header.tsx +++ b/front-end/src/layout/Header/Header.tsx @@ -1,46 +1,83 @@ -import React, { useEffect, useState, useContext } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import SearchIcon from '../../assets/icons/search_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png'; -import LoginIcon from '../../assets/icons/person_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png'; -import ProfileIcon from "../../assets/icons/clarify_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png" -import { UserContext } from '../../UserContext'; -import './Header.scss'; -import Logo from '../../assets/img/logo-text-side.png'; -import LogoSmall from '../../assets/img/logo-no-text.png'; -import LoginModal from '../../views/modals/LoginModal'; -import SearchBar from '../../components/SearchBar/SearchBar'; +import React, { useEffect, useState, useContext } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import SearchIcon from "../../assets/icons/search_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png"; +import LoginIcon from "../../assets/icons/person_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png"; +import ProfileIcon from "../../assets/icons/clarify_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png"; +import { UserContext } from "../../UserContext"; +import "./Header.scss"; +import Logo from "../../assets/img/logo-text-side.png"; +import LogoSmall from "../../assets/img/logo-no-text.png"; +import LoginModal from "../../views/modals/LoginModal"; +import LogoutIcon from "../../assets/icons/logout_35dp_FCAF00_FILL0_wght400_GRAD0_opsz40.png"; +import SearchBar from "../../components/SearchBar/SearchBar"; -const Header: React.FC<{ onSelectDate: (daysAhead: number) => void }> = ({ onSelectDate }) => { +const Header: React.FC<{ onSelectDate: (daysAhead: number) => void }> = ({ + onSelectDate, +}) => { const [showModal, setShowModal] = useState(false); - const [modalType, setModalType] = useState('login'); - const handleShow = () => setShowModal(true); + const [modalType, setModalType] = useState("login"); + const navigate = useNavigate(); + const handleShow = () => { + setModalType("login"); + setShowModal(true); + }; + const handleClose = () => setShowModal(false); - const { user } = useContext(UserContext); + const { user, setUser } = useContext(UserContext); const location = useLocation(); - const isBookingPage = location.pathname.startsWith('/booking/'); + const isBookingPage = location.pathname.startsWith("/booking/"); console.log(user); const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < 576); const [isSearchOpen, setIsSearchOpen] = useState(false); useEffect(() => { const handleResize = () => setIsSmallScreen(window.innerWidth < 576); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); }, []); - const scrollToTop = () => { - window.scrollTo({ top: 0, behavior: 'smooth' }); + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: "smooth" }); + }; + const handleLogout = () => { + fetch("/api/auth/logout", { + method: "POST", + }) + .then((res) => res.json()) + .then((data) => { + if (data.message) { + setUser(null); + navigate("/"); + alert(data.message); + } else if (data.error) { + console.log(data.error); + } + }); }; - return (
@@ -48,31 +85,57 @@ const Header: React.FC<{ onSelectDate: (daysAhead: number) => void }> = ({ onSel
- Logo + Logo
-
-
setIsSearchOpen(true)}> + {/* SearchIcon visas alltid */} +
+
+
-
- {user ? ( - - Login - - ) : ( -
- Login + + {/* Om användaren är inloggad, visa ProfileIcon och LogoutIcon */} + {user ? ( + <> +
+ + Profile +
- )} -
+
+ +
+ + ) : ( + // Om användaren inte är inloggad, visa bara LoginIcon +
+ +
+ )}
- + setIsSearchOpen(false)} />
); diff --git a/front-end/src/views/modals/LoginModal.scss b/front-end/src/views/modals/LoginModal.scss index 3e46137..51abeeb 100644 --- a/front-end/src/views/modals/LoginModal.scss +++ b/front-end/src/views/modals/LoginModal.scss @@ -1,60 +1,76 @@ +.modal-background { + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} .modal-content { - background-color: #000F26; - background: $color-modal-bg; - padding: 20px; - border-radius: 8px; - width: 500px; - color: #FCAF00; - max-width: 90%; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.8); - position: relative; - } - .modal-open { - overflow: hidden; - } - .close-button { - position: absolute; - top: 10px; - right: 10px; - font-size: 24px; - cursor: pointer; - } - - h2 { - margin-bottom: 20px; - } - - .form-group { - text-align: left; - margin-bottom: 15px; - } - - label { - - margin-bottom: 5px; - } - - input { - width: 100%; - padding: 8px; - border: 1px solid #ccc; - border-radius: 4px; - } - - .submit-button { - background-color: #FCAF00; - color: #000F26; - font-size: 20px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8); - padding: 10px 40px; - border: none; - border-radius: 4px; - cursor: pointer; - - } - - .submit-button:hover { - filter: brightness(1.1); - transition: all 0.3s ease-in-out; - } \ No newline at end of file + background: $color-modal-bg; + padding: 20px; + border-radius: 8px; + width: 500px; + color: $color-secondary; + max-width: 90%; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.8); + position: relative; + @include text-stroke($color-primary-dark); +} + +// .modal-open { +// overflow: hidden; +// } + +.close-button { + position: absolute; + top: 10px; + right: 10px; + font-size: 24px; + cursor: pointer; +} + +h2 { + margin-bottom: 20px; +} + +.form-group { + text-align: left; + margin-bottom: 15px; +} + +label { + margin-bottom: 5px; +} + +input { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.submit-button { + background-color: $color-secondary; + color: $color-primary-dark; + font-size: 20px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8); + padding: 10px 40px; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.submit-button:hover { + filter: brightness(1.1); + transition: all 0.3s ease-in-out; +} + +@keyframes shake { + 0% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 50% { transform: translateX(5px); } + 75% { transform: translateX(-5px); } + 100% { transform: translateX(0); } +} + +.shake { + animation: shake 0.5s ease-in-out; +} \ No newline at end of file diff --git a/front-end/src/views/modals/LoginModal.tsx b/front-end/src/views/modals/LoginModal.tsx index 3ad287b..2fa001f 100644 --- a/front-end/src/views/modals/LoginModal.tsx +++ b/front-end/src/views/modals/LoginModal.tsx @@ -1,8 +1,7 @@ -import React, { useEffect, useContext } from "react"; +import React, { useEffect, useContext, useState } from "react"; import "./LoginModal.scss"; // Ensure this import is correct import { UserContext } from "../../UserContext"; - type Props = { type: string; show: boolean; @@ -16,12 +15,23 @@ const LoginModal: React.FC = ({ handleClose, setModalType, }) => { - const [email, setEmail] = React.useState(""); - const [password, setPassword] = React.useState(""); - const [firstName, setFirstName] = React.useState(""); - const [lastName, setLastName] = React.useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [oldPassword, setOldPassword] = useState(""); + const [error, setError] = useState(''); + const [newPassword, setNewPassword] = useState(""); const { setUser } = useContext(UserContext); useEffect(() => { + setEmail(""); + setPassword(""); + setFirstName(""); + setLastName(""); + setOldPassword(""); + setNewPassword(""); + setError(''); + if (show) { document.body.classList.add("modal-open"); } else { @@ -30,7 +40,7 @@ const LoginModal: React.FC = ({ return () => { document.body.classList.remove("modal-open"); }; - }, [show]); + }, [show, type]); if (!show) { return null; @@ -57,17 +67,62 @@ const LoginModal: React.FC = ({ setUser(data.user); handleClose(); } else { - alert(data.error); + setError(data.error); + } + }); + }; + const handleLogin = (e: React.FormEvent) => { + e.preventDefault(); + fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + password, + }), + }) + .then((res) => res.json()) + .then((data) => { + if (data.message) { + alert("User logged in successfully"); + setUser(data.user); + handleClose(); + } else { + setError(data.error); + } + }); + }; + const handleReset = (e: React.FormEvent) => { + e.preventDefault(); + fetch("/api/auth/reset-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + oldPassword, + newPassword, + }), + }) + .then((res) => res.json()) + .then((data) => { + if (data.message) { + alert("Password reset successfully"); + handleClose(); + } else { + setError(data.error); } }); }; - return (
- {type === "login" ? ( + {type === "login" && (
= ({ ×

Logga in

-
+
- + setEmail(e.target.value)} id="email" name="email" required /> + {error.includes("User") &&

{error}!

}
- + setPassword(e.target.value)} id="password" name="password" required /> + {error.includes("password") &&

{error}!

}

- Har du glömt lösenordet? Klicka här + Har du glömt lösenordet? Klicka setModalType("reset")} + >här

- ) : ( + )} + { type === "register" && (
+ + × + +

Skapa användare

+
+
+ + setEmail(e.target.value)} + value={email} + name="email" + required + /> + {error &&

{error}!

} +
+
+ + setPassword(e.target.value)} + value={password} + name="password" + required + /> +
+
+ + setFirstName(e.target.value)} value={firstName} name="firstname" required /> +
+
+ + setLastName(e.target.value)} value={lastName} id="lastname" name="lastname" required /> +
+ +
+

+ Har du redan ett konto? Klicka här +

+
+ +
+
+ )} + { type === "reset" && ( +
= ({ maxWidth: "90%", boxShadow: "0 2px 10px rgba(0, 0, 0, 0.8)", position: "relative", - }} + }} > - + × - -

Skapa användare

-
-
- - setEmail(e.target.value)} - value={email} - name="email" - required - /> -
+ +

Återställ lösenord

+
- - setPassword(e.target.value)} - value={password} - name="password" - required - /> + + setEmail(e.target.value)} value={email} name="email" required /> + {error.includes("User") &&

{error}!

}
- - setFirstName(e.target.value)} value={firstName} name="firstname" required /> + + setOldPassword(e.target.value)} value={oldPassword} name="oldpassword" required /> + {error.includes("password") &&

{error}!

}
- - setLastName(e.target.value)} value={lastName} id="lastname" name="lastname" required /> + + setNewPassword(e.target.value)} value={newPassword} name="newpassword" required />
-
-

- Har du redan ett konto? Klicka här -

-
- -
+
- )} + )} +
); }; -export default LoginModal; +export default LoginModal; \ No newline at end of file diff --git a/front-end/vite.config.ts b/front-end/vite.config.ts index bd32e41..2a992c6 100644 --- a/front-end/vite.config.ts +++ b/front-end/vite.config.ts @@ -1,5 +1,5 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ @@ -7,16 +7,20 @@ export default defineConfig({ preprocessorOptions: { scss: { additionalData: `@use "/src/sass" as *;`, - silenceDeprecations: ['legacy-js-api'], + silenceDeprecations: ["legacy-js-api"], }, }, }, server: { proxy: { - '/api': 'http://localhost:5000' + "/api": "http://localhost:5000", + "/socket.io": { + target: "http://localhost:5000", + ws: true, // WebSocket proxy + }, }, host: true, - port: 3000 + port: 3000, }, plugins: [react()], -}) +}); diff --git a/package.json b/package.json index f956a6d..d0809f3 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,13 @@ "nodemailer": "^6.9.15", "react": "^18.3.1", "react-bootstrap": "^2.10.5", + "react-cookie": "^7.2.2", "react-dom": "^18.3.1", "react-icons": "^5.3.0", "react-router-dom": "^6.26.2", - "sass": "^1.80.3" + "sass": "^1.80.3", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.9.0",