├── public
├── locales
│ ├── en
│ │ └── translation.json
│ └── vi
│ │ └── translation.json
├── favicon.png
└── index.html
├── src
├── containers
│ ├── AdminTemplate
│ │ ├── UserDashBoard
│ │ │ ├── component
│ │ │ │ ├── UserModal
│ │ │ │ │ └── style.scss
│ │ │ │ └── TableCellList
│ │ │ │ │ ├── style.scss
│ │ │ │ │ └── index.jsx
│ │ │ ├── constants.js
│ │ │ └── index.jsx
│ │ ├── components
│ │ │ ├── AdminFooter
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ ├── Buttons
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ ├── AdminDrawer
│ │ │ │ ├── style.scss
│ │ │ │ ├── DrawerItems.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── AdminAppBar
│ │ │ │ ├── index.jsx
│ │ │ │ └── AccountAvatar
│ │ │ │ │ └── index.jsx
│ │ │ ├── MuiEnhancedTable
│ │ │ │ ├── constants.js
│ │ │ │ └── style.scss
│ │ │ └── SearchBar
│ │ │ │ └── index.jsx
│ │ ├── MovieDashBoard
│ │ │ ├── style.scss
│ │ │ ├── components
│ │ │ │ ├── MovieModal
│ │ │ │ │ └── style.scss
│ │ │ │ └── TableCellList
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── style.scss
│ │ │ ├── constants.js
│ │ │ ├── index.jsx
│ │ │ └── ScheduleModal
│ │ │ │ └── index.jsx
│ │ └── index.jsx
│ ├── HomeTemplate
│ │ ├── HomePage
│ │ │ ├── style.scss
│ │ │ ├── index.jsx
│ │ │ ├── CinemaSystem
│ │ │ │ ├── MovieSchedule
│ │ │ │ │ ├── style.scss
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── style.scss
│ │ │ │ ├── CinemaGroup
│ │ │ │ │ └── index.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── Carousel
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ └── MovieList
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ ├── TicketBookingPage
│ │ │ ├── style.scss
│ │ │ ├── SeatSelector
│ │ │ │ ├── SeatNote
│ │ │ │ │ ├── style.scss
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── SeatGrid
│ │ │ │ │ ├── style.scss
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ ├── index.jsx
│ │ │ └── TicketBookingCard
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ ├── ProfilePage
│ │ │ ├── TransactionHistory
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ ├── style.scss
│ │ │ ├── AccountInfo
│ │ │ │ ├── Input
│ │ │ │ │ ├── style.scss
│ │ │ │ │ └── index.jsx
│ │ │ │ └── style.scss
│ │ │ └── index.jsx
│ │ ├── index.jsx
│ │ ├── components
│ │ │ ├── Footer
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ └── Navbar
│ │ │ │ └── style.scss
│ │ └── MovieDetailsPage
│ │ │ ├── style.scss
│ │ │ └── index.jsx
│ ├── AuthTemplate
│ │ ├── components
│ │ │ ├── Input
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ ├── Footer
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ ├── Background
│ │ │ │ ├── index.jsx
│ │ │ │ └── style.scss
│ │ │ ├── Button
│ │ │ │ ├── style.scss
│ │ │ │ └── index.jsx
│ │ │ ├── Header
│ │ │ │ └── index.jsx
│ │ │ └── Card
│ │ │ │ └── index.jsx
│ │ ├── RegisterPage
│ │ │ ├── style.scss
│ │ │ └── index.jsx
│ │ ├── LoginPage
│ │ │ ├── style.scss
│ │ │ └── index.jsx
│ │ └── index.jsx
│ └── NotFoundPage
│ │ └── style.scss
├── assets
│ ├── images
│ │ ├── no-img.jpeg
│ │ ├── header-logo.png
│ │ ├── auth-background.jpg
│ │ ├── nauti_heroesim.jpeg
│ │ ├── payment_method_pc.png
│ │ ├── payment_method_mobile.png
│ │ └── index.js
│ └── docs-images
│ │ ├── finnkino-tech-stack.png
│ │ ├── finnkino-detail-mobile.png
│ │ ├── finnkino-login-laptop.png
│ │ ├── finnkino-booking-laptop.png
│ │ ├── finnkino-profile-mobile.png
│ │ ├── finnkino-movie-management-laptop.png
│ │ └── finnkino-user-management-laptop.png
├── hooks
│ ├── index.js
│ ├── useScrollToTop.js
│ └── useAuth.js
├── components
│ ├── GlobalStyles
│ │ ├── index.js
│ │ └── GlobalStyles.scss
│ ├── ReactSlick
│ │ ├── MultipleItems.scss
│ │ └── MultipleItems.js
│ ├── Loader
│ │ ├── index.js
│ │ └── Loader.scss
│ ├── PageLoader
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── Modal
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── Image
│ │ └── index.js
│ └── MuiPicker
│ │ └── index.js
├── constants
│ └── index.js
├── api
│ ├── index.js
│ ├── config
│ │ ├── apiConfig.js
│ │ └── axiosClient.js
│ ├── ticketBookingApi.js
│ ├── cinemaApi.js
│ ├── movieApi.js
│ └── userApi.js
├── store
│ ├── constants
│ │ ├── userList.js
│ │ ├── cinemaSystem.js
│ │ ├── userDetails.js
│ │ ├── movieBanner.js
│ │ ├── movieDetails.js
│ │ ├── movieManagement.js
│ │ ├── movieList.js
│ │ ├── userProfile.js
│ │ ├── ticketBooking.js
│ │ └── userManagement.js
│ ├── index.js
│ ├── reducers
│ │ ├── userList.js
│ │ ├── userDetails.js
│ │ ├── cinemaSystem.js
│ │ ├── movieBanner.js
│ │ ├── movieDetails.js
│ │ ├── movieManagement.js
│ │ ├── index.js
│ │ ├── movieList.js
│ │ ├── userManagement.js
│ │ ├── userProfile.js
│ │ └── ticketBooking.js
│ └── actions
│ │ ├── userManagement.js
│ │ ├── movieBanner.js
│ │ ├── cinemaSystem.js
│ │ ├── userList.js
│ │ ├── movieDetails.js
│ │ ├── movieList.js
│ │ ├── userDetails.js
│ │ ├── userProfile.js
│ │ ├── movieManagement.js
│ │ └── ticketBooking.js
├── routes
│ ├── NotFoundRoute.js
│ ├── index.js
│ ├── AuthRoutes.js
│ ├── AdminRoutes.js
│ └── ClientRoutes.js
├── validators
│ ├── login.js
│ ├── movieScheduleValidator.js
│ ├── index.js
│ ├── userValidator.js
│ ├── register.js
│ ├── message
│ │ └── index.js
│ ├── accountInfoValidator.js
│ ├── pattern
│ │ └── index.js
│ └── movieValidator.js
├── i18n
│ └── index.js
├── guard
│ └── index.jsx
├── index.js
└── App.js
├── .env.development
├── config-overrides.js
├── jsconfig.json
├── .vscode
└── settings.json
├── .babelrc
├── .gitignore
├── .prettierrc
└── package.json
/public/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "login": "log in"
3 | }
4 |
--------------------------------------------------------------------------------
/public/locales/vi/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "login": "Đăng nhập"
3 | }
4 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/UserDashBoard/component/UserModal/style.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_URL = "https://movienew.cybersoft.edu.vn/api/"
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/style.scss:
--------------------------------------------------------------------------------
1 | #home-page {
2 | background-color: var(--black);
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/images/no-img.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/images/no-img.jpeg
--------------------------------------------------------------------------------
/src/assets/images/header-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/images/header-logo.png
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { override, useBabelRc } = require("customize-cra");
2 |
3 | module.exports = override(useBabelRc());
4 |
--------------------------------------------------------------------------------
/src/assets/images/auth-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/images/auth-background.jpg
--------------------------------------------------------------------------------
/src/assets/images/nauti_heroesim.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/images/nauti_heroesim.jpeg
--------------------------------------------------------------------------------
/src/assets/images/payment_method_pc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/images/payment_method_pc.png
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["src/*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/images/payment_method_mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/images/payment_method_mobile.png
--------------------------------------------------------------------------------
/src/assets/docs-images/finnkino-tech-stack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/docs-images/finnkino-tech-stack.png
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/style.scss:
--------------------------------------------------------------------------------
1 | .ticket-booking-page {
2 | background-color: var(--black);
3 | padding: 20px 10px;
4 | }
5 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import useAuth from "./useAuth";
2 | import useScrollToTop from "./useScrollToTop";
3 |
4 | export { useAuth, useScrollToTop };
5 |
--------------------------------------------------------------------------------
/src/assets/docs-images/finnkino-detail-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/docs-images/finnkino-detail-mobile.png
--------------------------------------------------------------------------------
/src/assets/docs-images/finnkino-login-laptop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/docs-images/finnkino-login-laptop.png
--------------------------------------------------------------------------------
/src/assets/docs-images/finnkino-booking-laptop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/docs-images/finnkino-booking-laptop.png
--------------------------------------------------------------------------------
/src/assets/docs-images/finnkino-profile-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/docs-images/finnkino-profile-mobile.png
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Input/style.scss:
--------------------------------------------------------------------------------
1 | .auth-input {
2 | .MuiFormLabel-root,
3 | .MuiInput-input {
4 | font-size: 13px;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "liveServer.settings.port": 5501
5 | }
6 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/AdminFooter/style.scss:
--------------------------------------------------------------------------------
1 | .admin-footer {
2 | margin: 64px 0 32px;
3 | font-size: 17px;
4 | color: var(--dark-gray);
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/docs-images/finnkino-movie-management-laptop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/docs-images/finnkino-movie-management-laptop.png
--------------------------------------------------------------------------------
/src/assets/docs-images/finnkino-user-management-laptop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulna07/finnkino-cinema/HEAD/src/assets/docs-images/finnkino-user-management-laptop.png
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Footer/style.scss:
--------------------------------------------------------------------------------
1 | .auth-footer {
2 | margin: 64px 0 32px !important;
3 | font-size: 13px !important;
4 | color: var(--white);
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/GlobalStyles/index.js:
--------------------------------------------------------------------------------
1 | import "./GlobalStyles.scss";
2 |
3 | function GlobalStyles({ children }) {
4 | return children;
5 | }
6 |
7 | export default GlobalStyles;
8 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | [
4 | "module-resolver",
5 | {
6 | "alias": {
7 | "@": "./src"
8 | }
9 | }
10 | ]
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/MovieDashBoard/style.scss:
--------------------------------------------------------------------------------
1 | @media (max-width: 739px) {
2 | .movie-dashboard__search {
3 | .search-bar__input {
4 | font-size: 10px;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/ReactSlick/MultipleItems.scss:
--------------------------------------------------------------------------------
1 | .carousel__arrow {
2 | &.carousel__arrow--next {
3 | right: 10%;
4 | }
5 |
6 | &.carousel__arrow--prev {
7 | left: 10%;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export const GROUP_ID = "GP09";
2 |
3 | export const ROLE = {
4 | ADMIN: "QuanTri",
5 | CLIENT: "KhachHang",
6 | };
7 |
8 | export const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
9 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/ProfilePage/TransactionHistory/style.scss:
--------------------------------------------------------------------------------
1 | .transaction-history {
2 | &__table-head-cell {
3 | padding: {
4 | top: 12px;
5 | bottom: 12px;
6 | }
7 | font-weight: 700;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/SeatSelector/SeatNote/style.scss:
--------------------------------------------------------------------------------
1 | .seat-selector__seat-note {
2 | &-box {
3 | width: 13px;
4 | height: 13px;
5 | }
6 |
7 | &-text {
8 | font-size: 13px;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/hooks/useScrollToTop.js:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from "react";
2 |
3 | const useScrollToTop = () => {
4 | useLayoutEffect(() => {
5 | window.scrollTo(0, 0);
6 | }, []);
7 | };
8 |
9 | export default useScrollToTop;
10 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import movieApi from "./movieApi";
2 | import cinemaApi from "./cinemaApi";
3 | import ticketBookingApi from "./ticketBookingApi";
4 | import userApi from "./userApi";
5 |
6 | export { movieApi, cinemaApi, ticketBookingApi, userApi };
7 |
--------------------------------------------------------------------------------
/src/store/constants/userList.js:
--------------------------------------------------------------------------------
1 | export const GET_USER_LIST_REQUEST = "@movieListReducer/GET_USER_LIST_REQUEST ";
2 | export const GET_USER_LIST_SUCCESS = "@movieListReducer/GET_USER_LIST_SUCCESS";
3 | export const GET_USER_LIST_FAIL = "@movieListReducer/GET_USER_LIST_FAIL";
4 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Background/index.jsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { Box } from "@mui/material";
3 |
4 | // Scss
5 | import "./style.scss";
6 |
7 | const Background = () => ;
8 |
9 | export default Background;
10 |
--------------------------------------------------------------------------------
/src/components/Loader/index.js:
--------------------------------------------------------------------------------
1 | import "./Loader.scss";
2 | import Box from "@mui/material/Box";
3 |
4 | const Loader = ({ className = "" }) => (
5 |
6 |
7 |
8 | );
9 |
10 | export default Loader;
11 |
--------------------------------------------------------------------------------
/src/routes/NotFoundRoute.js:
--------------------------------------------------------------------------------
1 | import { lazy } from "react";
2 |
3 | // Pages
4 | const NotFoundPage = lazy(() => import("@/containers/NotFoundPage"));
5 |
6 | const NotFoundRoute = {
7 | path: "*",
8 | element: ,
9 | };
10 |
11 | export default NotFoundRoute;
12 |
--------------------------------------------------------------------------------
/src/store/constants/cinemaSystem.js:
--------------------------------------------------------------------------------
1 | export const GET_CINEMA_LIST_REQUEST = "@cinemaListReducer/GET_CINEMA_LIST_REQUEST ";
2 | export const GET_CINEMA_LIST_SUCCESS = "@cinemaListReducer/GET_CINEMA_LIST_SUCCESS";
3 | export const GET_CINEMA_LIST_FAIL = "@cinemaListReducer/GET_CINEMA_LIST_FAIL";
4 |
--------------------------------------------------------------------------------
/src/store/constants/userDetails.js:
--------------------------------------------------------------------------------
1 | export const GET_USER_DETAILS_REQUEST = "@movieListReducer/GET_USER_DETAILS_REQUEST ";
2 | export const GET_USER_DETAILS_SUCCESS = "@movieListReducer/GET_USER_DETAILS_SUCCESS";
3 | export const GET_USER_DETAILS_FAIL = "@movieListReducer/GET_USER_DETAILS_FAIL";
4 |
--------------------------------------------------------------------------------
/src/store/constants/movieBanner.js:
--------------------------------------------------------------------------------
1 | export const GET_MOVIE_BANNER_REQUEST = "@movieBannerReducer/GET_MOVIE_BANNER_REQUEST";
2 | export const GET_MOVIE_BANNER_SUCCESS = "@movieBannerReducer/GET_MOVIE_BANNER_SUCCESS";
3 | export const GET_MOVIE_BANNER_FAIL = "@movieBannerReducer/GET_MOVIE_BANNER_FAIL";
4 |
--------------------------------------------------------------------------------
/src/store/constants/movieDetails.js:
--------------------------------------------------------------------------------
1 | export const GET_MOVIE_DETAILS_REQUEST = "@movieDetailReducer/GET_MOVIE_DETAILS_REQUEST";
2 | export const GET_MOVIE_DETAILS_SUCCESS = "@movieDetailReducer/GET_MOVIE_DETAILS_SUCCESS";
3 | export const GET_MOVIE_DETAILS_FAIL = "@movieDetailReducer/GET_MOVIE_DETAILS_FAIL";
4 |
--------------------------------------------------------------------------------
/src/components/PageLoader/style.scss:
--------------------------------------------------------------------------------
1 | .page-loader {
2 | min-width: 200px;
3 | position: fixed;
4 | top: 50%;
5 | left: 50%;
6 | transform: translate(-50%, -50%);
7 | text-align: center;
8 | color: var(--primary);
9 |
10 | .linear-progress {
11 | margin-top: 15px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/ProfilePage/style.scss:
--------------------------------------------------------------------------------
1 | .user-profile-page {
2 | background-color: var(--black);
3 | padding: 20px 10px;
4 |
5 | .tab-user-profile {
6 | color: var(--white) !important;
7 |
8 | &.Mui-selected {
9 | color: var(--primary) !important;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/store/constants/movieManagement.js:
--------------------------------------------------------------------------------
1 | export const GET_MOVIE_MANAGEMENT_REQUEST = "@cinemaListReducer/GET_MOVIE_MANAGEMENT_REQUEST ";
2 | export const GET_MOVIE_MANAGEMENT_SUCCESS = "@cinemaListReducer/GET_MOVIE_MANAGEMENT_SUCCESS";
3 | export const GET_MOVIE_MANAGEMENT_FAIL = "@cinemaListReducer/GET_MOVIE_MANAGEMENT_FAIL";
4 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/RegisterPage/style.scss:
--------------------------------------------------------------------------------
1 | .auth-register-form {
2 | .accept-policies .MuiFormControlLabel-label {
3 | font-size: 13px;
4 | }
5 | }
6 |
7 | .auth-link-to-login {
8 | line-height: 1.43;
9 | letter-spacing: 0.01071em;
10 | font-size: 13px;
11 | a {
12 | color: #1976d2;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Button/style.scss:
--------------------------------------------------------------------------------
1 | .auth-button {
2 | padding: 14px;
3 | margin: 15px 0 16px;
4 | background-color: var(--primary);
5 | color: var(--black);
6 | font: {
7 | weight: 700;
8 | size: 15px;
9 | }
10 |
11 | &:hover {
12 | background-color: var(--hover-primary);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/config/apiConfig.js:
--------------------------------------------------------------------------------
1 | const apiConfig = {
2 | authToken:
3 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5Mb3AiOiJCb290Y2FtcCAyNSIsIkhldEhhblN0cmluZyI6IjE2LzEyLzIwMjIiLCJIZXRIYW5UaW1lIjoiMTY3MTE0ODgwMDAwMCIsIm5iZiI6MTY0MTU3NDgwMCwiZXhwIjoxNjcxMjk2NDAwfQ.cB7cdIfS0TKI1Yx_WRS-tEOt5K5yf3QJCot63SYEOHo",
4 | };
5 |
6 | export default apiConfig;
7 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/LoginPage/style.scss:
--------------------------------------------------------------------------------
1 | .auth {
2 | &-login-form {
3 | .remember-login .MuiFormControlLabel-label {
4 | font-size: 13px;
5 | }
6 | }
7 |
8 | &-link-to-register {
9 | line-height: 1.43;
10 | letter-spacing: 0.01071em;
11 | font-size: 13px;
12 | a {
13 | color: #1976d2;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/Buttons/style.scss:
--------------------------------------------------------------------------------
1 | .modal__submit-btn {
2 | background-color: var(--primary);
3 | color: "var(--dark-gray)";
4 |
5 | &:hover {
6 | background-color: var(--hover-primary);
7 | opacity: 0.8;
8 | }
9 | }
10 |
11 | @media (max-width: 739px) {
12 | .movie-management__btn-icon {
13 | font-size: 18px;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/validators/login.js:
--------------------------------------------------------------------------------
1 | import * as yup from "yup";
2 | import pattern from "./pattern";
3 | import msg from "./message";
4 |
5 | const loginSchema = yup.object({
6 | username: yup.string().required(msg.required).matches(pattern.username, msg.username),
7 | password: yup.string().required(msg.required).matches(pattern.password, msg.password),
8 | });
9 |
10 | export default loginSchema;
11 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Button/index.jsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { LoadingButton } from "@mui/lab";
3 |
4 | // Scss
5 | import "./style.scss";
6 |
7 | const Button = ({ children, ...others }) => (
8 |
9 | {children}
10 |
11 | );
12 |
13 | export default Button;
14 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Outlet } from "react-router-dom";
3 |
4 | import Navbar from "./components/Navbar";
5 | import Footer from "./components/Footer";
6 |
7 | function HomeTemplate() {
8 | return (
9 | <>
10 |
11 |
12 |
13 | >
14 | );
15 | }
16 |
17 | export default HomeTemplate;
18 |
--------------------------------------------------------------------------------
/src/i18n/index.js:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 | import LanguageDetector from "i18next-browser-languagedetector";
4 | import Backend from "i18next-http-backend";
5 |
6 | i18n
7 | .use(Backend)
8 | .use(LanguageDetector)
9 | .use(initReactI18next)
10 | .init({
11 | debug: true,
12 | fallbackLng: ["vi", "en"],
13 | });
14 |
15 | export default i18n;
16 |
--------------------------------------------------------------------------------
/src/store/constants/movieList.js:
--------------------------------------------------------------------------------
1 | export const GET_MOVIE_LIST_REQUEST = "@movieListReducer/GET_MOVIE_LIST_REQUEST ";
2 | export const GET_MOVIE_LIST_SUCCESS = "@movieListReducer/GET_MOVIE_LIST_SUCCESS";
3 | export const GET_MOVIE_LIST_FAIL = "@movieListReducer/GET_MOVIE_LIST_FAIL";
4 | export const SET_MOVIE_TYPE_NOW = "@movieListReducer/SET_MOVIE_TYPE_NOW";
5 | export const SET_MOVIE_TYPE_SOON = "@movieListReducer/SET_MOVIE_TYPE_SOON";
6 |
--------------------------------------------------------------------------------
/src/components/Loader/Loader.scss:
--------------------------------------------------------------------------------
1 | .loader {
2 | margin: 200px auto;
3 | border: 8px solid #f3f3f3; /* Light grey */
4 | border-top: 8px solid var(--primary); /* Blue */
5 | border-radius: 50%;
6 | width: 50px;
7 | height: 50px;
8 | animation: spin 1s linear infinite;
9 | }
10 |
11 | @keyframes spin {
12 | 0% {
13 | transform: rotate(0deg);
14 | }
15 | 100% {
16 | transform: rotate(360deg);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.production
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/index.jsx:
--------------------------------------------------------------------------------
1 | import "./style.scss";
2 | import Carousel from "@/containers/HomeTemplate/HomePage/Carousel";
3 | import MovieList from "./MovieList";
4 | import CinemaSystem from "./CinemaSystem";
5 |
6 | function HomePage() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default HomePage;
17 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/ProfilePage/AccountInfo/Input/style.scss:
--------------------------------------------------------------------------------
1 | .account-info__input {
2 | .MuiFilledInput-root {
3 | background-color: var(--white);
4 | border-radius: 0;
5 |
6 | &::after {
7 | border-bottom: none;
8 | }
9 | }
10 |
11 | input {
12 | font-size: 14px;
13 | color: var(--black);
14 | padding: 10px;
15 |
16 | &:read-only {
17 | background-color: #cacaca;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Modal/style.scss:
--------------------------------------------------------------------------------
1 | .ticket-booking-dialog {
2 | .MuiDialog-paper {
3 | background-color: var(--gray);
4 | }
5 |
6 | &__title {
7 | color: var(--primary);
8 | font-weight: 700;
9 | }
10 |
11 | &__info {
12 | color: rgba(255, 255, 255, 0.8);
13 | }
14 |
15 | &__btn-accept {
16 | color: var(--primary);
17 |
18 | &:hover {
19 | background-color: rgba(255, 193, 7, 0.05);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/validators/movieScheduleValidator.js:
--------------------------------------------------------------------------------
1 | import * as yup from "yup";
2 | import msg from "./message";
3 |
4 | const movieScheduleSchema = yup.object({
5 | ngayChieuGioChieu: yup.string().required(msg.required),
6 | maRap: yup.string().required(msg.required),
7 | giaVe: yup
8 | .number(msg.required)
9 | .min(75000, msg.ticketPrice)
10 | .max(200000, msg.ticketPrice)
11 | .required(msg.required),
12 | });
13 |
14 | export default movieScheduleSchema;
15 |
--------------------------------------------------------------------------------
/src/assets/images/index.js:
--------------------------------------------------------------------------------
1 | const images = {
2 | noImage: require("@/assets/images/no-img.jpeg"),
3 | logo: require("@/assets/images/header-logo.png"),
4 | authBackground: require("@/assets/images/auth-background.jpg"),
5 | carousel: require("@/assets/images/nauti_heroesim.jpeg"),
6 | paymentMethodsPC: require("@/assets/images/payment_method_pc.png"),
7 | paymentMethodsMobile: require("@/assets/images/payment_method_mobile.png"),
8 | };
9 |
10 | export default images;
11 |
--------------------------------------------------------------------------------
/src/guard/index.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from "react-router-dom";
2 | import { useAuth } from "@/hooks";
3 |
4 | const RequireAuth = ({ children, roles }) => {
5 | const auth = useAuth();
6 |
7 | if (!auth.user) {
8 | return ;
9 | }
10 |
11 | const isAllowed = roles.includes(auth.user?.role);
12 | if (isAllowed) {
13 | return children;
14 | }
15 |
16 | return ;
17 | };
18 |
19 | export default RequireAuth;
20 |
--------------------------------------------------------------------------------
/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import { useRoutes } from "react-router-dom";
2 |
3 | // Routes
4 | import AuthRoutes from "./AuthRoutes";
5 | import ClientRoutes from "./ClientRoutes";
6 | import AdminRoutes from "./AdminRoutes";
7 | import NotFoundRoute from "./NotFoundRoute";
8 |
9 | // ==============================|| RENDER ROUTES ||============================== //
10 | const ThemeRoutes = () => useRoutes([AuthRoutes, ClientRoutes, AdminRoutes, NotFoundRoute]);
11 |
12 | export default ThemeRoutes;
13 |
--------------------------------------------------------------------------------
/src/components/Image/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import images from "@/assets/images";
3 |
4 | function Image({ src, alt, className, fallback: customeFallback = images.noImage, ...props }) {
5 | const [fallback, setFallback] = useState("");
6 | const handleError = () => {
7 | setFallback(customeFallback);
8 | };
9 | return (
10 |
11 | );
12 | }
13 |
14 | export default Image;
15 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Header/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | // Material UI
4 | import { Avatar } from "@mui/material";
5 |
6 | import FinnkinoLogo from "@/assets/images/header-logo.png";
7 |
8 | const Header = () => (
9 |
10 |
18 |
19 | );
20 |
21 | export default Header;
22 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from "redux";
2 | import thunkMiddleware from "redux-thunk";
3 | import rootReducer from "./reducers";
4 |
5 | const configureStore = () => {
6 | const composeEnhancers =
7 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
8 |
9 | const store = createStore(
10 | rootReducer,
11 | composeEnhancers(applyMiddleware(thunkMiddleware)),
12 | );
13 |
14 | return store;
15 | };
16 |
17 | export default configureStore;
18 |
--------------------------------------------------------------------------------
/src/validators/index.js:
--------------------------------------------------------------------------------
1 | import loginSchema from "./login";
2 | import registerSchema from "./register";
3 | import accountInfoSchema from "./accountInfoValidator";
4 | import { editMovieSchema, addMovieSchema } from "./movieValidator";
5 | import userSchema from "./userValidator";
6 | import movieScheduleSchema from "./movieScheduleValidator";
7 |
8 | export {
9 | loginSchema,
10 | registerSchema,
11 | editMovieSchema,
12 | addMovieSchema,
13 | userSchema,
14 | accountInfoSchema,
15 | movieScheduleSchema,
16 | };
17 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSameLine": false,
4 | "bracketSpacing": true,
5 | "embeddedLanguageFormatting": "auto",
6 | "htmlWhitespaceSensitivity": "css",
7 | "insertPragma": false,
8 | "jsxSingleQuote": false,
9 | "printWidth": 100,
10 | "proseWrap": "preserve",
11 | "quoteProps": "as-needed",
12 | "requirePragma": false,
13 | "semi": true,
14 | "singleQuote": false,
15 | "tabWidth": 2,
16 | "trailingComma": "all",
17 | "useTabs": false,
18 | "vueIndentScriptAndStyle": false
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/PageLoader/index.jsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { Box, LinearProgress } from "@mui/material";
3 |
4 | // Components
5 | import Image from "../Image";
6 | import images from "@/assets/images";
7 |
8 | // Style
9 | import "./style.scss";
10 |
11 | const PageLoader = () => {
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default PageLoader;
21 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Footer/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | // Material UI
4 | import { Typography } from "@mui/material";
5 |
6 | // Scss
7 | import "./style.scss";
8 |
9 | const Footer = () => (
10 |
11 | Copyright ©{" "}
12 |
13 | Finnkino
14 |
15 | {", "}
16 | {new Date().getFullYear()}
17 |
18 | );
19 |
20 | export default Footer;
21 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | const useAuth = () => {
4 | const [user, setUser] = useState(JSON.parse(localStorage.getItem("user")));
5 |
6 | const login = (user) => {
7 | user = { ...user, role: user.maLoaiNguoiDung };
8 | localStorage.setItem("user", JSON.stringify(user));
9 | setUser(user);
10 | };
11 |
12 | const logout = () => {
13 | localStorage.removeItem("user");
14 | setUser({});
15 | };
16 |
17 | return { user, login, logout };
18 | };
19 |
20 | export default useAuth;
21 |
--------------------------------------------------------------------------------
/src/components/MuiPicker/index.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import TextField from "@mui/material/TextField";
3 | import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
4 | import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
5 | import { DatePicker } from "@mui/x-date-pickers/DatePicker";
6 |
7 | export default function MuiDatePicker({ value, onChange }) {
8 | return (
9 | }
13 | />
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 |
3 | // i18next translation
4 | import "./i18n";
5 |
6 | // Redux config
7 | import { Provider } from "react-redux";
8 | import configureStore from "@/store";
9 |
10 | // Components
11 | import App from "@/App";
12 | import GlobalStyles from "@/components/GlobalStyles";
13 |
14 | const store = configureStore();
15 |
16 | const root = ReactDOM.createRoot(document.getElementById("root"));
17 | root.render(
18 |
19 |
20 |
21 |
22 | ,
23 | );
24 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/AdminFooter/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | // Material UI
4 | import { Typography } from "@mui/material";
5 |
6 | // Scss
7 | import "./style.scss";
8 |
9 | const AdminFooter = () => {
10 | return (
11 |
12 | Copyright ©{" "}
13 |
14 | Finnkino
15 |
16 | {", "}
17 | {new Date().getFullYear()}
18 |
19 | );
20 | };
21 |
22 | export default AdminFooter;
23 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Finnkino | Premium Cinemas
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/MovieDashBoard/components/MovieModal/style.scss:
--------------------------------------------------------------------------------
1 | .movie-form__input-label {
2 | &:hover,
3 | &:focus,
4 | &.Mui-focused {
5 | color: var(--primary);
6 | transition: all 0.2s;
7 | cursor: pointer;
8 | }
9 | }
10 |
11 | .movie-form__error {
12 | &.muioutlinedinput-root {
13 | & fieldset {
14 | border-color: red;
15 | }
16 | }
17 | }
18 |
19 | .modal__img {
20 | width: 150px;
21 | height: 150px;
22 | object-fit: cover;
23 | object-position: center;
24 | }
25 |
26 | @media (max-width: 739px) {
27 | .movie-modal {
28 | width: 96%;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/validators/userValidator.js:
--------------------------------------------------------------------------------
1 | import * as yup from "yup";
2 | import pattern from "./pattern";
3 | import msg from "./message";
4 |
5 | const userSchema = yup.object().shape({
6 | taiKhoan: yup.string().required(msg.required),
7 | matKhau: yup.string().required(msg.required).matches(pattern.password, msg.password),
8 | email: yup.string().required(msg.required).email(msg.email),
9 | soDT: yup.string().required(msg.required).matches(pattern.phoneNumber, msg.phoneNumber),
10 | maLoaiNguoiDung: yup.string().required(msg.required),
11 | hoTen: yup.string().required(msg.required),
12 | });
13 |
14 | export default userSchema;
15 |
--------------------------------------------------------------------------------
/src/api/ticketBookingApi.js:
--------------------------------------------------------------------------------
1 | import axiosClient from "./config/axiosClient";
2 |
3 | const resourceName = "QuanLyDatVe/";
4 |
5 | const ticketBookingApi = {
6 | bookTicket: (ticket) => {
7 | const url = resourceName + "DatVe";
8 | return axiosClient.post(url, ticket);
9 | },
10 | getTicketOfficeList: (params) => {
11 | const url = resourceName + "LayDanhSachPhongVe";
12 | return axiosClient.get(url, { params });
13 | },
14 | createShowtime: (showtime) => {
15 | const url = resourceName + "TaoLichChieu";
16 | return axiosClient.post(url, showtime);
17 | },
18 | };
19 |
20 | export default ticketBookingApi;
21 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Background/style.scss:
--------------------------------------------------------------------------------
1 | .auth-background {
2 | width: 100%;
3 | height: 100%;
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 | z-index: -1;
8 | background: url("../../../../assets/images/auth-background.jpg") center / cover no-repeat;
9 |
10 | &::before {
11 | content: "";
12 | width: 100%;
13 | height: 100%;
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | background: rgba(0, 0, 0, 0.3);
18 | background-image: linear-gradient(
19 | 0deg,
20 | rgba(0, 0, 0, 0.8) 0,
21 | transparent 60%,
22 | rgba(0, 0, 0, 0.8)
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/routes/AuthRoutes.js:
--------------------------------------------------------------------------------
1 | import { lazy } from "react";
2 | import { Navigate } from "react-router-dom";
3 |
4 | // Pages
5 | const AuthPage = lazy(() => import("@/containers/AuthTemplate"));
6 | const LoginPage = lazy(() => import("@/containers/AuthTemplate/LoginPage"));
7 | const RegisterPage = lazy(() => import("@/containers/AuthTemplate/RegisterPage"));
8 |
9 | const AuthRoutes = {
10 | path: "auth",
11 | element: ,
12 | children: [
13 | { path: "", element: },
14 | { path: "login", element: },
15 | { path: "register", element: },
16 | ],
17 | };
18 |
19 | export default AuthRoutes;
20 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 |
3 | // Routes config
4 | import { BrowserRouter as Router } from "react-router-dom";
5 | import Routes from "@/routes";
6 |
7 | //Datepicker
8 | import { LocalizationProvider } from "@mui/x-date-pickers";
9 | import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
10 |
11 | // Components
12 | import PageLoader from "./components/PageLoader";
13 |
14 | const App = () => (
15 |
16 | }>
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/src/store/constants/userProfile.js:
--------------------------------------------------------------------------------
1 | // Fetch user profile
2 | export const GET_USER_PROFILE_REQUEST = "@userProfileReducer/GET_USER_PROFILE_REQUEST";
3 | export const GET_USER_PROFILE_SUCCESS = "@userProfileReducer/GET_USER_PROFILE_SUCCESS";
4 | export const GET_USER_PROFILE_FAIL = "@userProfileReducer/GET_USER_PROFILE_FAIL";
5 |
6 | // Update user profile
7 | export const UPDATE_USER_PROFILE_REQUEST = "@userProfileReducer/UPDATE_USER_PROFILE_REQUEST";
8 | export const UPDATE_USER_PROFILE_SUCCESS = "@userProfileReducer/UPDATE_USER_PROFILE_SUCCESS";
9 | export const UPDATE_USER_PROFILE_FAIL = "@userProfileReducer/UPDATE_USER_PROFILE_FAIL";
10 |
11 | // Close modal
12 | export const CLOSE_MODAL = "@userProfileReducer/CLOSE_MODAL";
13 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Input/index.jsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { TextField } from "@mui/material";
3 |
4 | // React hook form
5 | import { useController } from "react-hook-form";
6 |
7 | // Scss
8 | import "./style.scss";
9 |
10 | const Input = ({ control, name, ...others }) => {
11 | const {
12 | field,
13 | fieldState: { error },
14 | } = useController({
15 | name,
16 | control,
17 | });
18 |
19 | return (
20 |
30 | );
31 | };
32 |
33 | export default Input;
34 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/ProfilePage/AccountInfo/Input/index.jsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { TextField } from "@mui/material";
3 |
4 | // React hook form
5 | import { useController } from "react-hook-form";
6 |
7 | // Scss
8 | import "./style.scss";
9 |
10 | const Input = ({ control, name, ...others }) => {
11 | const {
12 | field,
13 | fieldState: { error },
14 | } = useController({
15 | name,
16 | control,
17 | });
18 |
19 | return (
20 |
30 | );
31 | };
32 |
33 | export default Input;
34 |
--------------------------------------------------------------------------------
/src/api/config/axiosClient.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import apiConfig from "./apiConfig";
3 |
4 | const axiosClient = axios.create({
5 | baseURL: process.env.REACT_APP_BASE_URL,
6 | });
7 |
8 | axiosClient.interceptors.request.use(
9 | (config) => {
10 | config.headers.TokenCybersoft = apiConfig.authToken;
11 |
12 | const user = JSON.parse(localStorage.getItem("user"));
13 | if (user) {
14 | config.headers.Authorization = `Bearer ${user?.accessToken}`;
15 | }
16 |
17 | return config;
18 | },
19 | (error) => Promise.reject(error),
20 | );
21 |
22 | axiosClient.interceptors.response.use(
23 | (response) => response.data.content,
24 | (error) => Promise.reject(error.response.data.content),
25 | );
26 |
27 | export default axiosClient;
28 |
--------------------------------------------------------------------------------
/src/validators/register.js:
--------------------------------------------------------------------------------
1 | import * as yup from "yup";
2 | import pattern from "./pattern";
3 | import msg from "./message";
4 |
5 | const registerSchema = yup.object({
6 | fullName: yup.string().required(msg.required).matches(pattern.fullName, msg.fullName),
7 | username: yup.string().required(msg.required).matches(pattern.username, msg.username),
8 | email: yup.string().required(msg.required).email(msg.email),
9 | phoneNumber: yup.string().required(msg.required).matches(pattern.phoneNumber, msg.phoneNumber),
10 | password: yup.string().required(msg.required).matches(pattern.password, msg.password),
11 | confirmedPassword: yup
12 | .string()
13 | .required(msg.required)
14 | .oneOf([yup.ref("password")], msg.confirmedPassword),
15 | });
16 |
17 | export default registerSchema;
18 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/UserDashBoard/component/TableCellList/style.scss:
--------------------------------------------------------------------------------
1 | .movie-table__head-item {
2 | color: var(--dark-gray);
3 | font-weight: 600;
4 | font-size: 16px;
5 |
6 | &.Mui-active {
7 | color: var(--primary);
8 | }
9 | }
10 |
11 | .management-table__table-cell {
12 | --width-s: 100px;
13 | --width-m: 150px;
14 | --width-l: 200px;
15 |
16 | max-height: var(--width-s);
17 | text-align: center;
18 |
19 | &.table-cell__user-number {
20 | width: var(--width-m);
21 | padding: 0;
22 | text-align: center;
23 | }
24 |
25 | &.table-cell__user-account {
26 | width: var(--width-m);
27 | }
28 |
29 | &.table-cell__user-password {
30 | width: var(--width-m);
31 | }
32 |
33 | &.table-cell__user-email {
34 | width: var(--width-l);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/SeatSelector/SeatGrid/style.scss:
--------------------------------------------------------------------------------
1 | .seat-selector {
2 | &__indicator-wrapper {
3 | cursor: pointer;
4 |
5 | &--left {
6 | margin-left: -15px;
7 | }
8 |
9 | &--right {
10 | margin-left: 15px;
11 | }
12 | }
13 |
14 | &__seat-wrapper {
15 | width: 22px;
16 | cursor: pointer;
17 |
18 | &.sold {
19 | cursor: no-drop;
20 |
21 | > .seat-selector__seat {
22 | pointer-events: none;
23 | }
24 | }
25 | }
26 |
27 | &__seat,
28 | &__indicator {
29 | width: 22px;
30 | height: 22px;
31 | font-size: 12px;
32 | text-align: center;
33 | line-height: 22px;
34 | }
35 |
36 | &__indicator {
37 | color: var(--white);
38 | border: 0.5px solid var(--white);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/validators/message/index.js:
--------------------------------------------------------------------------------
1 | const msg = {
2 | required: "Đây là trường bắt buộc.",
3 | fullName: "Họ và tên không hiệu lực.",
4 | username:
5 | "Tài khoản từ 4 - 16 ký tự số và chữ viết thường, không được chứa khoảng trắng hoặc ký tự đặc biệt.",
6 | email: "Địa chỉ email không hiệu lực.",
7 | phoneNumber: "Số điện thoại không hiệu lực.",
8 | password: "Mật khẩu tối thiểu 8 ký tự, ít nhất 1 ký tự chữ và 1 số.",
9 | confirmedPassword: "Xác nhận mật khẩu không đúng.",
10 | url: "Trường này phải là đường dẫn URL",
11 | imageUrl: "Trường này phải là đường dẫn hình ảnh",
12 | movieDesc: "Mô tả phim phải ở dạng chữ cái và không quá 300 kí tự",
13 | rating: "Đánh giá phải có giá trị lớn hơn 0",
14 | ticketPrice: "Giá vé phải lớn hơn 75.000 VNĐ và không quá 200.000 VNĐ",
15 | };
16 |
17 | export default msg;
18 |
--------------------------------------------------------------------------------
/src/api/cinemaApi.js:
--------------------------------------------------------------------------------
1 | import axiosClient from "./config/axiosClient";
2 |
3 | const resourceName = "QuanLyRap/";
4 |
5 | const cinemaApi = {
6 | getCinemaSystemSchedule: (params) => {
7 | const url = resourceName + "LayThongTinLichChieuHeThongRap";
8 | return axiosClient.get(url, { params });
9 | },
10 | getMovieSchedule: (params) => {
11 | const url = resourceName + "LayThongTinLichChieuPhim";
12 | return axiosClient.get(url, { params });
13 | },
14 | getCinemaSystemList: (params) => {
15 | const url = resourceName + "LayThongTinHeThongRap";
16 | return axiosClient.get(url, { params });
17 | },
18 | getCinemaGroupBySystem: (params) => {
19 | const url = resourceName + `LayThongTinCumRapTheoHeThong?maHeThongRap=${params}`;
20 | return axiosClient.get(url);
21 | },
22 | };
23 |
24 | export default cinemaApi;
25 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/components/Card/index.jsx:
--------------------------------------------------------------------------------
1 | import { useLocation } from "react-router-dom";
2 |
3 | // Material UI
4 | import { Stack, Avatar, Typography } from "@mui/material";
5 |
6 | import FinnkinoLogo from "@/assets/images/header-logo.png";
7 |
8 | const Card = ({ children }) => {
9 | const { pathname } = useLocation();
10 |
11 | const cardTitle = pathname === "/auth/login" ? "Đăng Nhập" : "Đăng Ký";
12 |
13 | return (
14 |
15 |
16 |
17 | {cardTitle}
18 |
19 | {children}
20 |
21 | );
22 | };
23 |
24 | export default Card;
25 |
--------------------------------------------------------------------------------
/src/store/constants/ticketBooking.js:
--------------------------------------------------------------------------------
1 | // Fetch ticket booking details
2 | export const GET_TICKET_BOOKING_DETAILS_REQUEST =
3 | "@ticketBookingReducer/GET_TICKET_BOOKING_DETAILS_REQUEST";
4 | export const GET_TICKET_BOOKING_DETAILS_SUCCESS =
5 | "@ticketBookingReducer/GET_TICKET_BOOKING_DETAILS_SUCCESS";
6 | export const GET_TICKET_BOOKING_DETAILS_FAIL =
7 | "@ticketBookingReducer/GET_TICKET_BOOKING_DETAILS_FAIL";
8 |
9 | // Book ticket
10 | export const BOOK_TICKET_REQUEST = "@ticketBookingReducer/BOOK_TICKET_REQUEST";
11 | export const BOOK_TICKET_SUCCESS = "@ticketBookingReducer/BOOK_TICKET_SUCCESS";
12 | export const BOOK_TICKET_FAIL = "@ticketBookingReducer/BOOK_TICKETS__FAIL";
13 |
14 | // Choose seat
15 | export const CHOOSE_SEAT = "@ticketBookingReducer/CHOOSE_SEAT";
16 |
17 | // Close modal
18 | export const CLOSE_MODAL = "@ticketBookingReducer/CLOSE_MODAL";
19 |
--------------------------------------------------------------------------------
/src/validators/accountInfoValidator.js:
--------------------------------------------------------------------------------
1 | import * as yup from "yup";
2 | import pattern from "./pattern";
3 | import msg from "./message";
4 |
5 | const accountInfoSchema = yup.object({
6 | username: yup.string(),
7 | fullName: yup.string(),
8 | email: yup.string().required(msg.required).email(msg.email),
9 | phoneNumber: yup.string().required(msg.required).matches(pattern.phoneNumber, msg.phoneNumber),
10 | currentPasswordRef: yup.string(),
11 | currentPassword: yup
12 | .string()
13 | .oneOf([yup.ref("currentPasswordRef")], "Mật khẩu hiện tại không đúng."),
14 | newPassword: yup.string().required(msg.required).matches(pattern.password, msg.password),
15 | confirmedNewPassword: yup
16 | .string()
17 | .required(msg.required)
18 | .oneOf([yup.ref("newPassword")], msg.confirmedPassword),
19 | });
20 |
21 | export default accountInfoSchema;
22 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/MovieDashBoard/constants.js:
--------------------------------------------------------------------------------
1 | const headCells = [
2 | {
3 | id: "maPhim",
4 | numeric: true,
5 | disablePadding: true,
6 | label: "Mã phim",
7 | sortFunction: true,
8 | },
9 | {
10 | id: "hinhAnh",
11 | numeric: false,
12 | disablePadding: false,
13 | label: "Hình ảnh",
14 | sortFunction: false,
15 | },
16 | {
17 | id: "tenPhim",
18 | numeric: false,
19 | disablePadding: false,
20 | label: "Tên phim",
21 | sortFunction: true,
22 | },
23 | {
24 | id: "moTa",
25 | numeric: false,
26 | disablePadding: false,
27 | label: "Mô tả phim",
28 | sortFunction: true,
29 | },
30 | {
31 | id: "hanhDong",
32 | numeric: false,
33 | disablePadding: false,
34 | label: "Hành động",
35 | sortFunction: false,
36 | },
37 | ];
38 |
39 | export { headCells };
40 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/ProfilePage/AccountInfo/style.scss:
--------------------------------------------------------------------------------
1 | .account-info {
2 | &__input-label {
3 | margin-bottom: 12px;
4 | font-size: 14px;
5 | color: var(--primary);
6 | }
7 |
8 | &__checkbox-change-password {
9 | color: var(--primary);
10 | margin-bottom: 15px;
11 |
12 | .MuiCheckbox-root {
13 | color: var(--white);
14 |
15 | &.Mui-checked {
16 | color: var(--primary);
17 | }
18 | }
19 |
20 | .MuiTypography-root {
21 | font-size: 14px;
22 | }
23 | }
24 |
25 | &__btn-save {
26 | padding: 8px 18px;
27 | border-radius: 0;
28 | margin-top: 15px;
29 | background-color: var(--primary);
30 | color: var(--black);
31 | font: {
32 | weight: 700;
33 | size: 13px;
34 | }
35 |
36 | &:hover {
37 | background-color: var(--hover-primary);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/store/reducers/userList.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/userList";
2 |
3 | const initialState = {
4 | loading: false,
5 | error: null,
6 | data: null,
7 | };
8 |
9 | const userListReducer = (state = initialState, { type, payload }) => {
10 | switch (type) {
11 | case actType.GET_USER_LIST_REQUEST:
12 | state.loading = true;
13 | state.data = null;
14 | state.error = null;
15 | return { ...state };
16 |
17 | case actType.GET_USER_LIST_SUCCESS:
18 | state.loading = false;
19 | state.data = payload;
20 | state.error = null;
21 | return { ...state };
22 |
23 | case actType.GET_USER_LIST_FAIL:
24 | state.loading = false;
25 | state.data = null;
26 | state.error = payload;
27 | return { ...state };
28 |
29 | default:
30 | return { ...state };
31 | }
32 | };
33 |
34 | export default userListReducer;
35 |
--------------------------------------------------------------------------------
/src/store/reducers/userDetails.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/userDetails";
2 |
3 | const initialState = {
4 | loading: false,
5 | data: null,
6 | error: null,
7 | };
8 |
9 | const userDetailsReducer = (state = initialState, action) => {
10 | switch (action.type) {
11 | case actType.GET_USER_DETAILS_REQUEST:
12 | state.loading = true;
13 | state.data = null;
14 | state.error = null;
15 | return { ...state };
16 |
17 | case actType.GET_USER_DETAILS_SUCCESS:
18 | state.loading = false;
19 | state.data = action.payload;
20 | state.error = null;
21 | return { ...state };
22 |
23 | case actType.GET_USER_DETAILS_FAIL:
24 | state.loading = false;
25 | state.data = null;
26 | state.error = action.payload;
27 | return { ...state };
28 |
29 | default:
30 | return { ...state };
31 | }
32 | };
33 |
34 | export default userDetailsReducer;
35 |
--------------------------------------------------------------------------------
/src/validators/pattern/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Regex pattern for input validation:
3 | * - fullName:
4 | * - contains no special characters or numbers
5 | * - Vietnamese name supported
6 | * - username:
7 | * - from 4 to 16 alphanumeric characters, lowercase
8 | * - password:
9 | * - minimum eight characters, at least one letter and one number
10 | */
11 |
12 | const pattern = {
13 | fullName:
14 | /^[a-zA-ZÀÁÂÃÈÉÊÌÍÒÓÔÕÙÚĂĐĨŨƠàáâãèéêìíòóôõùúăđĩũơƯĂẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼỀỀỂẾưăạảấầẩẫậắằẳẵặẹẻẽềềểếỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪễệỉịọỏốồổỗộớờởỡợụủứừỬỮỰỲỴÝỶỸửữựỳỵỷỹ\s\W|_]+$/,
15 | username: /^[a-z0-9]{4,16}$/,
16 | phoneNumber: /(84|0[3|5|7|8|9])+([0-9]{8})\b/,
17 | password: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/,
18 | url: /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/g,
19 | imageUrl: /(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png)/g,
20 | };
21 |
22 | export default pattern;
23 |
--------------------------------------------------------------------------------
/src/routes/AdminRoutes.js:
--------------------------------------------------------------------------------
1 | import { lazy } from "react";
2 | import { Navigate } from "react-router-dom";
3 |
4 | // Route Guard
5 | import RequireAuth from "@/guard";
6 |
7 | // Constants
8 | import { ROLE } from "@/constants";
9 |
10 | // Pages
11 | const AdminTemplate = lazy(() => import("@/containers/AdminTemplate"));
12 | const MovieDashboard = lazy(() => import("@/containers/AdminTemplate/MovieDashBoard"));
13 | const UserDashboard = lazy(() => import("@/containers/AdminTemplate/UserDashBoard"));
14 |
15 | const AdminRoutes = {
16 | path: "admin",
17 | element: (
18 |
19 |
20 |
21 | ),
22 | children: [
23 | { path: "", element: },
24 | { path: "user-management", element: },
25 | { path: "movie-management", element: },
26 | ],
27 | };
28 |
29 | export default AdminRoutes;
30 |
--------------------------------------------------------------------------------
/src/store/reducers/cinemaSystem.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_CINEMA_LIST_REQUEST,
3 | GET_CINEMA_LIST_SUCCESS,
4 | GET_CINEMA_LIST_FAIL,
5 | } from "../constants/cinemaSystem";
6 |
7 | const initialState = {
8 | loading: false,
9 | data: null,
10 | error: null,
11 | };
12 |
13 | const cinemaSystemReducer = (state = initialState, action) => {
14 | switch (action.type) {
15 | case GET_CINEMA_LIST_REQUEST:
16 | state.loading = true;
17 | state.data = null;
18 | state.error = null;
19 | return { ...state };
20 |
21 | case GET_CINEMA_LIST_SUCCESS:
22 | state.loading = false;
23 | state.data = action.payload;
24 | state.error = null;
25 | return { ...state };
26 |
27 | case GET_CINEMA_LIST_FAIL:
28 | state.loading = false;
29 | state.data = null;
30 | state.error = action.payload;
31 | return { ...state };
32 |
33 | default:
34 | return { ...state };
35 | }
36 | };
37 |
38 | export default cinemaSystemReducer;
39 |
--------------------------------------------------------------------------------
/src/store/reducers/movieBanner.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_MOVIE_BANNER_REQUEST,
3 | GET_MOVIE_BANNER_SUCCESS,
4 | GET_MOVIE_BANNER_FAIL,
5 | } from "../constants/movieBanner";
6 |
7 | const initialState = {
8 | loading: false,
9 | data: null,
10 | error: null,
11 | };
12 |
13 | const movieBannerReducer = (state = initialState, action) => {
14 | switch (action.type) {
15 | case GET_MOVIE_BANNER_REQUEST:
16 | state.loading = true;
17 | state.data = null;
18 | state.error = null;
19 | return { ...state };
20 |
21 | case GET_MOVIE_BANNER_SUCCESS:
22 | state.loading = false;
23 | state.data = action.payload;
24 | state.error = null;
25 | return { ...state };
26 |
27 | case GET_MOVIE_BANNER_FAIL:
28 | state.loading = false;
29 | state.data = null;
30 | state.error = action.payload;
31 | return { ...state };
32 |
33 | default:
34 | return { ...state };
35 | }
36 | };
37 |
38 | export default movieBannerReducer;
39 |
--------------------------------------------------------------------------------
/src/store/reducers/movieDetails.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_MOVIE_DETAILS_REQUEST,
3 | GET_MOVIE_DETAILS_SUCCESS,
4 | GET_MOVIE_DETAILS_FAIL,
5 | } from "../constants/movieDetails";
6 |
7 | const initialState = {
8 | loading: false,
9 | data: null,
10 | error: null,
11 | };
12 |
13 | const movieDetailsReducer = (state = initialState, action) => {
14 | switch (action.type) {
15 | case GET_MOVIE_DETAILS_REQUEST:
16 | state.loading = true;
17 | state.data = null;
18 | state.error = null;
19 | return { ...state };
20 |
21 | case GET_MOVIE_DETAILS_SUCCESS:
22 | state.loading = false;
23 | state.data = action.payload;
24 | state.error = null;
25 | return { ...state };
26 |
27 | case GET_MOVIE_DETAILS_FAIL:
28 | state.loading = false;
29 | state.data = null;
30 | state.error = action.payload;
31 | return { ...state };
32 |
33 | default:
34 | return { ...state };
35 | }
36 | };
37 |
38 | export default movieDetailsReducer;
39 |
--------------------------------------------------------------------------------
/src/store/constants/userManagement.js:
--------------------------------------------------------------------------------
1 | export const GET_USER_DELETE_REQUEST = "@movieListReducer/GET_USER_DELETE_REQUEST ";
2 | export const GET_USER_DELETE_SUCCESS = "@movieListReducer/GET_USER_DELETE_SUCCESS";
3 | export const GET_USER_DELETE_FAIL = "@movieListReducer/GET_USER_DELETE_FAIL";
4 | export const GET_USER_ADD_REQUEST = "@movieListReducer/GET_USER_ADD_REQUEST ";
5 | export const GET_USER_ADD_SUCCESS = "@movieListReducer/GET_USER_ADD_SUCCESS";
6 | export const GET_USER_ADD_FAIL = "@movieListReducer/GET_USER_ADD_FAIL";
7 | export const GET_USER_EDIT_REQUEST = "@movieListReducer/GET_USER_EDIT_REQUEST ";
8 | export const GET_USER_EDIT_SUCCESS = "@movieListReducer/GET_USER_EDIT_SUCCESS";
9 | export const GET_USER_EDIT_FAIL = "@movieListReducer/GET_USER_EDIT_FAIL";
10 | export const GET_USER_SEARCH_REQUEST = "@movieListReducer/GET_USER_SEARCH_REQUEST ";
11 | export const GET_USER_SEARCH_SUCCESS = "@movieListReducer/GET_USER_SEARCH_SUCCESS";
12 | export const GET_USER_SEARCH_FAIL = "@movieListReducer/GET_USER_SEARCH_FAIL";
13 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/UserDashBoard/constants.js:
--------------------------------------------------------------------------------
1 | const headCells = [
2 | {
3 | id: "STT",
4 | numeric: false,
5 | disablePadding: true,
6 | label: "Số thứ tự",
7 | sortFunction: true,
8 | },
9 | {
10 | id: "taiKhoan",
11 | numeric: false,
12 | disablePadding: false,
13 | label: "Tài khoản",
14 | sortFunction: false,
15 | },
16 | {
17 | id: "matKhau",
18 | numeric: false,
19 | disablePadding: false,
20 | label: "Mật khẩu",
21 | sortFunction: false,
22 | },
23 | {
24 | id: "email",
25 | numeric: false,
26 | disablePadding: false,
27 | label: "Email",
28 | sortFunction: true,
29 | },
30 | {
31 | id: "soDienThoai",
32 | numeric: false,
33 | disablePadding: false,
34 | label: "Số điện thoại",
35 | sortFunction: true,
36 | },
37 | {
38 | id: "hanhDong",
39 | numeric: false,
40 | disablePadding: false,
41 | label: "Hành động",
42 | sortFunction: false,
43 | },
44 | ];
45 |
46 | export default headCells;
47 |
--------------------------------------------------------------------------------
/src/store/reducers/movieManagement.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_MOVIE_MANAGEMENT_REQUEST,
3 | GET_MOVIE_MANAGEMENT_SUCCESS,
4 | GET_MOVIE_MANAGEMENT_FAIL,
5 | } from "../constants/movieManagement";
6 |
7 | const initialState = {
8 | loading: false,
9 | error: null,
10 | data: null,
11 | };
12 |
13 | const movieManagementReducer = (state = initialState, action) => {
14 | switch (action.type) {
15 | case GET_MOVIE_MANAGEMENT_REQUEST:
16 | state.loading = true;
17 | state.data = null;
18 | state.error = null;
19 | return { ...state };
20 |
21 | case GET_MOVIE_MANAGEMENT_SUCCESS:
22 | state.loading = false;
23 | state.data = action.payload;
24 | state.error = null;
25 | return { ...state };
26 |
27 | case GET_MOVIE_MANAGEMENT_FAIL:
28 | state.loading = false;
29 | state.data = null;
30 | state.error = action.payload;
31 | return { ...state };
32 |
33 | default:
34 | return { ...state };
35 | }
36 | };
37 |
38 | export default movieManagementReducer;
39 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/SeatSelector/SeatNote/index.jsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { Box, Typography, Grid, Stack } from "@mui/material";
3 |
4 | // Scss
5 | import "./style.scss";
6 |
7 | const seatNoteItems = [
8 | {
9 | type: "selected",
10 | content: "Ghế đang chọn",
11 | },
12 | {
13 | type: "sold",
14 | content: "Ghế đã bán",
15 | },
16 | {
17 | type: "vip",
18 | content: "Ghế VIP",
19 | },
20 | {
21 | type: "selectable",
22 | content: "Có thể chọn",
23 | },
24 | {
25 | type: "unavailable",
26 | content: "Không thể chọn",
27 | },
28 | ];
29 |
30 | const SeatNote = () =>
31 | seatNoteItems.map((item, idx) => (
32 |
33 |
34 |
35 | {item.content}
36 |
37 |
38 | ));
39 |
40 | export default SeatNote;
41 |
--------------------------------------------------------------------------------
/src/store/actions/userManagement.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/userManagement";
2 | import { GROUP_ID } from "@/constants";
3 | import { userApi } from "@/api";
4 |
5 | const actUserDeleteRequest = () => {
6 | return {
7 | type: actType.GET_USER_DELETE_REQUEST,
8 | };
9 | };
10 |
11 | const actUserDeleteSuccess = (data) => {
12 | return {
13 | type: actType.GET_USER_DELETE_SUCCESS,
14 | payload: data,
15 | };
16 | };
17 |
18 | const actUserDeleteFail = (error) => {
19 | return {
20 | type: actType.GET_USER_DELETE_FAIL,
21 | payload: error,
22 | };
23 | };
24 |
25 | const actGetUserDetele = (userAccount) => {
26 | return (dispatch) => {
27 | dispatch(actUserDeleteRequest());
28 | const fetchUserDelete = async () => {
29 | try {
30 | const userDelete = await userApi.deleteUser(userAccount);
31 | dispatch(actUserDeleteSuccess(userDelete));
32 | } catch (error) {
33 | dispatch(actUserDeleteFail(error));
34 | }
35 | };
36 |
37 | fetchUserDelete();
38 | };
39 | };
40 |
41 | export { actGetUserDetele };
42 |
--------------------------------------------------------------------------------
/src/store/actions/movieBanner.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_MOVIE_BANNER_REQUEST,
3 | GET_MOVIE_BANNER_SUCCESS,
4 | GET_MOVIE_BANNER_FAIL,
5 | } from "../constants/movieBanner";
6 | import { movieApi } from "@/api";
7 |
8 | const actFetchBanners = () => {
9 | return (dispatch) => {
10 | dispatch(actMovieBannerRequest());
11 | const fetchMovieBanners = async () => {
12 | try {
13 | const banners = await movieApi.getBannerList();
14 | dispatch(actMovieBannerSuccess(banners));
15 | } catch (error) {
16 | dispatch(actMovieBannerFail(error));
17 | }
18 | };
19 | fetchMovieBanners();
20 | };
21 | };
22 |
23 | const actMovieBannerRequest = () => {
24 | return {
25 | type: GET_MOVIE_BANNER_REQUEST,
26 | };
27 | };
28 |
29 | const actMovieBannerSuccess = (data) => {
30 | return {
31 | type: GET_MOVIE_BANNER_SUCCESS,
32 | payload: data,
33 | };
34 | };
35 |
36 | const actMovieBannerFail = (error) => {
37 | return {
38 | type: GET_MOVIE_BANNER_FAIL,
39 | payload: error,
40 | };
41 | };
42 |
43 | export default actFetchBanners;
44 |
--------------------------------------------------------------------------------
/src/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 |
3 | // Reducers
4 | import movieListReducer from "./movieList";
5 | import movieBannerReducer from "./movieBanner";
6 | import movieDetailsReducer from "./movieDetails";
7 | import cinemaSystemReducer from "./cinemaSystem";
8 | import movieManagementReducer from "./movieManagement";
9 | import userDetailsReducer from "./userDetails";
10 | import userManagementReducer from "./userManagement";
11 | import userListReducer from "./userList";
12 | import ticketBookingReducer from "./ticketBooking";
13 | import userProfileReducer from "./userProfile";
14 |
15 | const rootReducer = combineReducers({
16 | movieList: movieListReducer,
17 | movieBanner: movieBannerReducer,
18 | movieDetails: movieDetailsReducer,
19 | cinemaSystem: cinemaSystemReducer,
20 | movieManagement: movieManagementReducer,
21 | userDetails: userDetailsReducer,
22 | userManagement: userManagementReducer,
23 | userList: userListReducer,
24 | ticketBooking: ticketBookingReducer,
25 | userProfile: userProfileReducer,
26 | });
27 |
28 | export default rootReducer;
29 |
--------------------------------------------------------------------------------
/src/store/actions/cinemaSystem.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_CINEMA_LIST_REQUEST,
3 | GET_CINEMA_LIST_SUCCESS,
4 | GET_CINEMA_LIST_FAIL,
5 | } from "../constants/cinemaSystem";
6 | import { cinemaApi } from "@/api";
7 |
8 | const actGetCinemaList = () => {
9 | return (dispatch) => {
10 | dispatch(actGetCinemaListRequest());
11 | const fetchCinemaList = async () => {
12 | const result = await cinemaApi.getCinemaSystemSchedule();
13 | try {
14 | dispatch(actGetCinemaListSuccess(result));
15 | } catch (error) {
16 | dispatch(actGetCinemaListFail(error));
17 | }
18 | };
19 |
20 | fetchCinemaList();
21 | };
22 | };
23 |
24 | const actGetCinemaListRequest = () => {
25 | return {
26 | type: GET_CINEMA_LIST_REQUEST,
27 | };
28 | };
29 |
30 | const actGetCinemaListSuccess = (data) => {
31 | return {
32 | type: GET_CINEMA_LIST_SUCCESS,
33 | payload: data,
34 | };
35 | };
36 |
37 | const actGetCinemaListFail = (error) => {
38 | return {
39 | type: GET_CINEMA_LIST_FAIL,
40 | payload: error,
41 | };
42 | };
43 |
44 | export default actGetCinemaList;
45 |
--------------------------------------------------------------------------------
/src/store/actions/userList.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/userList";
2 | import { GROUP_ID } from "@/constants";
3 | import { userApi } from "@/api";
4 |
5 | const actGetUserList = (keyword = "") => {
6 | return (dispatch) => {
7 | dispatch(actUserListRequest());
8 |
9 | const fetchUserList = async () => {
10 | try {
11 | const params = { maNhom: GROUP_ID };
12 | const userList = await userApi.getUserList(params, keyword);
13 | dispatch(actUserListSuccess(userList));
14 | } catch (error) {
15 | dispatch(actUserListFail(error));
16 | }
17 | };
18 |
19 | fetchUserList();
20 | };
21 | };
22 |
23 | const actUserListRequest = () => {
24 | return {
25 | type: actType.GET_USER_LIST_REQUEST,
26 | };
27 | };
28 |
29 | const actUserListSuccess = (data) => {
30 | return {
31 | type: actType.GET_USER_LIST_SUCCESS,
32 | payload: data,
33 | };
34 | };
35 |
36 | const actUserListFail = (error) => {
37 | return {
38 | type: actType.GET_USER_LIST_FAIL,
39 | payload: error,
40 | };
41 | };
42 |
43 | export default actGetUserList;
44 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/CinemaSystem/MovieSchedule/style.scss:
--------------------------------------------------------------------------------
1 | .movie-schedule-card {
2 | --movie-schedule-height: 120px;
3 | flex-direction: row;
4 | padding: 10px 0;
5 | margin-bottom: 10px;
6 | color: var(--light-gray);
7 | border-bottom: 1px solid #313131;
8 |
9 | img {
10 | width: 100%;
11 | height: var(--movie-schedule-height);
12 | object-fit: cover;
13 | object-position: center;
14 | }
15 |
16 | .movie-schedule__info {
17 | width: 100%;
18 | height: var(--movie-schedule-height);
19 | }
20 | }
21 |
22 | .movie-schedule__movie-name {
23 | color: var(--primary);
24 | font-size: 18px;
25 | margin-bottom: 6px;
26 | }
27 |
28 | .movie-schedule__schedule-list {
29 | margin-top: 8px;
30 | }
31 | .movie-schedule__schedule-item {
32 | display: inline-block;
33 | margin-top: 2px;
34 | color: var(--primary);
35 | }
36 |
37 | @media (max-width: 1023px) {
38 | .movie-schedule-card {
39 | img {
40 | height: 100px;
41 | }
42 | }
43 | }
44 |
45 | @media (min-width: 740px) and (max-width: 1023px) {
46 | }
47 |
48 | @media (max-width: 739px) {
49 | }
50 |
--------------------------------------------------------------------------------
/src/store/actions/movieDetails.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_MOVIE_DETAILS_REQUEST,
3 | GET_MOVIE_DETAILS_SUCCESS,
4 | GET_MOVIE_DETAILS_FAIL,
5 | } from "../constants/movieDetails";
6 | import { movieApi } from "@/api";
7 |
8 | const actFetchMovieDetails = (movieId) => {
9 | return (dispatch) => {
10 | dispatch(actMovieDetailsRequest());
11 |
12 | const fetchMovieDetails = async () => {
13 | try {
14 | const movieDetails = await movieApi.getMovieDetails(movieId);
15 | dispatch(actMovieDetailsSuccess(movieDetails));
16 | } catch (error) {
17 | dispatch(actMovieDetailsFail(error));
18 | }
19 | };
20 |
21 | fetchMovieDetails();
22 | };
23 | };
24 |
25 | const actMovieDetailsRequest = () => {
26 | return {
27 | type: GET_MOVIE_DETAILS_REQUEST,
28 | };
29 | };
30 |
31 | const actMovieDetailsSuccess = (data) => {
32 | return {
33 | type: GET_MOVIE_DETAILS_SUCCESS,
34 | payload: data,
35 | };
36 | };
37 |
38 | const actMovieDetailsFail = (error) => {
39 | return {
40 | type: GET_MOVIE_DETAILS_FAIL,
41 | payload: error,
42 | };
43 | };
44 |
45 | export default actFetchMovieDetails;
46 |
--------------------------------------------------------------------------------
/src/store/actions/movieList.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/movieList";
2 | import { GROUP_ID } from "@/constants";
3 | import { movieApi } from "@/api";
4 |
5 | const actGetMovieList = (movieName = "") => {
6 | return (dispatch) => {
7 | dispatch(actGetMovieListRequest());
8 |
9 | const fetchMovieList = async () => {
10 | try {
11 | const params = { maNhom: GROUP_ID };
12 | const movieList = await movieApi.getMovieList(params, movieName);
13 | dispatch(actGetMovieListSuccess(movieList));
14 | } catch (error) {
15 | dispatch(actGetMovieListFail(error));
16 | }
17 | };
18 |
19 | fetchMovieList();
20 | };
21 | };
22 |
23 | const actGetMovieListRequest = () => {
24 | return {
25 | type: actType.GET_MOVIE_LIST_REQUEST,
26 | };
27 | };
28 |
29 | const actGetMovieListSuccess = (data) => {
30 | return {
31 | type: actType.GET_MOVIE_LIST_SUCCESS,
32 | payload: data,
33 | };
34 | };
35 |
36 | const actGetMovieListFail = (error) => {
37 | return {
38 | type: actType.GET_MOVIE_LIST_FAIL,
39 | payload: error,
40 | };
41 | };
42 |
43 | export default actGetMovieList;
44 |
--------------------------------------------------------------------------------
/src/store/reducers/movieList.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/movieList";
2 |
3 | const initialState = {
4 | loading: false,
5 | error: null,
6 | data: null,
7 | movieType: "now",
8 | };
9 |
10 | const movieListReducer = (state = initialState, { type, payload }) => {
11 | switch (type) {
12 | case actType.GET_MOVIE_LIST_REQUEST:
13 | state.loading = true;
14 | state.data = null;
15 | state.error = null;
16 | return { ...state };
17 |
18 | case actType.GET_MOVIE_LIST_SUCCESS:
19 | state.loading = false;
20 | state.data = payload;
21 | state.error = null;
22 | return { ...state };
23 |
24 | case actType.GET_MOVIE_LIST_FAIL:
25 | state.loading = false;
26 | state.data = null;
27 | state.error = payload;
28 | return { ...state };
29 |
30 | case actType.SET_MOVIE_TYPE_NOW:
31 | state.movieType = "now";
32 | return { ...state };
33 |
34 | case actType.SET_MOVIE_TYPE_SOON:
35 | state.movieType = "soon";
36 | return { ...state };
37 |
38 | default:
39 | return { ...state };
40 | }
41 | };
42 |
43 | export default movieListReducer;
44 |
--------------------------------------------------------------------------------
/src/store/actions/userDetails.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/userDetails";
2 | import { GROUP_ID } from "@/constants";
3 | import { userApi } from "@/api";
4 |
5 | const actUserDetailsRequest = () => {
6 | return {
7 | type: actType.GET_USER_DETAILS_REQUEST,
8 | };
9 | };
10 |
11 | export const actUserDetailsSuccess = (data) => {
12 | return {
13 | type: actType.GET_USER_DETAILS_SUCCESS,
14 | payload: data,
15 | };
16 | };
17 |
18 | const actUserDetailsFail = (error) => {
19 | return {
20 | type: actType.GET_USER_DETAILS_FAIL,
21 | payload: error,
22 | };
23 | };
24 |
25 | const actGetUserDetails = (keyword = "") => {
26 | return (dispatch) => {
27 | dispatch(actUserDetailsRequest());
28 |
29 | const fetchUserDetails = async () => {
30 | try {
31 | const params = { maNhom: GROUP_ID };
32 | const userDetails = await userApi.getUserDetails(params, keyword);
33 | dispatch(actUserDetailsSuccess(userDetails));
34 | } catch (error) {
35 | dispatch(actUserDetailsFail(error));
36 | }
37 | };
38 |
39 | fetchUserDetails();
40 | };
41 | };
42 |
43 | export default actGetUserDetails;
44 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/AdminDrawer/style.scss:
--------------------------------------------------------------------------------
1 | .admin-drawer {
2 | &__avatar {
3 | position: absolute !important;
4 | top: 50%;
5 | left: 50%;
6 | transform: translate(-50%, -50%);
7 | width: var(--logo-size) !important;
8 | height: var(--logo-size) !important;
9 | }
10 |
11 | nav > div:first-child {
12 | margin-bottom: 7px;
13 | }
14 |
15 | &__divider {
16 | background-color: var(--light-gray);
17 | }
18 |
19 | &-list {
20 | color: var(--white) !important;
21 | }
22 |
23 | &-list__item {
24 | position: relative;
25 | transition: padding-left 0.3s !important;
26 |
27 | &.active {
28 | background-color: var(--light-gray) !important;
29 | }
30 |
31 | &::after {
32 | background-color: var(--primary);
33 |
34 | content: "";
35 | display: block;
36 | width: 0;
37 | height: 3px;
38 |
39 | position: absolute;
40 | top: 50%;
41 | left: 10px;
42 | transform: translateY(-50%);
43 |
44 | transition: width 0.3s !important;
45 | }
46 |
47 | &:not(.active):hover {
48 | background-color: var(--light-gray) !important;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/index.jsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 | import { Navigate } from "react-router-dom";
3 | import { useAuth } from "@/hooks";
4 |
5 | // Material UI
6 | import { Box, Grid, Stack } from "@mui/material";
7 |
8 | // Components
9 | import AuthBackground from "./components/Background";
10 | import AuthHeader from "./components/Header";
11 | import AuthCard from "./components/Card";
12 | import AuthFooter from "./components/Footer";
13 |
14 | const AuthTemplate = () => {
15 | const auth = useAuth();
16 |
17 | if (auth.user) {
18 | return ;
19 | }
20 |
21 | return (
22 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default AuthTemplate;
47 |
--------------------------------------------------------------------------------
/src/routes/ClientRoutes.js:
--------------------------------------------------------------------------------
1 | import { lazy } from "react";
2 |
3 | // Route Guard
4 | import RequireAuth from "@/guard";
5 |
6 | // Constants
7 | import { ROLE } from "@/constants";
8 |
9 | // Pages
10 | const HomeTemp = lazy(() => import("@/containers/HomeTemplate"));
11 | const HomePage = lazy(() => import("@/containers/HomeTemplate/HomePage"));
12 | const MovieDetailPage = lazy(() => import("@/containers/HomeTemplate/MovieDetailsPage"));
13 | const TicketBookingPage = lazy(() => import("@/containers/HomeTemplate/TicketBookingPage"));
14 | const ProfilePage = lazy(() => import("@/containers/HomeTemplate/ProfilePage"));
15 |
16 | const ClientRoutes = {
17 | path: "/",
18 | element: ,
19 | children: [
20 | {
21 | path: "",
22 | element: ,
23 | },
24 | { path: "movie-detail/:id", element: },
25 | {
26 | path: "ticket-booking/:scheduleId",
27 | element: (
28 |
29 |
30 |
31 | ),
32 | },
33 | {
34 | path: "profile",
35 | element: (
36 |
37 |
38 |
39 | ),
40 | },
41 | ],
42 | };
43 |
44 | export default ClientRoutes;
45 |
--------------------------------------------------------------------------------
/src/store/reducers/userManagement.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/userManagement";
2 |
3 | const initialState = {
4 | loading: false,
5 | error: null,
6 | success: false,
7 | };
8 |
9 | const userManagementReducer = (state = initialState, action) => {
10 | switch (action.type) {
11 | case actType.GET_USER_DELETE_REQUEST:
12 | case actType.GET_USER_ADD_REQUEST:
13 | case actType.GET_USER_EDIT_REQUEST:
14 | case actType.GET_USER_SEARCH_REQUEST:
15 | state.loading = true;
16 | state.success = false;
17 | state.error = null;
18 | return { ...state };
19 |
20 | case actType.GET_USER_DELETE_SUCCESS:
21 | case actType.GET_USER_ADD_SUCCESS:
22 | case actType.GET_USER_EDIT_SUCCESS:
23 | case actType.GET_USER_SEARCH_SUCCESS:
24 | state.loading = false;
25 | state.success = true;
26 | state.error = null;
27 | return { ...state };
28 |
29 | case actType.GET_USER_DELETE_FAIL:
30 | case actType.GET_USER_ADD_FAIL:
31 | case actType.GET_USER_EDIT_FAIL:
32 | case actType.GET_USER_SEARCH_FAIL:
33 | state.loading = false;
34 | state.success = false;
35 | state.error = action.payload;
36 | return { ...state };
37 |
38 | default:
39 | return { ...state };
40 | }
41 | };
42 |
43 | export default userManagementReducer;
44 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/components/Footer/style.scss:
--------------------------------------------------------------------------------
1 | footer {
2 | background-color: var(--dark-gray);
3 | }
4 |
5 | .footer__apps {
6 | margin-top: 50px;
7 | padding: 50px 0 30px 0;
8 | justify-content: center;
9 | }
10 |
11 | .footer__app-item + .footer__app-item {
12 | margin-left: 30px;
13 | }
14 |
15 | .footer__list-item {
16 | margin-bottom: 16px;
17 | .footer__list-title {
18 | color: var(--white);
19 | font-weight: 500;
20 | margin-bottom: 2px;
21 | }
22 |
23 | .footer__list-link {
24 | display: block;
25 | font-size: 13px;
26 | text-decoration: none;
27 | color: #ffffff8a;
28 |
29 | &:hover {
30 | color: #ffffff8a;
31 | cursor: pointer;
32 | text-decoration: underline;
33 | }
34 | }
35 | }
36 |
37 | .list--social {
38 | .footer__list-link {
39 | display: inline-block;
40 | font-size: 20px;
41 | margin-right: 6px;
42 | cursor: pointer;
43 | }
44 | }
45 |
46 | .footer__payment {
47 | justify-content: center;
48 | img {
49 | padding-top: 40px;
50 | width: 90%;
51 | margin: 0 -15px;
52 | opacity: 0.6;
53 | }
54 | }
55 |
56 | .footer__rights {
57 | padding: 50px 0;
58 | font-size: 13px;
59 | color: var(--white);
60 | width: 100%;
61 | text-align: center;
62 | }
63 |
64 | @media (max-width: 1023px) {
65 | .footer__apps {
66 | margin-top: 0;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/AdminDrawer/DrawerItems.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate, useLocation } from "react-router-dom";
2 |
3 | // Material UI
4 | import { ListItemButton, ListItemIcon, ListItemText } from "@mui/material";
5 | import { PeopleAlt, LocalMovies, CalendarMonth } from "@mui/icons-material";
6 |
7 | const items = [
8 | {
9 | Icon: PeopleAlt,
10 | label: "Quản lý người dùng",
11 | path: "user-management",
12 | },
13 | {
14 | Icon: LocalMovies,
15 | label: "Quản lý phim",
16 | path: "movie-management",
17 | },
18 | ];
19 |
20 | const DrawerItems = () => {
21 | const navigate = useNavigate();
22 | const { pathname } = useLocation();
23 |
24 | const handleClick = (path) => navigate(path);
25 |
26 | const active = items.findIndex((item) => {
27 | const subpath = pathname.split("/");
28 | return item.path === subpath[2];
29 | });
30 |
31 | return items.map((Item, idx) => (
32 | handleClick(Item.path)}
36 | >
37 |
38 |
39 |
40 | span": { fontSize: "16px" } }} />
41 |
42 | ));
43 | };
44 | export default DrawerItems;
45 |
--------------------------------------------------------------------------------
/src/validators/movieValidator.js:
--------------------------------------------------------------------------------
1 | import * as yup from "yup";
2 | import pattern from "./pattern";
3 | import msg from "./message";
4 |
5 | const editMovieSchema = yup.object({
6 | tenPhim: yup.string().required(msg.required),
7 | trailer: yup.string().required(msg.required).matches(pattern.url, msg.url),
8 | moTa: yup.string().required(msg.required),
9 | ngayKhoiChieu: yup.string().required(msg.required),
10 | dangChieu: yup.boolean().required(msg.required).typeError(msg.required),
11 | sapChieu: yup.boolean().required(msg.required).typeError(msg.required),
12 | hot: yup.boolean().required(msg.required).typeError(msg.required),
13 | danhGia: yup.number().min(1, msg.rating).required(msg.required),
14 | hinhAnh: yup.object(msg.required).nullable(),
15 | });
16 |
17 | const addMovieSchema = yup.object({
18 | tenPhim: yup.string().required(msg.required),
19 | trailer: yup.string().required(msg.required).matches(pattern.url, msg.url),
20 | moTa: yup.string().required(msg.required),
21 | ngayKhoiChieu: yup.string().required(msg.required),
22 | dangChieu: yup.boolean().required(msg.required).typeError(msg.required),
23 | sapChieu: yup.boolean().required(msg.required).typeError(msg.required),
24 | hot: yup.boolean().required(msg.required).typeError(msg.required),
25 | danhGia: yup.number().min(1, msg.rating).required(msg.required),
26 | hinhAnh: yup.mixed().required(msg.required),
27 | });
28 |
29 | export { editMovieSchema, addMovieSchema };
30 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/SeatSelector/style.scss:
--------------------------------------------------------------------------------
1 | .seat-selector {
2 | padding: 10px 14px;
3 | background-color: var(--white);
4 |
5 | // Mobile and Tablet
6 | @media (max-width: 1023px) {
7 | padding: 8px 12px;
8 | }
9 |
10 | // Seat colors
11 | .selected {
12 | color: var(--white);
13 | background-color: #7dc71d;
14 | }
15 |
16 | .sold {
17 | color: var(--white);
18 | background-color: #e11c01;
19 | }
20 |
21 | .vip {
22 | background-color: var(--primary);
23 | }
24 |
25 | .selectable {
26 | background-color: #dbdee1;
27 | }
28 |
29 | .unavailable {
30 | background-color: #0cbfca;
31 | }
32 |
33 | &__title {
34 | margin-bottom: 10px;
35 | font-size: 25px;
36 |
37 | // Mobile and Tablet
38 | @media (max-width: 1023px) {
39 | font-size: 20px;
40 | }
41 | }
42 |
43 | &__map {
44 | &-wrapper {
45 | padding: 24px;
46 | background-color: var(--dark-gray);
47 | }
48 |
49 | &-grid {
50 | margin: 0 auto;
51 | width: 460px;
52 | }
53 | }
54 |
55 | &__screen {
56 | height: 7px;
57 | width: 70%;
58 | margin-bottom: 20px;
59 | margin: 20px auto;
60 | background-color: var(--primary);
61 |
62 | &-title {
63 | text-align: center;
64 | margin-bottom: 15px;
65 | }
66 | }
67 |
68 | &__seat-notes-container {
69 | margin-top: 23px;
70 | color: var(--white);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/CinemaSystem/style.scss:
--------------------------------------------------------------------------------
1 | //Cinema System
2 | .cinema-system-wrapper {
3 | background-color: var(--dark);
4 | }
5 |
6 | .cinema-system__item {
7 | margin: 12px;
8 |
9 | &.active,
10 | &:hover {
11 | background-color: var(--gray);
12 | border-radius: 50%;
13 | cursor: pointer;
14 | }
15 | }
16 |
17 | .cinema-system__logo {
18 | height: 100%;
19 | width: 100%;
20 | object-fit: cover;
21 | border-radius: 50%;
22 | }
23 |
24 | //Cinema Group
25 | .cinema-group__content-wrapper {
26 | max-height: 288px;
27 | background-color: transparent;
28 | }
29 |
30 | .cinema-system__item-content {
31 | width: 100%;
32 | color: var(--white);
33 | }
34 |
35 | .cinema-group__tab-item {
36 | display: flex;
37 | color: var(--light-gray);
38 | text-align: left;
39 | flex-direction: row;
40 | justify-content: flex-start;
41 | align-items: center;
42 |
43 | &.Mui-selected {
44 | color: var(--primary);
45 | }
46 |
47 | .cinema-group__tab-img {
48 | width: calc(var(--logo-size) - 10px);
49 | margin: 0px 18px 0 0;
50 | }
51 | }
52 |
53 | .cinema-group__tab-panels {
54 | flex: 2;
55 | width: 100%;
56 | }
57 |
58 | .cinema-group__panel-item {
59 | max-height: 288px;
60 | overflow: auto;
61 |
62 | &::-webkit-scrollbar {
63 | width: 2px;
64 | }
65 |
66 | &::-webkit-scrollbar-track {
67 | background-color: transparent;
68 | }
69 |
70 | &::-webkit-scrollbar-thumb {
71 | background: var(--primary);
72 | height: 68px;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/index.jsx:
--------------------------------------------------------------------------------
1 | import { useScrollToTop } from "@/hooks";
2 | import { useEffect } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { useParams } from "react-router-dom";
5 |
6 | // Material UI
7 | import { Box, Container, Grid } from "@mui/material";
8 |
9 | // Components
10 | import TicketBookingCard from "./TicketBookingCard";
11 | import SeatSelector from "./SeatSelector";
12 | import Modal from "@/components/Modal";
13 |
14 | // Redux actions
15 | import { actGetTicketBookingDetails, actCloseModal } from "@/store/actions/ticketBooking";
16 |
17 | // Scss
18 | import "./style.scss";
19 |
20 | const TicketBookingPage = () => {
21 | useScrollToTop();
22 | const { scheduleId } = useParams();
23 | const dispatch = useDispatch();
24 | const modalProps = useSelector((rootReducer) => rootReducer.ticketBooking.modal);
25 |
26 | useEffect(() => {
27 | dispatch(actGetTicketBookingDetails(scheduleId));
28 | }, []);
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default TicketBookingPage;
48 |
--------------------------------------------------------------------------------
/src/api/movieApi.js:
--------------------------------------------------------------------------------
1 | import axiosClient from "./config/axiosClient";
2 |
3 | const resourceName = "QuanLyPhim/";
4 |
5 | const movieApi = {
6 | getBannerList: () => {
7 | const url = resourceName + "LayDanhSachBanner";
8 | return axiosClient.get(url);
9 | },
10 | getMovieList: (params, movieName) => {
11 | let url;
12 | if (movieName !== "") {
13 | url = resourceName + `LayDanhSachPhim?maNhom=${params.maNhom}&tenPhim=${movieName}`;
14 | return axiosClient.get(url);
15 | } else {
16 | url = resourceName + "LayDanhSachPhim";
17 | return axiosClient.get(url, { params });
18 | }
19 | },
20 | getPaginatedMovieList: (params) => {
21 | const url = resourceName + "LayDanhSachPhimPhanTrang";
22 | return axiosClient.get(url, { params });
23 | },
24 | getMovieListByDate: (params) => {
25 | const url = resourceName + "LayDanhSachPhimTheoNgay";
26 | return axiosClient.get(url, { params });
27 | },
28 | getMovieDetails: (params) => {
29 | const url = resourceName + `LayThongTinPhim?MaPhim=${params}`;
30 | return axiosClient.get(url);
31 | },
32 | deleteMovie: (params) => {
33 | const url = resourceName + `XoaPhim?MaPhim=${params}`;
34 | return axiosClient.delete(url);
35 | },
36 | addMovie: (formData) => {
37 | const url = resourceName + "ThemPhimUploadHinh";
38 | return axiosClient.post(url, formData);
39 | },
40 | editMovie: (formData) => {
41 | const url = resourceName + "CapNhatPhimUpload";
42 | return axiosClient.post(url, formData);
43 | },
44 | };
45 |
46 | export default movieApi;
47 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/components/Navbar/style.scss:
--------------------------------------------------------------------------------
1 | #main-header {
2 | position: relative;
3 | background-color: var(--gray);
4 |
5 | .main-header__navbar-btn {
6 | color: var(--white);
7 | }
8 |
9 | .main-header__navbar-btn-wrapper + .main-header__navbar-btn-wrapper {
10 | margin-left: 6px;
11 | }
12 |
13 | .language-switcher {
14 | color: var(--white);
15 | margin-left: 10px;
16 |
17 | .MuiSelect-select {
18 | padding: 10px;
19 | }
20 |
21 | svg {
22 | display: none;
23 | }
24 |
25 | fieldset {
26 | border: none;
27 | }
28 | }
29 | }
30 |
31 | .language-item {
32 | &.Mui-selected {
33 | background-color: var(--primary) !important;
34 |
35 | &:hover {
36 | background-color: var(--primary);
37 | }
38 | }
39 | }
40 |
41 | .main-header__sidebar-icon {
42 | color: var(--primary);
43 | }
44 |
45 | .main-header__logo {
46 | width: 60px;
47 | padding: 0;
48 | }
49 |
50 | .main-header__navbar-item-name {
51 | font-family: "Open Sans", sans-serif;
52 |
53 | &:hover {
54 | background-color: var(--black);
55 | }
56 | }
57 |
58 | #menu-appbar {
59 | ul {
60 | background-color: var(--dark-gray);
61 | color: var(--white);
62 | }
63 | }
64 |
65 | @media (max-width: 1023px) {
66 | #menu-appbar {
67 | margin-top: 4px;
68 |
69 | ul {
70 | width: 150px;
71 | }
72 | }
73 | .main-header__navbar-item {
74 | padding: 16px 18px;
75 | }
76 |
77 | .main-header__navbar-item-name {
78 | background-color: transparent;
79 | color: var(--white);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/Carousel/style.scss:
--------------------------------------------------------------------------------
1 | .home__carousel {
2 | max-width: 1140px;
3 | margin: 0 auto;
4 |
5 | .home__carousel-img {
6 | height: 543px;
7 | object-fit: cover;
8 | object-position: center;
9 | }
10 | }
11 |
12 | .carousel__arrow {
13 | z-index: 2;
14 |
15 | &.carousel__arrow--next {
16 | right: 10%;
17 | }
18 |
19 | &.carousel__arrow--prev {
20 | left: 10%;
21 | }
22 | }
23 |
24 | .carousel__arrow.carousel__arrow--prev,
25 | .carousel__arrow.carousel__arrow--next {
26 | &:before {
27 | font-size: 30px;
28 | }
29 | }
30 |
31 | .home__carousel-indicators {
32 | font-size: 8px;
33 | color: var(--white);
34 | box-shadow: 0 0 0 1px transparent;
35 | }
36 |
37 | .slick-active > .home__carousel-indicator {
38 | color: transparent;
39 | box-shadow: 0 0 0 1px var(--white);
40 | border-radius: 50%;
41 | }
42 |
43 | @media (max-width: 1023px) {
44 | //mobile and tablet
45 |
46 | .home__carousel {
47 | max-width: 100%;
48 | }
49 | .home__carousel-indicators {
50 | font-size: 6px;
51 | }
52 | }
53 |
54 | @media (min-width: 740px) and (max-width: 1023px) {
55 | //tablet
56 | .home__carousel {
57 | .home__carousel-img {
58 | height: 256px;
59 | object-fit: contain;
60 | }
61 | }
62 | }
63 |
64 | @media (max-width: 739px) {
65 | //mobile
66 | .home__carousel {
67 | .home__carousel-img {
68 | height: 125px;
69 | object-fit: contain;
70 | }
71 | }
72 |
73 | .carousel__arrow.carousel__arrow--prev,
74 | .carousel__arrow.carousel__arrow--next {
75 | &:before {
76 | font-size: 20px;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/CinemaSystem/MovieSchedule/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link, NavLink } from "react-router-dom";
3 | import moment from "moment";
4 |
5 | // Material UI
6 | import { Grid, Typography } from "@mui/material";
7 |
8 | //Components
9 | import Image from "@/components/Image";
10 |
11 | import "./style.scss";
12 |
13 | function MovieSchedule({ movie, cinemaGroup }) {
14 | const renderSchedule = () => {
15 | return movie.lstLichChieuTheoPhim?.slice(0, 6).map((schedule, index) => (
16 |
17 |
21 | {moment(schedule.ngayChieuGioChieu).format("D/M/YYYY hh:mm")}
22 |
23 |
24 | ));
25 | };
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {movie.tenPhim}
36 |
37 |
38 |
39 | {cinemaGroup.diaChi}
40 |
41 |
42 | {renderSchedule()}
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | export default MovieSchedule;
50 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/Buttons/index.jsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { LoadingButton } from "@mui/lab";
3 | import { Button, IconButton } from "@mui/material";
4 | import DeleteIcon from "@mui/icons-material/Delete";
5 | import { BorderColor } from "@mui/icons-material";
6 | import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
7 |
8 | // Scss
9 | import "./style.scss";
10 |
11 | const SubmitButton = ({ children, onClick, ...others }) => (
12 |
19 | {children}
20 |
21 | );
22 |
23 | const AddItemBtn = ({ children, onClick, ...props }) => (
24 |
39 | );
40 |
41 | const EditMovieBtn = (props) => (
42 |
43 |
44 |
45 | );
46 |
47 | const DeleteMovieBtn = (props) => (
48 |
49 |
50 |
51 | );
52 |
53 | const CreateScheduleBtn = (props) => (
54 |
55 |
56 |
57 | );
58 |
59 | export { SubmitButton, EditMovieBtn, DeleteMovieBtn, AddItemBtn, CreateScheduleBtn };
60 |
--------------------------------------------------------------------------------
/src/api/userApi.js:
--------------------------------------------------------------------------------
1 | import axiosClient from "./config/axiosClient";
2 |
3 | const resourceName = "QuanLyNguoiDung/";
4 |
5 | const userApi = {
6 | login: (user) => {
7 | const url = resourceName + "DangNhap";
8 | return axiosClient.post(url, user);
9 | },
10 | register: (user) => {
11 | const url = resourceName + "DangKy";
12 | return axiosClient.post(url, user);
13 | },
14 | getUser: () => {
15 | const url = resourceName + "ThongTinTaiKhoan";
16 | return axiosClient.post(url);
17 | },
18 | updateUserProfile: (user) => {
19 | const url = resourceName + "CapNhatThongTinNguoiDung";
20 | return axiosClient.put(url, user);
21 | },
22 | getUserList: (params, keyword) => {
23 | if (keyword !== "") {
24 | const url = resourceName + `LayDanhSachNguoiDung?MaNhom=${params.maNhom}&tuKhoa=${keyword}`;
25 | return axiosClient.get(url);
26 | } else {
27 | const url = resourceName + "LayDanhSachNguoiDung";
28 | return axiosClient.get(url, { params });
29 | }
30 | },
31 | getUserDetails: (params, keyword) => {
32 | const url = resourceName + `LayDanhSachNguoiDung?MaNhom=${params.maNhom}&tuKhoa=${keyword}`;
33 | return axiosClient.get(url);
34 | },
35 | deleteUser: (userAccount) => {
36 | const url = resourceName + `XoaNguoiDung?TaiKhoan=${userAccount}`;
37 | return axiosClient.delete(url);
38 | },
39 | addUser: (formData) => {
40 | const url = resourceName + "ThemNguoiDung";
41 | return axiosClient.post(url, formData);
42 | },
43 | editUser: (userAccount) => {
44 | const url = resourceName + "CapNhatThongTinNguoiDung";
45 | return axiosClient.post(url, userAccount);
46 | },
47 | };
48 |
49 | export default userApi;
50 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Outlet } from "react-router-dom";
3 |
4 | // Material UI
5 | import { createTheme, ThemeProvider } from "@mui/material/styles";
6 | import { Stack, Box, Toolbar, Grid, Paper } from "@mui/material";
7 |
8 | // Components
9 | import AdminAppBar from "./components/AdminAppBar";
10 | import AdminDrawer from "./components/AdminDrawer";
11 | import AdminFooter from "./components/AdminFooter";
12 |
13 | const theme = createTheme();
14 |
15 | function AdminTemplate() {
16 | const [open, setOpen] = useState(true);
17 | const toggleDrawer = () => setOpen(!open);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
28 | theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900],
29 | flexGrow: 1,
30 | height: "100vh",
31 | overflow: "auto",
32 | }}
33 | >
34 |
35 |
36 |
37 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default AdminTemplate;
55 |
--------------------------------------------------------------------------------
/src/components/Modal/index.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 | import { useDispatch, useSelector } from "react-redux";
3 |
4 | // Material UI
5 | import { Stack } from "@mui/material";
6 | import {
7 | Button,
8 | Dialog,
9 | DialogActions,
10 | DialogContent,
11 | DialogContentText,
12 | DialogTitle,
13 | } from "@mui/material";
14 |
15 | // Scss
16 | import "./style.scss";
17 |
18 | const Modal = ({ actCloseModal, modalProps }) => {
19 | const { title, children, buttonContent, open, path } = modalProps;
20 | const navigate = useNavigate();
21 | const dispatch = useDispatch();
22 |
23 | const handleClose = (event, reason) => {
24 | if (reason === "backdropClick") {
25 | return;
26 | }
27 |
28 | if (path) {
29 | navigate(path);
30 | }
31 |
32 | dispatch(actCloseModal());
33 | };
34 |
35 | const renderContent = () =>
36 | children?.map((item, idx) => (
37 |
38 | {item}
39 |
40 | ));
41 |
42 | return (
43 |
60 | );
61 | };
62 |
63 | export default Modal;
64 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/UserDashBoard/component/TableCellList/index.jsx:
--------------------------------------------------------------------------------
1 | import { TableCell } from "@mui/material";
2 |
3 | // Components
4 | import { EditMovieBtn, DeleteMovieBtn } from "../../../components/Buttons";
5 |
6 | //Others
7 | import "./style.scss";
8 |
9 | const UserTableCells = (props) => {
10 | const { row, index, labelId, handleDeleteMovie, handleEditMovie } = props;
11 | return (
12 | <>
13 |
19 | {index}
20 |
21 |
25 | {row.taiKhoan}
26 |
27 |
32 | {row.matKhau}
33 |
34 |
35 | {row.email}
36 |
37 |
38 | {row.soDT}
39 |
40 |
45 | handleDeleteMovie(row.taiKhoan)} />
46 | handleEditMovie(row.taiKhoan)} />
47 |
48 | >
49 | );
50 | };
51 |
52 | export default UserTableCells;
53 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/MovieDashBoard/components/TableCellList/index.jsx:
--------------------------------------------------------------------------------
1 | //Material UI
2 | import { Button, TableCell } from "@mui/material";
3 |
4 | // Components
5 | import Image from "@/components/Image";
6 | import { EditMovieBtn, DeleteMovieBtn, CreateScheduleBtn } from "../../../components/Buttons";
7 |
8 | //Others
9 | import "./style.scss";
10 |
11 | const MovieTableCells = (props) => {
12 | const { row, labelId, handleDeleteMovie, handleEditMovie, handleSchedule } = props;
13 |
14 | return (
15 | <>
16 |
22 | {row.maPhim}
23 |
24 |
28 |
29 |
30 |
35 | {row.tenPhim}
36 |
37 | {row.moTa}
38 |
43 | handleDeleteMovie(row.maPhim)} />
44 | handleEditMovie(row.maPhim)} />
45 | handleSchedule(row.maPhim)} />
46 |
47 | >
48 | );
49 | };
50 |
51 | export default MovieTableCells;
52 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/SeatSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 |
3 | // Material UI
4 | import { Box, Typography, Grid } from "@mui/material";
5 |
6 | // Components
7 | import Loader from "@/components/Loader";
8 | import SeatGrid from "./SeatGrid";
9 | import SeatNote from "./SeatNote";
10 |
11 | // Scss
12 | import "./style.scss";
13 |
14 | const SeatSelector = () => {
15 | const { loading } = useSelector((rootReducer) => rootReducer.ticketBooking.ticketBookingDetails);
16 |
17 | return (
18 |
19 | {loading ? (
20 |
21 | ) : (
22 | <>
23 |
24 | Chọn Ghế:
25 |
26 |
30 |
35 |
36 |
37 |
38 | Màn hình
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | >
50 | )}
51 |
52 | );
53 | };
54 |
55 | export default SeatSelector;
56 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/AdminAppBar/index.jsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import { styled } from "@mui/material/styles";
3 | import { Toolbar, IconButton, Typography } from "@mui/material";
4 | import { Menu } from "@mui/icons-material";
5 |
6 | // Components
7 | import AccountAvatar from "./AccountAvatar";
8 |
9 | import { AppBar as MuiAppBar } from "@mui/material";
10 |
11 | const AppBar = styled(MuiAppBar, {
12 | shouldForwardProp: (prop) => prop !== "open",
13 | })(({ theme, open }) => ({
14 | zIndex: theme.zIndex.drawer + 1,
15 | transition: theme.transitions.create(["width", "margin"], {
16 | easing: theme.transitions.easing.sharp,
17 | duration: theme.transitions.duration.leavingScreen,
18 | }),
19 | ...(open && {
20 | marginLeft: 240,
21 | width: `calc(100% - ${240}px)`,
22 | transition: theme.transitions.create(["width", "margin"], {
23 | easing: theme.transitions.easing.sharp,
24 | duration: theme.transitions.duration.enteringScreen,
25 | }),
26 | }),
27 | }));
28 |
29 | const AdminAppBar = ({ toggleDrawer, open }) => (
30 |
35 |
40 |
51 |
52 |
53 |
54 | Finnkino Cinema
55 |
56 |
57 |
58 |
59 | );
60 |
61 | export default AdminAppBar;
62 |
--------------------------------------------------------------------------------
/src/containers/NotFoundPage/style.scss:
--------------------------------------------------------------------------------
1 | .fundo {
2 | animation: scales 3s alternate infinite;
3 | transform-origin: center;
4 | }
5 | .pao-baixo {
6 | animation: rotatepao 14s cubic-bezier(0.1, 0.49, 0.41, 0.97) infinite;
7 | transform-origin: center;
8 | }
9 | .pao-cima {
10 | animation: rotatepao 7s 1s cubic-bezier(0.1, 0.49, 0.41, 0.97) infinite;
11 | transform-origin: center;
12 | }
13 | .olhos {
14 | animation: olhos 2s alternate infinite;
15 | transform-origin: center;
16 | }
17 |
18 | .left-sparks {
19 | animation: left-sparks 4s alternate infinite;
20 | transform-origin: 150px 156px;
21 | }
22 |
23 | .right-sparks {
24 | animation: left-sparks 4s alternate infinite;
25 | transform-origin: 310px 150px;
26 | }
27 |
28 | .olhos {
29 | animation: olhos 2s alternate infinite;
30 | transform-origin: center;
31 | }
32 | @keyframes scales {
33 | from {
34 | transform: scale(0.98);
35 | }
36 | to {
37 | transform: scale(1);
38 | }
39 | }
40 |
41 | @keyframes rotatepao {
42 | 0% {
43 | transform: rotate(0deg);
44 | }
45 | 50%,
46 | 60% {
47 | transform: rotate(-20deg);
48 | }
49 | 100% {
50 | transform: rotate(0deg);
51 | }
52 | }
53 |
54 | @keyframes olhos {
55 | 0% {
56 | transform: rotateX(0deg);
57 | }
58 | 100% {
59 | transform: rotateX(30deg);
60 | }
61 | }
62 |
63 | @keyframes left-sparks {
64 | 0% {
65 | opacity: 0;
66 | }
67 | }
68 |
69 | .not-found-page {
70 | min-height: 600px;
71 | margin: 0px auto;
72 | width: auto;
73 | max-width: 460px;
74 | display: flex;
75 | align-items: center;
76 | justify-content: center;
77 | }
78 |
79 | .path {
80 | stroke-dasharray: 300;
81 | stroke-dashoffset: 300;
82 | animation: dash 4s alternate infinite;
83 | }
84 |
85 | @keyframes dash {
86 | 0%,
87 | 30% {
88 | fill: 4B4B62;
89 | stroke-dashoffset: 0;
90 | }
91 | 80%,
92 | 100% {
93 | fill: transparent;
94 | stroke-dashoffset: -200;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/store/actions/userProfile.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/userProfile";
2 | import { userApi } from "@/api";
3 |
4 | /*
5 | * Fetch user profile
6 | */
7 | const actGetUserProfile = () => {
8 | return (dispatch) => {
9 | dispatch(actUserProfileRequest());
10 |
11 | (async () => {
12 | try {
13 | const user = await userApi.getUser();
14 | dispatch(actUserProfileSuccess(user));
15 | } catch (error) {
16 | dispatch(actUserProfileFail(error));
17 | }
18 | })();
19 | };
20 | };
21 |
22 | const actUserProfileRequest = () => ({
23 | type: actType.GET_USER_PROFILE_REQUEST,
24 | });
25 |
26 | const actUserProfileFail = (error) => ({
27 | type: actType.GET_USER_PROFILE_FAIL,
28 | payload: error,
29 | });
30 |
31 | const actUserProfileSuccess = (data) => ({
32 | type: actType.GET_USER_PROFILE_SUCCESS,
33 | payload: data,
34 | });
35 |
36 | /*
37 | * Update user profile
38 | */
39 | const actUpdateUserProfile = (user, setShowModal) => {
40 | return (dispatch) => {
41 | dispatch(actUpdateUserProfileRequest());
42 |
43 | (async () => {
44 | try {
45 | user = await userApi.updateUserProfile(user);
46 | dispatch(actUpdateUserProfileSuccess(user));
47 | setShowModal(true);
48 | } catch (error) {
49 | dispatch(actUpdateUserProfileFail(error));
50 | }
51 | })();
52 | };
53 | };
54 |
55 | const actUpdateUserProfileRequest = () => ({
56 | type: actType.UPDATE_USER_PROFILE_REQUEST,
57 | });
58 |
59 | const actUpdateUserProfileFail = (error) => ({
60 | type: actType.UPDATE_USER_PROFILE_FAIL,
61 | payload: error,
62 | });
63 |
64 | const actUpdateUserProfileSuccess = (data) => ({
65 | type: actType.UPDATE_USER_PROFILE_SUCCESS,
66 | payload: data,
67 | });
68 |
69 | /*
70 | * Close modal
71 | */
72 | const actCloseModal = () => ({
73 | type: actType.CLOSE_MODAL,
74 | });
75 |
76 | export { actGetUserProfile, actUpdateUserProfile, actCloseModal };
77 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/MovieList/style.scss:
--------------------------------------------------------------------------------
1 | .home__movie-list {
2 | margin-top: 90px;
3 |
4 | .home-list__btn {
5 | font-size: 18px;
6 | font-weight: 400;
7 | padding: 12px 14px;
8 | color: var(--white);
9 | border-radius: 0;
10 |
11 | &.active {
12 | background-color: var(--dark-gray);
13 | }
14 | }
15 | }
16 |
17 | //Movie list's slider
18 | .movie-list__carousel-wrapper {
19 | position: relative;
20 | padding-bottom: 58px;
21 | background-color: var(--dark-gray);
22 |
23 | .movie-list__carousel-btn {
24 | width: 100%;
25 | font-size: 12px;
26 | }
27 | }
28 |
29 | .movie-list__carousel {
30 | .multi-carousel__img {
31 | height: 200px;
32 | width: 100%;
33 | margin: 24px 0 20px;
34 | padding: 0 10px;
35 | object-fit: contain;
36 | }
37 | }
38 | .multi-carousel__arrow {
39 | position: absolute;
40 | top: 50%;
41 | transform: translateY(-50%);
42 | font-size: 36px;
43 | color: var(--primary);
44 | background-color: transparent;
45 | border: none;
46 | outline: none;
47 | cursor: pointer;
48 |
49 | &.multi-carousel__arrow--next {
50 | right: -30px;
51 | }
52 |
53 | &.multi-carousel__arrow--prev {
54 | left: -20px;
55 | }
56 |
57 | &:hover {
58 | color: var(--hover-primary);
59 | }
60 | }
61 |
62 | .movie-list__slider {
63 | padding-bottom: 58px;
64 | background-color: var(--dark-gray);
65 | }
66 |
67 | .movie-list__slider-item-link {
68 | display: inline-block;
69 | }
70 |
71 | .movie-list__carousel-btn-icon {
72 | font-size: 8px;
73 | margin-left: 4px;
74 | }
75 |
76 | @media (max-width: 1023px) {
77 | .home__movie-list {
78 | .home-list__btn-list {
79 | text-align: left;
80 | }
81 | .home-list__btn {
82 | font-size: 12px;
83 | }
84 | }
85 | }
86 |
87 | @media (max-width: 739px) {
88 | .movie-list__carousel-wrapper {
89 | padding: 0;
90 | }
91 |
92 | .home__movie-list {
93 | margin-top: 60px;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/MuiEnhancedTable/constants.js:
--------------------------------------------------------------------------------
1 | import {
2 | alpha,
3 | Box,
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableContainer,
8 | TableHead,
9 | TablePagination,
10 | TableRow,
11 | TableSortLabel,
12 | Toolbar,
13 | Typography,
14 | Paper,
15 | IconButton,
16 | Tooltip,
17 | FormControlLabel,
18 | Switch,
19 | } from "@mui/material";
20 | import { useSelector, useDispatch } from "react-redux";
21 |
22 | // Components
23 | import Image from "@/components/Image";
24 | import Loader from "@/components/Loader";
25 | import MovieModal from "../../MovieDashBoard/components/MovieModal";
26 | import { EditMovieBtn, DeleteMovieBtn } from "../Buttons";
27 |
28 | //Others
29 | import { movieApi, userApi } from "@/api";
30 | import actGetUserDetails from "@/store/actions/userDetails";
31 |
32 | const headCells = [
33 | {
34 | id: "maPhim",
35 | numeric: true,
36 | disablePadding: true,
37 | label: "Mã phim",
38 | sortFunction: true,
39 | },
40 | {
41 | id: "hinhAnh",
42 | numeric: false,
43 | disablePadding: false,
44 | label: "Hình ảnh",
45 | sortFunction: false,
46 | },
47 | {
48 | id: "tenPhim",
49 | numeric: false,
50 | disablePadding: false,
51 | label: "Tên phim",
52 | sortFunction: true,
53 | },
54 | {
55 | id: "moTa",
56 | numeric: false,
57 | disablePadding: false,
58 | label: "Mô tả phim",
59 | sortFunction: true,
60 | },
61 | {
62 | id: "hanhDong",
63 | numeric: false,
64 | disablePadding: false,
65 | label: "Hành động",
66 | sortFunction: false,
67 | },
68 | ];
69 |
70 | const fetchUserDelete = async (userAccount) => {
71 | try {
72 | await userApi.deleteUser(userAccount);
73 | } catch (error) {
74 | alert(error);
75 | }
76 | };
77 |
78 | const fetchMovieDelete = async (movieId) => {
79 | try {
80 | await movieApi.deleteMovie(movieId);
81 | } catch (error) {
82 | alert(error);
83 | }
84 | };
85 |
86 | export { headCells, fetchUserDelete, fetchMovieDelete };
87 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/MovieDashBoard/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 |
4 | // Material UI
5 | import { Container } from "@mui/system";
6 | import { Button } from "@mui/material";
7 |
8 | //Components
9 | import SearchBar from "../components/SearchBar";
10 | import MovieModal from "./components/MovieModal";
11 | import MuiEnhancedTable from "../components/MuiEnhancedTable";
12 | import MovieTableCells from "./components/TableCellList";
13 | import { AddItemBtn } from "../components/Buttons";
14 |
15 | //Others
16 | import actGetMovieList from "@/store/actions/movieList";
17 | import { headCells } from "./constants";
18 | import "./style.scss";
19 |
20 | function MovieDashBoard() {
21 | const [openModalMovie, setOpenModalMovie] = useState(false);
22 | const dispatch = useDispatch();
23 | const movieList = useSelector((state) => state.movieList.data);
24 | const movieListLoading = useSelector((state) => state.movieList.loading);
25 |
26 | useEffect(() => {
27 | dispatch(actGetMovieList());
28 | }, []);
29 |
30 | const handleSearch = (value) => {
31 | dispatch(actGetMovieList(value));
32 | };
33 |
34 | return (
35 | <>
36 |
41 |
42 | setOpenModalMovie(true)}>Thêm phim
43 |
50 |
51 |
58 | >
59 | );
60 | }
61 |
62 | export default MovieDashBoard;
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "movielab",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.9.3",
7 | "@emotion/styled": "^11.9.3",
8 | "@fortawesome/fontawesome-svg-core": "^1.3.0",
9 | "@fortawesome/free-brands-svg-icons": "^6.0.0",
10 | "@fortawesome/free-regular-svg-icons": "^6.0.0",
11 | "@fortawesome/free-solid-svg-icons": "^6.0.0",
12 | "@fortawesome/react-fontawesome": "^0.1.17",
13 | "@hookform/resolvers": "^2.9.3",
14 | "@mui/icons-material": "^5.8.4",
15 | "@mui/lab": "^5.0.0-alpha.90",
16 | "@mui/material": "^5.8.7",
17 | "@mui/x-date-pickers": "^5.0.0-beta.3",
18 | "axios": "^0.27.2",
19 | "formik": "^2.2.9",
20 | "i": "^0.3.7",
21 | "i18next": "^21.9.0",
22 | "i18next-browser-languagedetector": "^6.1.5",
23 | "i18next-http-backend": "^1.4.1",
24 | "moment": "^2.29.4",
25 | "normalize.css": "^8.0.1",
26 | "npm": "^8.14.0",
27 | "react": "^18.2.0",
28 | "react-dom": "^18.2.0",
29 | "react-hook-form": "^7.33.1",
30 | "react-i18next": "^11.18.3",
31 | "react-redux": "^8.0.2",
32 | "react-router-dom": "^6.3.0",
33 | "react-scripts": "5.0.1",
34 | "react-slick": "^0.29.0",
35 | "redux": "^4.2.0",
36 | "redux-thunk": "^2.4.1",
37 | "slick-carousel": "^1.8.1",
38 | "yup": "^0.32.11"
39 | },
40 | "devDependencies": {
41 | "babel-plugin-module-resolver": "^4.1.0",
42 | "customize-cra": "^1.0.0",
43 | "react-app-rewired": "^2.2.1"
44 | },
45 | "scripts": {
46 | "start": "react-app-rewired start",
47 | "build": "react-app-rewired build",
48 | "test": "react-app-rewired test",
49 | "eject": "react-scripts eject"
50 | },
51 | "eslintConfig": {
52 | "extends": [
53 | "react-app",
54 | "react-app/jest"
55 | ]
56 | },
57 | "browserslist": {
58 | "production": [
59 | ">0.2%",
60 | "not dead",
61 | "not op_mini all"
62 | ],
63 | "development": [
64 | "last 1 chrome version",
65 | "last 1 firefox version",
66 | "last 1 safari version"
67 | ]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/TicketBookingCard/style.scss:
--------------------------------------------------------------------------------
1 | .ticket-booking-card {
2 | padding: 16px;
3 | color: var(--white);
4 | background-color: var(--gray);
5 |
6 | // Mobile
7 | @media (max-width: 739px) {
8 | padding: 12px;
9 | }
10 |
11 | &__media {
12 | object-fit: contain;
13 | height: 260px;
14 | margin: 0 auto;
15 | margin-bottom: 16px;
16 |
17 | @media (max-width: 1023px) {
18 | object-fit: cover;
19 | height: auto;
20 | width: 75%;
21 | }
22 |
23 | // Mobile
24 | @media (max-width: 739px) {
25 | width: 80%;
26 | }
27 | }
28 |
29 | &__content {
30 | font-size: 13px;
31 | padding: 0;
32 | }
33 |
34 | &__movie-name {
35 | margin-bottom: 10px;
36 | font: {
37 | weight: 700;
38 | size: 22px;
39 | }
40 | text-align: center;
41 |
42 | // Mobile
43 | @media (max-width: 739px) {
44 | font-size: 20px;
45 | }
46 | }
47 |
48 | &__movie-age-limit-label {
49 | font: {
50 | size: 11px;
51 | weight: 700;
52 | }
53 | padding: 4px 6px;
54 | background-color: var(--primary);
55 | color: var(--black);
56 | }
57 |
58 | &__movie-age-limit-content {
59 | font-size: 11px;
60 | color: var(--primary);
61 | }
62 |
63 | &__booking-details {
64 | padding: 4px 0;
65 | }
66 |
67 | &__divider {
68 | background-color: var(--light-gray);
69 | }
70 |
71 | &__total {
72 | font-size: 17px;
73 |
74 | // Mobile
75 | @media (max-width: 739px) {
76 | font-size: 15px;
77 | }
78 | }
79 |
80 | &__btn-booking {
81 | margin-top: 20px;
82 | padding: 8px 32px;
83 | font: {
84 | size: 15px;
85 | weight: 700;
86 | }
87 | color: var(--black);
88 | background-color: var(--primary);
89 | border-radius: 0;
90 |
91 | // Mobile
92 | @media (max-width: 739px) {
93 | font-size: 13px;
94 | }
95 |
96 | &:hover {
97 | background-color: var(--hover-primary);
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/store/actions/movieManagement.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_MOVIE_MANAGEMENT_REQUEST,
3 | GET_MOVIE_MANAGEMENT_SUCCESS,
4 | GET_MOVIE_MANAGEMENT_FAIL,
5 | } from "../constants/movieManagement";
6 | import { movieApi } from "@/api";
7 |
8 | const actMovieManagementRequest = () => {
9 | return {
10 | type: GET_MOVIE_MANAGEMENT_REQUEST,
11 | };
12 | };
13 |
14 | const actMovieManagementSuccess = (data) => {
15 | return {
16 | type: GET_MOVIE_MANAGEMENT_SUCCESS,
17 | payload: data,
18 | };
19 | };
20 |
21 | const actMovieManagementFail = (error) => {
22 | return {
23 | type: GET_MOVIE_MANAGEMENT_FAIL,
24 | payload: error,
25 | };
26 | };
27 |
28 | const actFetchMovieEdit = (movie) => {
29 | return (dispatch) => {
30 | dispatch(actMovieManagementRequest());
31 |
32 | const fetchMovieEdit = async () => {
33 | try {
34 | const movieEdit = await movieApi.editMovie(movie);
35 | dispatch(actMovieManagementSuccess(movieEdit));
36 | } catch (error) {
37 | dispatch(actMovieManagementFail(error));
38 | }
39 | };
40 |
41 | fetchMovieEdit();
42 | };
43 | };
44 |
45 | const actFetchMovieAdd = (movie) => {
46 | return (dispatch) => {
47 | dispatch(actMovieManagementRequest());
48 |
49 | const fetchMovieAdd = async () => {
50 | try {
51 | const movieAdd = await movieApi.addMovie(movie);
52 | dispatch(actMovieManagementSuccess(movieAdd));
53 | } catch (error) {
54 | dispatch(actMovieManagementFail(error));
55 | }
56 | };
57 |
58 | fetchMovieAdd();
59 | };
60 | };
61 |
62 | const actFetchMovieDelete = (movieId) => {
63 | return (dispatch) => {
64 | dispatch(actMovieManagementRequest());
65 |
66 | const fetchMovieDelete = async () => {
67 | try {
68 | const movieAdd = await movieApi.deleteMovie(movieId);
69 | dispatch(actMovieManagementSuccess(movieAdd));
70 | } catch (error) {
71 | dispatch(actMovieManagementFail(error));
72 | }
73 | };
74 |
75 | fetchMovieDelete();
76 | };
77 | };
78 |
79 | export { actFetchMovieEdit, actFetchMovieAdd, actFetchMovieDelete };
80 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/UserDashBoard/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 |
4 | // Material UI
5 | import { Container } from "@mui/system";
6 |
7 | //Components
8 | import SearchBar from "../components/SearchBar";
9 | import { AddItemBtn } from "../components/Buttons";
10 | import MuiEnhancedTable from "../components/MuiEnhancedTable";
11 | import UserTableCells from "./component/TableCellList";
12 | import headCells from "./constants";
13 | import UserModal from "./component/UserModal";
14 |
15 | //Others
16 | import actGetUserList from "@/store/actions/userList";
17 | import { actUserDetailsSuccess } from "@/store/actions/userDetails";
18 |
19 | function UserDashBoard() {
20 | const [openModalUser, setOpenModalUser] = useState(false);
21 |
22 | const dispatch = useDispatch();
23 | const userList = useSelector((state) => state.userList.data);
24 | const updateUser = useSelector((state) => state.userDetails.data);
25 | const userListLoading = useSelector((state) => state.userList.loading);
26 |
27 | useEffect(() => {
28 | dispatch(actGetUserList());
29 | }, [updateUser]);
30 |
31 | const handleSearch = (value) => {
32 | dispatch(actGetUserList(value));
33 | dispatch(actUserDetailsSuccess(userList));
34 | };
35 |
36 | return (
37 | <>
38 |
43 |
44 | setOpenModalUser(true)}>Thêm người dùng
45 |
52 |
53 |
60 | >
61 | );
62 | }
63 |
64 | export default UserDashBoard;
65 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/ProfilePage/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useDispatch } from "react-redux";
3 |
4 | // Material UI
5 | import { Box, Container, Tab } from "@mui/material";
6 | import { TabContext, TabList, TabPanel } from "@mui/lab";
7 |
8 | // Components
9 | import AccountInfo from "./AccountInfo";
10 | import TransactionHistory from "./TransactionHistory";
11 |
12 | // Redux actions
13 | import { actGetUserProfile } from "@/store/actions/userProfile";
14 |
15 | // Scss
16 | import "./style.scss";
17 |
18 | const ProfilePage = () => {
19 | const [value, setValue] = useState("1");
20 | const dispatch = useDispatch();
21 |
22 | useEffect(() => {
23 | dispatch(actGetUserProfile());
24 | }, []);
25 |
26 | const handleChangeTab = (event, newValue) => setValue(newValue);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | {/* Tab titles */}
34 |
39 |
45 |
51 |
52 |
53 | {/* Tabs content */}
54 | {/* Account info */}
55 |
56 |
57 |
58 | {/* Transaction history */}
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default ProfilePage;
69 |
--------------------------------------------------------------------------------
/src/store/actions/ticketBooking.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/ticketBooking";
2 | import { ticketBookingApi } from "@/api";
3 |
4 | /*
5 | * Fetch ticket booking details
6 | */
7 | const actGetTicketBookingDetails = (showtimeCode) => {
8 | return (dispatch) => {
9 | dispatch(actTicketBookingDetailsRequest());
10 |
11 | (async () => {
12 | try {
13 | const params = { maLichChieu: showtimeCode };
14 | const ticketBookingDetails = await ticketBookingApi.getTicketOfficeList(params);
15 | dispatch(actTicketBookingDetailsSuccess(ticketBookingDetails));
16 | } catch (error) {
17 | dispatch(actTicketBookingDetailsFail(error));
18 | }
19 | })();
20 | };
21 | };
22 |
23 | const actTicketBookingDetailsRequest = () => ({
24 | type: actType.GET_TICKET_BOOKING_DETAILS_REQUEST,
25 | });
26 |
27 | const actTicketBookingDetailsFail = (error) => ({
28 | type: actType.GET_TICKET_BOOKING_DETAILS_FAIL,
29 | payload: error,
30 | });
31 |
32 | const actTicketBookingDetailsSuccess = (data) => ({
33 | type: actType.GET_TICKET_BOOKING_DETAILS_SUCCESS,
34 | payload: data,
35 | });
36 |
37 | /*
38 | * Book ticket
39 | */
40 | const actBookTicket = (ticket) => {
41 | return (dispatch) => {
42 | dispatch(actBookTicketRequest());
43 |
44 | (async () => {
45 | try {
46 | await ticketBookingApi.bookTicket(ticket);
47 | dispatch(actBookTicketSuccess());
48 | } catch (error) {
49 | dispatch(actBookTicketFail(error));
50 | }
51 | })();
52 | };
53 | };
54 |
55 | const actBookTicketRequest = () => ({
56 | type: actType.BOOK_TICKET_REQUEST,
57 | });
58 |
59 | const actBookTicketFail = (error) => ({
60 | type: actType.BOOK_TICKET_FAIL,
61 | payload: error,
62 | });
63 |
64 | const actBookTicketSuccess = () => ({
65 | type: actType.BOOK_TICKET_SUCCESS,
66 | });
67 |
68 | /*
69 | * Choose seats
70 | */
71 | const actChooseSeat = (seat) => ({
72 | type: actType.CHOOSE_SEAT,
73 | payload: seat,
74 | });
75 |
76 | /*
77 | * Close modal
78 | */
79 | const actCloseModal = () => ({
80 | type: actType.CLOSE_MODAL,
81 | });
82 |
83 | export { actGetTicketBookingDetails, actBookTicket, actChooseSeat, actCloseModal };
84 |
--------------------------------------------------------------------------------
/src/store/reducers/userProfile.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/userProfile";
2 |
3 | const initialState = {
4 | data: { content: null, loading: false, error: "" },
5 | update: { loading: false, error: "" },
6 | modal: {
7 | open: false,
8 | title: "Thông báo",
9 | children: [
10 | "Cập nhật thông tin tài khoản thành công!",
11 | "Vui lòng liên hệ supports@finnkino.com để được hỗ trợ tốt hơn.",
12 | ],
13 | buttonContent: "Chấp nhận",
14 | path: "",
15 | },
16 | };
17 |
18 | const userProfileReducer = (state = initialState, { type, payload }) => {
19 | switch (type) {
20 | // Fetch user profile
21 | case actType.GET_USER_PROFILE_REQUEST:
22 | return {
23 | ...state,
24 | data: { content: null, loading: true, error: "" },
25 | };
26 |
27 | case actType.GET_USER_PROFILE_FAIL:
28 | return {
29 | ...state,
30 | data: { content: null, loading: false, error: payload },
31 | };
32 |
33 | case actType.GET_USER_PROFILE_SUCCESS:
34 | return {
35 | ...state,
36 | data: { content: payload, loading: false, error: "" },
37 | };
38 |
39 | // Update user profile
40 | case actType.UPDATE_USER_PROFILE_REQUEST:
41 | return {
42 | ...state,
43 | update: { loading: true, error: "" },
44 | };
45 |
46 | case actType.UPDATE_USER_PROFILE_FAIL:
47 | return {
48 | ...state,
49 | update: { loading: false, error: payload },
50 | };
51 |
52 | case actType.UPDATE_USER_PROFILE_SUCCESS:
53 | return {
54 | ...state,
55 | data: {
56 | content: {
57 | ...state.data.content,
58 | matKhau: payload.matKhau,
59 | email: payload.email,
60 | soDT: payload.soDT,
61 | },
62 | loading: false,
63 | error: "",
64 | },
65 | update: { loading: false, error: "" },
66 | modal: { ...state.modal, open: true },
67 | };
68 |
69 | // Close modal
70 | case actType.CLOSE_MODAL:
71 | return { ...state, modal: { ...state.modal, open: false } };
72 |
73 | default:
74 | return state;
75 | }
76 | };
77 |
78 | export default userProfileReducer;
79 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/SearchBar/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Paper from "@mui/material/Paper";
3 | import InputBase from "@mui/material/InputBase";
4 | import Divider from "@mui/material/Divider";
5 | import IconButton from "@mui/material/IconButton";
6 | import SearchIcon from "@mui/icons-material/Search";
7 | import PropTypes from "prop-types";
8 | import { useRef } from "react";
9 |
10 | SearchBar.propTypes = {
11 | onSubmit: PropTypes.func,
12 | };
13 |
14 | SearchBar.defaultProps = {
15 | onSubmit: null,
16 | };
17 |
18 | function SearchBar(props) {
19 | const { onSubmit, className } = props;
20 | const [searchTerms, setSearchTerms] = useState("");
21 | const typingTimeoutRef = useRef(null);
22 |
23 | const handleSearchTermChange = (e) => {
24 | const value = e.target.value;
25 | setSearchTerms(value);
26 |
27 | if (!onSubmit) return;
28 |
29 | if (typingTimeoutRef.current) {
30 | clearTimeout(typingTimeoutRef.current);
31 | }
32 |
33 | typingTimeoutRef.current = setTimeout(() => {
34 | const searchValues = {
35 | searchTerms: value,
36 | };
37 |
38 | onSubmit(searchValues.searchTerms);
39 | }, 300);
40 | };
41 | return (
42 |
53 |
60 |
61 |
73 |
74 |
75 |
76 | );
77 | }
78 |
79 | export default SearchBar;
80 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/AdminDrawer/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | // Material UI
4 | import { styled } from "@mui/material/styles";
5 | import { Stack, IconButton, List, Divider, Avatar, Drawer as MuiDrawer } from "@mui/material";
6 | import { ChevronLeft } from "@mui/icons-material";
7 | import {} from "@mui/material";
8 |
9 | import DrawerItems from "./DrawerItems";
10 |
11 | import FinnkinoLogo from "@/assets/images/header-logo.png";
12 |
13 | // Scss
14 | import "./style.scss";
15 |
16 | const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open" })(
17 | ({ theme, open }) => ({
18 | "& .MuiDrawer-paper": {
19 | position: "relative",
20 | whiteSpace: "nowrap",
21 | width: 240,
22 | transition: theme.transitions.create("width", {
23 | easing: theme.transitions.easing.sharp,
24 | duration: theme.transitions.duration.enteringScreen,
25 | }),
26 | boxSizing: "border-box",
27 | ...(!open && {
28 | overflowX: "hidden",
29 | transition: theme.transitions.create("width", {
30 | easing: theme.transitions.easing.sharp,
31 | duration: theme.transitions.duration.leavingScreen,
32 | }),
33 | width: theme.spacing(7),
34 | [theme.breakpoints.up("xl")]: {
35 | width: theme.spacing(9),
36 | },
37 | }),
38 | },
39 | }),
40 | );
41 |
42 | const AdminDrawer = ({ toggleDrawer, open }) => (
43 |
49 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 |
69 | export default AdminDrawer;
70 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/Carousel/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import Slider from "react-slick";
4 |
5 | //FontAwesome
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
7 | import { faCircle } from "@fortawesome/free-solid-svg-icons";
8 |
9 | // React Slick config
10 | import "slick-carousel/slick/slick.css";
11 | import "slick-carousel/slick/slick-theme.css";
12 |
13 | import actFetchBanners from "@/store/actions/movieBanner";
14 |
15 | // Components
16 | import Image from "@/components/Image";
17 | import Loader from "@/components/Loader";
18 |
19 | import "./style.scss";
20 |
21 | function SampleNextArrow(props) {
22 | const { className, style, onClick } = props;
23 | return ;
24 | }
25 |
26 | function SamplePrevArrow(props) {
27 | const { className, style, onClick } = props;
28 | return ;
29 | }
30 |
31 | function Carousel() {
32 | const dispatch = useDispatch();
33 | const data = useSelector((state) => state.movieBanner.data);
34 | const loading = useSelector((state) => state.movieBanner.loading);
35 |
36 | useEffect(() => {
37 | dispatch(actFetchBanners());
38 | }, []);
39 |
40 | const settings = {
41 | dots: true,
42 | infinite: true,
43 | autoplay: true,
44 | speed: 1000,
45 | autoplaySpeed: 3000,
46 | cssEase: "linear",
47 | slidesToShow: 1,
48 | slidesToScroll: 1,
49 | nextArrow: ,
50 | prevArrow: ,
51 | customPaging: function () {
52 | return ;
53 | },
54 | dotsClass: "slick-dots slick-thumb home__carousel-indicators",
55 | };
56 |
57 | const renderCarouselItem = () => {
58 | if (loading) return ;
59 |
60 | const carouselItem = data?.map((item) => (
61 |
62 | ));
63 |
64 | return {carouselItem};
65 | };
66 |
67 | return {renderCarouselItem()}
;
68 | }
69 |
70 | export default Carousel;
71 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/MovieDashBoard/components/TableCellList/style.scss:
--------------------------------------------------------------------------------
1 | .management-table__table-cell {
2 | --width-s: 100px;
3 | --width-m: 150px;
4 | --width-l: 200px;
5 |
6 | max-height: var(--width-s);
7 | text-align: center;
8 |
9 | &.table-cell__movie-id {
10 | width: var(--width-s);
11 | padding: 0;
12 | text-align: right;
13 | }
14 |
15 | &.table-cell__movie-img {
16 | img {
17 | width: var(--width-m);
18 | height: var(--width-m);
19 | object-fit: cover;
20 | object-position: center;
21 | }
22 | }
23 |
24 | &.table-cell__movie-name {
25 | width: var(--width-l);
26 | overflow: hidden;
27 | }
28 |
29 | &.table-cell__movie-desc {
30 | overflow: hidden;
31 | text-align: justify;
32 | }
33 |
34 | &.table-cell__management-actions {
35 | width: var(--width-m);
36 | }
37 | }
38 |
39 | @media (min-width: 740px) and (max-width: 1023px) {
40 | .movie-table__table-cell {
41 | height: calc(var(--width-s) / 4 * 3);
42 |
43 | &.table-cell__movie-id {
44 | width: calc(var(--width-s) / 2);
45 | padding: 0;
46 | text-align: right;
47 | }
48 |
49 | &.table-cell__movie-img {
50 | img {
51 | width: calc(var(--width-m) / 2);
52 | height: calc(var(--width-m) / 2);
53 | object-fit: cover;
54 | object-position: center;
55 | }
56 | }
57 |
58 | &.table-cell__movie-name {
59 | width: calc(var(--width-l) / 2);
60 | overflow: hidden;
61 | }
62 |
63 | &.table-cell__movie-desc {
64 | overflow: hidden;
65 | text-align: justify;
66 | }
67 |
68 | &.table-cell__management-actions {
69 | width: calc(var(--width-s) / 2);
70 | }
71 | }
72 | }
73 |
74 | @media (max-width: 739px) {
75 | .movie-table__table-cell {
76 | max-height: var(--width-m);
77 | font-size: 12px;
78 |
79 | &.table-cell__movie-id {
80 | width: var(--width-s) / 2;
81 | padding: 0;
82 | text-align: right;
83 | }
84 |
85 | &.table-cell__movie-img {
86 | img {
87 | width: calc(var(--width-m) / 5 * 4);
88 | height: calc(var(--width-m) / 5 * 4);
89 | object-fit: cover;
90 | object-position: center;
91 | }
92 | }
93 |
94 | &.table-cell__movie-name {
95 | width: var(--width-s);
96 | overflow: hidden;
97 | }
98 |
99 | &.table-cell__movie-desc {
100 | overflow: hidden;
101 | text-align: justify;
102 | }
103 |
104 | &.table-cell__management-actions {
105 | width: var(--width-s);
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/GlobalStyles/GlobalStyles.scss:
--------------------------------------------------------------------------------
1 | @import "normalize.css";
2 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap");
3 |
4 | :root {
5 | --primary: #fdca00;
6 | --hover-primary: #b18d00;
7 | --gray: #222;
8 | --dark-gray: #141414;
9 | --light-gray: #606060;
10 | --black: #070707;
11 | --white: #fff;
12 | --border-color: #dddddd26;
13 |
14 | --logo-size: 68px;
15 | --padding-tablet: 15px;
16 | --padding-mobile: 7.5px;
17 | }
18 |
19 | * {
20 | margin: 0;
21 | padding: 0;
22 | box-sizing: border-box;
23 | }
24 |
25 | body {
26 | font-family: "Open Sans", sans-serif;
27 | line-height: 1.5;
28 | text-rendering: optimizeSpeed;
29 |
30 | .btn-wrapper {
31 | padding: 10px 16px;
32 | border-radius: 0 !important;
33 | border: 1px solid transparent;
34 | cursor: pointer;
35 | }
36 |
37 | // Custom scrollbar
38 | &::-webkit-scrollbar {
39 | width: 16px;
40 |
41 | &-thumb {
42 | background-color: var(--light-gray);
43 | }
44 | }
45 | }
46 |
47 | .btn-wrapper {
48 | &.btn-filled {
49 | color: var(--dark-gray);
50 | background-color: var(--primary);
51 |
52 | > a {
53 | color: var(--dark-gray);
54 | }
55 |
56 | &:hover {
57 | background-color: var(--hover-primary);
58 | }
59 | }
60 |
61 | &.btn-outline {
62 | color: var(--white);
63 | background-color: transparent;
64 | border-color: var(--white);
65 |
66 | &:hover {
67 | background-color: rgba(0, 0, 0, 0.3);
68 | border-color: #e0e0e0;
69 | }
70 | }
71 | }
72 |
73 | a[href] {
74 | text-decoration: none;
75 | }
76 |
77 | .btn__left-icon {
78 | padding-right: 6px;
79 | }
80 |
81 | .btn__right-icon {
82 | padding-left: 6px;
83 | }
84 |
85 | .form__input-wrapper {
86 | margin: 10px 0 !important;
87 | }
88 |
89 | @media (min-width: 1024px) {
90 | .hide-on-pc {
91 | display: none !important;
92 | }
93 | }
94 |
95 | @media (max-width: 1023px) {
96 | .hide-on-mobile-tablet {
97 | display: none !important;
98 | }
99 | }
100 |
101 | @media (min-width: 740px) and (max-width: 1023px) {
102 | .hide-on-tablet {
103 | display: none !important;
104 | }
105 |
106 | .container {
107 | padding-left: var(--padding-tablet) !important;
108 | padding-right: var(--padding-tablet) !important;
109 | }
110 | }
111 |
112 | @media (max-width: 739px) {
113 | .hide-on-mobile {
114 | display: none !important;
115 | }
116 |
117 | .container {
118 | padding-left: var(--padding-mobile) !important;
119 | padding-right: var(--padding-mobile) !important;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/MuiEnhancedTable/style.scss:
--------------------------------------------------------------------------------
1 | .movie-table__head-item {
2 | color: var(--dark-gray);
3 | font-weight: 600;
4 | font-size: 16px;
5 |
6 | &.Mui-active {
7 | color: var(--primary);
8 | }
9 | }
10 |
11 | .movie-table__table-cell {
12 | --width-s: 100px;
13 | --width-m: 150px;
14 | --width-l: 200px;
15 |
16 | max-height: var(--width-s);
17 | text-align: center;
18 |
19 | &.table-cell__movie-id {
20 | width: var(--width-s);
21 | padding: 0;
22 | text-align: right;
23 | }
24 |
25 | &.table-cell__movie-img {
26 | img {
27 | width: var(--width-m);
28 | height: var(--width-m);
29 | object-fit: cover;
30 | object-position: center;
31 | }
32 | }
33 |
34 | &.table-cell__movie-name {
35 | width: var(--width-l);
36 | overflow: hidden;
37 | }
38 |
39 | &.table-cell__movie-desc {
40 | overflow: hidden;
41 | text-align: justify;
42 | }
43 |
44 | &.table-cell__movie-actions {
45 | width: var(--width-l);
46 | }
47 | }
48 |
49 | @media (min-width: 740px) and (max-width: 1023px) {
50 | .movie-table__table-cell {
51 | height: calc(var(--width-s) / 4 * 3);
52 |
53 | &.table-cell__movie-id {
54 | width: calc(var(--width-s) / 2);
55 | padding: 0;
56 | text-align: right;
57 | }
58 |
59 | &.table-cell__movie-img {
60 | img {
61 | width: calc(var(--width-m) / 2);
62 | height: calc(var(--width-m) / 2);
63 | object-fit: cover;
64 | object-position: center;
65 | }
66 | }
67 |
68 | &.table-cell__movie-name {
69 | width: calc(var(--width-l) / 2);
70 | overflow: hidden;
71 | }
72 |
73 | &.table-cell__movie-desc {
74 | overflow: hidden;
75 | text-align: justify;
76 | }
77 |
78 | &.table-cell__movie-actions {
79 | width: calc(var(--width-s) / 2);
80 | }
81 | }
82 | }
83 |
84 | @media (max-width: 739px) {
85 | .movie-table__table-cell {
86 | max-height: var(--width-m);
87 | font-size: 12px;
88 |
89 | &.table-cell__movie-id {
90 | width: var(--width-s) / 2;
91 | padding: 0;
92 | text-align: right;
93 | }
94 |
95 | &.table-cell__movie-img {
96 | img {
97 | width: calc(var(--width-m) / 5 * 4);
98 | height: calc(var(--width-m) / 5 * 4);
99 | object-fit: cover;
100 | object-position: center;
101 | }
102 | }
103 |
104 | &.table-cell__movie-name {
105 | width: var(--width-s);
106 | overflow: hidden;
107 | }
108 |
109 | &.table-cell__movie-desc {
110 | overflow: hidden;
111 | text-align: justify;
112 | }
113 |
114 | &.table-cell__movie-actions {
115 | width: var(--width-s);
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/SeatSelector/SeatGrid/index.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 |
3 | // Material UI
4 | import { Box, Grid } from "@mui/material";
5 |
6 | // Constants
7 | import { ALPHABET } from "@/constants";
8 |
9 | import { actChooseSeat } from "@/store/actions/ticketBooking";
10 |
11 | // Scss
12 | import "./style.scss";
13 |
14 | const SeatGrid = () => {
15 | const { ticketBookingDetails, selectedSeats } = useSelector(
16 | (rootReducer) => rootReducer.ticketBooking,
17 | );
18 |
19 | const dispatch = useDispatch();
20 |
21 | const seats = ticketBookingDetails.data?.danhSachGhe;
22 |
23 | if (!seats || seats.length < 1) return;
24 |
25 | const NO_SEATS_ON_ROWS = 16;
26 | const NO_SEAT_INDICATORS_ON_ROWS = 2;
27 | const GRID_COLS = NO_SEATS_ON_ROWS + NO_SEAT_INDICATORS_ON_ROWS;
28 | const GRID_ROWS = seats.length / NO_SEATS_ON_ROWS;
29 | const NO_SEAT_INDICATORS = NO_SEAT_INDICATORS_ON_ROWS * GRID_ROWS;
30 |
31 | // Handle choose seat
32 | const handleChooseSeat = (id, price, code) => {
33 | const seat = { id, price, code };
34 | dispatch(actChooseSeat(seat));
35 | };
36 |
37 | const gridItems = [];
38 | for (let gridCell = 0, row = 0; gridCell < seats.length + NO_SEAT_INDICATORS; ++gridCell) {
39 | const rowHead = gridCell % GRID_COLS === 0;
40 | const rowTail = gridCell % GRID_COLS === GRID_COLS - 1;
41 |
42 | const isSeat = !rowHead && !rowTail;
43 | if (isSeat) {
44 | const seatNum = gridCell % GRID_COLS;
45 | const seatIdx = gridCell - (row * 2 + 1);
46 | const seatCode = ALPHABET[row] + seatNum;
47 |
48 | // Get seat details
49 | const {
50 | maGhe: seatId,
51 | maRap: cinemaId,
52 | loaiGhe: seatType,
53 | giaVe: seatPrice,
54 | daDat: sold,
55 | taiKhoanNguoiDat: username,
56 | } = seats[seatIdx];
57 |
58 | // Select type of seat
59 | let seatTypeClass = "selectable";
60 |
61 | if (seatType === "Vip") {
62 | seatTypeClass = "vip";
63 | }
64 |
65 | const idx = selectedSeats.findIndex((selectedSeat) => selectedSeat.id === seatId);
66 | if (idx !== -1) {
67 | seatTypeClass = "selected";
68 | }
69 |
70 | if (sold) {
71 | seatTypeClass = "sold";
72 | }
73 |
74 | // Render seat
75 | gridItems.push(
76 |
77 |
78 | handleChooseSeat(seatId, seatPrice, seatCode)}
81 | >
82 | {sold ? "X" : seatNum}
83 |
84 |
85 | ,
86 | );
87 |
88 | continue;
89 | }
90 |
91 | const indicatorPosition = rowHead
92 | ? "seat-selector__indicator-wrapper--left"
93 | : "seat-selector__indicator-wrapper--right";
94 |
95 | // Render seat indicator
96 | gridItems.push(
97 |
98 |
99 | {ALPHABET[rowTail ? row++ : row]}
100 |
101 | ,
102 | );
103 | }
104 |
105 | return gridItems;
106 | };
107 |
108 | export default SeatGrid;
109 |
--------------------------------------------------------------------------------
/src/components/ReactSlick/MultipleItems.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import Slider from "react-slick";
4 |
5 | //Components
6 | import Image from "@/components/Image";
7 |
8 | import "./MultipleItems.scss";
9 |
10 | function SampleNextArrow(props) {
11 | const { className, style, onClick } = props;
12 | if (props.icon) {
13 | return (
14 |
20 | );
21 | } else {
22 | return (
23 |
28 | );
29 | }
30 | }
31 |
32 | function SamplePrevArrow(props) {
33 | const { className, style, onClick } = props;
34 | if (props.icon) {
35 | return (
36 |
42 | );
43 | } else {
44 | return (
45 |
50 | );
51 | }
52 | }
53 |
54 | function MultipleItems({
55 | className,
56 | data,
57 | Component,
58 | dots = false,
59 | speed = 2000,
60 | autoplaySpeed = 3000,
61 | slidesToShow = 3.2,
62 | slidesToScroll = 3.2,
63 | autoplay = true,
64 | nextArrow = "",
65 | prevArrow = "",
66 | ...props
67 | }) {
68 | const settings = {
69 | dots: dots,
70 | infinite: true,
71 | autoplay: autoplay,
72 | speed: speed,
73 | autoplaySpeed: autoplaySpeed,
74 | slidesToShow: slidesToShow,
75 | slidesToScroll: slidesToShow,
76 | nextArrow: ,
77 | prevArrow: ,
78 | responsive: [
79 | {
80 | breakpoint: 1023,
81 | settings: {
82 | slidesToShow: 5.2,
83 | slidesToScroll: 5,
84 | infinite: true,
85 | dots: true,
86 | },
87 | },
88 | {
89 | breakpoint: 739,
90 | settings: {
91 | slidesToShow: 2.2,
92 | slidesToScroll: 2,
93 | initialSlide: 2,
94 | },
95 | },
96 | ],
97 | };
98 |
99 | const renderCarouselContent = () => {
100 | return data?.map((item) => {
101 | switch (Component) {
102 | case "img":
103 | case "Image":
104 | return (
105 |
106 |
107 |
108 | );
109 | default:
110 | return (
111 |
112 | {item.tenPhim}
113 |
114 | );
115 | }
116 | });
117 | };
118 | return (
119 |
120 | {renderCarouselContent()}
121 |
122 | );
123 | }
124 |
125 | export default MultipleItems;
126 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/components/AdminAppBar/AccountAvatar/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { useAuth } from "@/hooks";
4 |
5 | // Material UI
6 | import {
7 | Avatar,
8 | Menu,
9 | MenuItem,
10 | ListItemIcon,
11 | Divider,
12 | IconButton,
13 | Tooltip,
14 | Typography,
15 | Stack,
16 | } from "@mui/material";
17 | import { Logout, AccountCircle } from "@mui/icons-material";
18 |
19 | const AccountAvatar = () => {
20 | const auth = useAuth();
21 | const navigate = useNavigate();
22 | const [anchorEl, setAnchorEl] = useState(null);
23 |
24 | const open = !!anchorEl;
25 |
26 | const handleOpenMenu = (event) => {
27 | setAnchorEl(event.currentTarget);
28 | };
29 |
30 | const handleCloseMenu = () => setAnchorEl(null);
31 |
32 | const handleLogout = () => {
33 | auth.logout();
34 | navigate("/auth/login");
35 | };
36 |
37 | return (
38 | <>
39 |
40 |
41 |
49 |
54 |
55 |
56 |
57 | {auth.user.taiKhoan}
58 |
59 |
60 |
109 | >
110 | );
111 | };
112 |
113 | export default AccountAvatar;
114 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/MovieList/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Link } from "react-router-dom";
4 |
5 | //FontAwesome
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
7 | import { faAngleDoubleRight, faAngleLeft, faAngleRight } from "@fortawesome/free-solid-svg-icons";
8 |
9 | //Material UI
10 | import { Box, Button, Container, Typography } from "@mui/material";
11 |
12 | //Components
13 | import MultipleItems from "@/components/ReactSlick/MultipleItems";
14 | import Loader from "@/components/Loader";
15 |
16 | //Others
17 | import "./style.scss";
18 | import { SET_MOVIE_TYPE_NOW, SET_MOVIE_TYPE_SOON } from "@/store/constants/movieList";
19 | import actGetMovieList from "@/store/actions/movieList";
20 |
21 | function MovieList() {
22 | useEffect(() => {
23 | dispatch(actGetMovieList());
24 | }, []);
25 |
26 | const dispatch = useDispatch();
27 | const movieList = useSelector((state) => state.movieList.data);
28 | const loading = useSelector((state) => state.movieList.loading);
29 | const movieType = useSelector((state) => state.movieList.movieType);
30 |
31 | let movieTypeList;
32 |
33 | const handleMovieType = () => {
34 | if (movieType === "now") {
35 | return (movieTypeList = movieList?.filter((movie) => movie.dangChieu));
36 | } else if (movieType === "soon") {
37 | return (movieTypeList = movieList?.filter((movie) => movie.sapChieu));
38 | }
39 | };
40 |
41 | return (
42 |
43 |
44 |
51 |
58 |
59 |
60 |
61 |
62 | {loading ? (
63 |
64 | ) : (
65 | }
74 | prevArrow={}
75 | />
76 | )}
77 |
78 |
79 |
80 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
99 | export default MovieList;
100 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/LoginPage/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useNavigate, Link as RouterLink } from "react-router-dom";
3 | import { useAuth } from "@/hooks";
4 | import { useForm } from "react-hook-form";
5 |
6 | // Material UI
7 | import {
8 | FormControlLabel,
9 | Checkbox,
10 | Link,
11 | Box,
12 | InputAdornment,
13 | Stack,
14 | Alert,
15 | IconButton,
16 | Typography,
17 | } from "@mui/material";
18 | import { Visibility, VisibilityOff } from "@mui/icons-material";
19 |
20 | // Components
21 | import Input from "../components/Input";
22 | import Button from "../components/Button";
23 |
24 | // Yup resolver
25 | import { yupResolver } from "@hookform/resolvers/yup";
26 |
27 | // Login schema
28 | import { loginSchema } from "@/validators";
29 |
30 | // Api
31 | import { userApi } from "@/api";
32 |
33 | // Scss
34 | import "./style.scss";
35 |
36 | const LoginPage = () => {
37 | const auth = useAuth();
38 | const navigate = useNavigate();
39 | const [showPassword, setShowPassword] = useState(true);
40 | const [loading, setLoading] = useState(false);
41 | const [error, setError] = useState(false);
42 | const { control, handleSubmit } = useForm({
43 | reValidateMode: "onSubmit",
44 | defaultValues: { username: "", password: "" },
45 | resolver: yupResolver(loginSchema),
46 | });
47 |
48 | const handleClickShowPassword = () => setShowPassword(!showPassword);
49 |
50 | const handleLogin = (user) => {
51 | (async () => {
52 | try {
53 | setLoading(true);
54 |
55 | user = { taiKhoan: user.username, matKhau: user.password };
56 | user = await userApi.login(user);
57 | auth.login(user);
58 |
59 | navigate(-1);
60 | } catch (error) {
61 | setError(error);
62 | } finally {
63 | setLoading(false);
64 | }
65 | })();
66 | };
67 |
68 | return (
69 |
76 | {error && (
77 |
78 | {error}
79 |
80 | )}
81 |
82 |
90 |
91 | {showPassword ? : }
92 |
93 |
94 | ),
95 | }}
96 | sx={{ mb: 2 }}
97 | />
98 | }
100 | label="Ghi nhớ đăng nhập"
101 | className="remember-login"
102 | />
103 |
106 |
107 |
108 | Quên mật khẩu?
109 |
110 |
111 | Không có tài khoản? Đăng ký
112 |
113 |
114 |
115 | );
116 | };
117 |
118 | export default LoginPage;
119 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/CinemaSystem/CinemaGroup/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | //Material UI
5 | import { Tabs, Tab, Typography, Box } from "@mui/material";
6 |
7 | //Components
8 | import Image from "@/components/Image";
9 | import MovieSchedule from "../MovieSchedule";
10 |
11 | function TabPanel(props) {
12 | const { children, value, index, ...other } = props;
13 |
14 | return (
15 |
22 | {value === index && (
23 |
24 | {children}
25 |
26 | )}
27 |
28 | );
29 | }
30 |
31 | TabPanel.propTypes = {
32 | children: PropTypes.node,
33 | index: PropTypes.number.isRequired,
34 | value: PropTypes.number.isRequired,
35 | };
36 |
37 | function a11yProps(index) {
38 | return {
39 | id: `vertical-tab-${index}`,
40 | "aria-controls": `vertical-tabpanel-${index}`,
41 | };
42 | }
43 |
44 | function CinemaGroup({ data }) {
45 | const [value, setValue] = React.useState(0);
46 |
47 | const handleChange = (event, newValue) => {
48 | setValue(newValue);
49 | };
50 |
51 | const renderTab = () => {
52 | return data.lstCumRap.map((cinemaGroup, index) => {
53 | return (
54 |
65 | }
66 | />
67 | );
68 | });
69 | };
70 |
71 | const renderTabPanel = () => {
72 | return data.lstCumRap?.map((cinemaGroup, index) => {
73 | return (
74 |
93 | {cinemaGroup.danhSachPhim?.map((movie, index) => (
94 |
95 | ))}
96 |
97 | );
98 | });
99 | };
100 |
101 | return (
102 |
106 |
118 | {renderTab()}
119 |
120 | {renderTabPanel()}
121 |
122 | );
123 | }
124 |
125 | export default CinemaGroup;
126 |
--------------------------------------------------------------------------------
/src/store/reducers/ticketBooking.js:
--------------------------------------------------------------------------------
1 | import * as actType from "../constants/ticketBooking";
2 |
3 | const initialState = {
4 | ticketBookingDetails: {
5 | data: null,
6 | loading: false,
7 | error: "",
8 | },
9 | bookTicket: {
10 | loading: false,
11 | error: "",
12 | },
13 | selectedSeats: [],
14 | modal: { open: false, title: "", children: [], buttonContent: "Chấp nhận", path: "" },
15 | };
16 |
17 | const ticketBookingReducer = (state = initialState, { type, payload }) => {
18 | switch (type) {
19 | // Fetch ticket booking details
20 | case actType.GET_TICKET_BOOKING_DETAILS_REQUEST:
21 | return {
22 | ...state,
23 | ticketBookingDetails: {
24 | data: null,
25 | loading: true,
26 | error: "",
27 | },
28 | };
29 |
30 | case actType.GET_TICKET_BOOKING_DETAILS_FAIL:
31 | return {
32 | ...state,
33 | ticketBookingDetails: {
34 | data: null,
35 | loading: false,
36 | error: payload,
37 | },
38 | };
39 |
40 | case actType.GET_TICKET_BOOKING_DETAILS_SUCCESS:
41 | return {
42 | ...state,
43 | ticketBookingDetails: {
44 | data: payload,
45 | loading: false,
46 | error: "",
47 | },
48 | selectedSeats: [],
49 | };
50 |
51 | // Book ticket
52 | case actType.BOOK_TICKET_REQUEST:
53 | return {
54 | ...state,
55 | bookTicket: {
56 | loading: true,
57 | error: "",
58 | },
59 | };
60 |
61 | case actType.BOOK_TICKET_FAIL:
62 | return {
63 | ...state,
64 | bookTicket: {
65 | loading: false,
66 | error: payload,
67 | },
68 | };
69 |
70 | case actType.BOOK_TICKET_SUCCESS:
71 | if (!state.selectedSeats.length) {
72 | return {
73 | ...state,
74 | bookTicket: {
75 | loading: false,
76 | error: "",
77 | },
78 | modal: {
79 | ...state.modal,
80 | open: true,
81 | title: "Thông báo",
82 | children: ["Vui lòng chọn ít nhất một chỗ ngồi! Bạn có thể mua được tối đa 5 ghế."],
83 | path: "",
84 | },
85 | };
86 | }
87 |
88 | return {
89 | ...state,
90 | bookTicket: {
91 | loading: false,
92 | error: "",
93 | },
94 | modal: {
95 | ...state.modal,
96 | open: true,
97 | title: "Thông báo",
98 | children: [
99 | "Đặt vé thành công!",
100 | "Chúc bạn có trải nghiệm xem phim vui vẻ tại Finnkino Cinema",
101 | ],
102 | path: "/",
103 | },
104 | };
105 |
106 | // Choose seat
107 | case actType.CHOOSE_SEAT:
108 | const selectedSeats = [...state.selectedSeats];
109 |
110 | const idx = selectedSeats.findIndex((selectedSeat) => selectedSeat.id === payload.id);
111 | if (idx !== -1) {
112 | selectedSeats.splice(idx, 1);
113 | return { ...state, selectedSeats };
114 | }
115 |
116 | if (selectedSeats.length === 5) {
117 | return {
118 | ...state,
119 | modal: {
120 | ...state.modal,
121 | open: true,
122 | title: "Thông báo",
123 | children: [
124 | "Bạn chỉ có thể mua được tối đa 5 ghế.",
125 | "Vui lòng liên hệ supports@finnkino.com để được hỗ trợ tốt hơn.",
126 | ],
127 | path: "",
128 | },
129 | };
130 | }
131 |
132 | selectedSeats.push(payload);
133 |
134 | return { ...state, selectedSeats };
135 |
136 | // Close modal
137 | case actType.CLOSE_MODAL:
138 | return { ...state, modal: { ...state.modal, open: false } };
139 | default:
140 | return state;
141 | }
142 | };
143 |
144 | export default ticketBookingReducer;
145 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/HomePage/CinemaSystem/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import PropTypes from "prop-types";
4 |
5 | import actGetCinemaList from "@/store/actions/cinemaSystem";
6 |
7 | //Material UI
8 | import { Tab, Typography, Tabs, Box } from "@mui/material";
9 |
10 | //Components
11 | import Image from "@/components/Image";
12 | import CinemaGroup from "./CinemaGroup";
13 | import Loader from "@/components/Loader";
14 |
15 | import "./style.scss";
16 |
17 | function TabPanel(props) {
18 | const { children, value, index, ...other } = props;
19 |
20 | return (
21 |
28 | {value === index && (
29 |
30 | {children}
31 |
32 | )}
33 |
34 | );
35 | }
36 |
37 | TabPanel.propTypes = {
38 | children: PropTypes.node,
39 | index: PropTypes.number.isRequired,
40 | value: PropTypes.number.isRequired,
41 | };
42 |
43 | function a11yProps(index) {
44 | return {
45 | id: `vertical-tab-${index}`,
46 | "aria-controls": `vertical-tabpanel-${index}`,
47 | };
48 | }
49 |
50 | function CinemaSystem() {
51 | const dispatch = useDispatch();
52 | const cinemaSystemData = useSelector((state) => state.cinemaSystem.data);
53 | const loading = useSelector((state) => state.cinemaSystem.loading);
54 | const [value, setValue] = useState(0);
55 |
56 | useEffect(() => {
57 | dispatch(actGetCinemaList());
58 | }, []);
59 |
60 | const handleChange = (event, newValue) => {
61 | setValue(newValue);
62 | };
63 |
64 | const logoTabStyle = {
65 | minWidth: "var(--logo-size)",
66 | width: "var(--logo-size)",
67 | minHeight: "var(--logo-size)",
68 | height: "var(--logo-size)",
69 | padding: "10px",
70 | };
71 |
72 | const renderTab = () => {
73 | return cinemaSystemData?.map((cinemaSystem, index) => (
74 |
84 | }
85 | {...a11yProps(index)}
86 | variant="fullWidth"
87 | />
88 | ));
89 | };
90 |
91 | const renderTabPanel = () => {
92 | return cinemaSystemData?.map((cinemaGroup, index) => (
93 |
94 |
95 |
96 | ));
97 | };
98 |
99 | return (
100 | <>
101 | {loading ? (
102 |
103 | ) : (
104 |
105 |
116 |
132 | {renderTab()}
133 |
134 | {renderTabPanel()}
135 |
136 |
137 | )}
138 | >
139 | );
140 | }
141 |
142 | export default CinemaSystem;
143 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/components/Footer/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | //FontAwesome
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import {
6 | faTwitter,
7 | faInstagram,
8 | faFacebook,
9 | faSnapchat,
10 | faLinkedin,
11 | faYoutube,
12 | } from "@fortawesome/free-brands-svg-icons";
13 |
14 | // Material UI
15 | import { Grid } from "@mui/material";
16 |
17 | // Components
18 | import Image from "@/components/Image";
19 | import images from "@/assets/images";
20 |
21 | import "./style.scss";
22 |
23 | function Footer() {
24 | return (
25 |
119 | );
120 | }
121 |
122 | export default Footer;
123 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/TicketBookingPage/TicketBookingCard/index.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 |
3 | // Material UI
4 | import {
5 | Card,
6 | CardMedia,
7 | CardContent,
8 | CardActions,
9 | Divider,
10 | Typography,
11 | List,
12 | ListItem,
13 | ListItemText,
14 | Box,
15 | Stack,
16 | } from "@mui/material";
17 | import { LoadingButton } from "@mui/lab";
18 |
19 | // Components
20 | import Loader from "@/components/Loader";
21 |
22 | // Redux actions
23 | import { actBookTicket } from "@/store/actions/ticketBooking";
24 |
25 | // Scss
26 | import "./style.scss";
27 |
28 | const TicketBookingCard = () => {
29 | const dispatch = useDispatch();
30 | const { ticketBookingDetails, selectedSeats, bookTicket } = useSelector(
31 | (rootReducer) => rootReducer.ticketBooking,
32 | );
33 |
34 | const movie = ticketBookingDetails.data?.thongTinPhim;
35 | const loading = ticketBookingDetails.loading;
36 |
37 | const renderSelectedSeats = () =>
38 | selectedSeats?.map((selectedSeat, idx) => {
39 | const endLine = idx === selectedSeats.length - 1;
40 | return (
41 |
42 | {selectedSeat.code}
43 | {endLine ? "" : ", "}
44 |
45 | );
46 | });
47 |
48 | const renderPriceTotal = () => {
49 | const priceTotal = selectedSeats?.reduce(
50 | (priceTotal, selectedSeat) => priceTotal + selectedSeat.price,
51 | 0,
52 | );
53 |
54 | return priceTotal.toLocaleString();
55 | };
56 |
57 | const handleBookTicket = () => {
58 | const ticket = {
59 | maLichChieu: movie?.maLichChieu,
60 | danhSachVe: selectedSeats?.map((seat) => ({ maGhe: seat.id, giaVe: seat.price })),
61 | };
62 |
63 | dispatch(actBookTicket(ticket));
64 | };
65 |
66 | return (
67 |
68 | {loading ? (
69 |
70 | ) : (
71 | <>
72 | {/* Movie image */}
73 |
79 | {/* Card content */}
80 |
81 | {/* Movie name */}
82 |
83 | {movie?.tenPhim}
84 |
85 |
86 | C13
87 |
88 | (*) Phim chỉ dành cho khán giả từ 13 tuổi trở lên
89 |
90 |
91 | {/* Booking details */}
92 |
93 |
94 |
95 | Rạp: {movie?.tenCumRap} | {movie?.tenRap}
96 |
97 |
98 |
99 |
100 | Địa chỉ: {movie?.diaChi}
101 |
102 |
103 |
104 |
105 | Ngày chiếu: {movie?.gioChieu} | {movie?.ngayChieu}
106 |
107 |
108 |
109 |
110 | {/* Booked Seats */}
111 |
112 |
113 |
114 | Ghế: {renderSelectedSeats()}
115 |
116 |
117 |
118 |
119 | {/* Total payment */}
120 |
121 | Tổng:{" "}
122 | {renderPriceTotal()} VNĐ
123 |
124 |
125 | {/* Book ticket */}
126 |
127 |
133 | Đặt Vé
134 |
135 | {" "}
136 | >
137 | )}
138 |
139 | );
140 | };
141 |
142 | export default TicketBookingCard;
143 |
--------------------------------------------------------------------------------
/src/containers/AuthTemplate/RegisterPage/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useNavigate, Navigate, Link as RouterLink } from "react-router-dom";
3 | import { useAuth } from "@/hooks";
4 | import { useForm } from "react-hook-form";
5 |
6 | // Material UI
7 | import {
8 | FormControlLabel,
9 | Checkbox,
10 | Link,
11 | Box,
12 | InputAdornment,
13 | Stack,
14 | Alert,
15 | IconButton,
16 | Typography,
17 | } from "@mui/material";
18 |
19 | import { Visibility, VisibilityOff } from "@mui/icons-material";
20 |
21 | // Components
22 | import Input from "../components/Input";
23 | import Button from "../components/Button";
24 |
25 | // Yup resolver
26 | import { yupResolver } from "@hookform/resolvers/yup";
27 |
28 | // Register schema
29 | import { registerSchema } from "@/validators";
30 |
31 | // Api
32 | import { userApi } from "@/api";
33 |
34 | // Constants
35 | import { GROUP_ID } from "@/constants";
36 |
37 | // Scss
38 | import "./style.scss";
39 |
40 | const RegisterPage = () => {
41 | const auth = useAuth();
42 | const navigate = useNavigate();
43 | const [showPassword, setShowPassword] = useState(true);
44 | const [showConfirmedPassword, setShowConfirmedPassword] = useState(true);
45 | const [checked, setChecked] = useState(false);
46 | const [loading, setLoading] = useState(false);
47 | const [error, setError] = useState("");
48 | const { control, handleSubmit } = useForm({
49 | reValidateMode: "onSubmit",
50 | defaultValues: {
51 | fullName: "",
52 | username: "",
53 | email: "",
54 | phoneNumber: "",
55 | password: "",
56 | confirmedPassword: "",
57 | },
58 | resolver: yupResolver(registerSchema),
59 | });
60 |
61 | if (auth.user) {
62 | return ;
63 | }
64 |
65 | const handleShowPassword = () => setShowPassword(!showPassword);
66 | const handleShowConfirmedPassword = () => setShowConfirmedPassword(!showConfirmedPassword);
67 |
68 | const handleRegister = (user) => {
69 | (async () => {
70 | try {
71 | setLoading(true);
72 |
73 | // Register
74 | user = {
75 | hoTen: user.fullName,
76 | taiKhoan: user.username,
77 | email: user.email,
78 | soDt: user.phoneNumber,
79 | matKhau: user.password,
80 | maNhom: GROUP_ID,
81 | };
82 | user = await userApi.register(user);
83 |
84 | // Login automatically if registering successfully
85 | user = { taiKhoan: user.taiKhoan, matKhau: user.matKhau };
86 | user = await userApi.login(user);
87 | auth.login(user);
88 | navigate("/", { replace: true });
89 | } catch (error) {
90 | setError(error);
91 | } finally {
92 | setLoading(false);
93 | }
94 | })();
95 | };
96 |
97 | return (
98 |
105 | {error && (
106 |
107 | {error}
108 |
109 | )}
110 |
111 |
112 |
113 |
114 |
122 |
123 | {showPassword ? : }
124 |
125 |
126 | ),
127 | }}
128 | />
129 |
137 |
141 | {showConfirmedPassword ? : }
142 |
143 |
144 | ),
145 | }}
146 | />
147 | setChecked(!checked)} />
150 | }
151 | className="accept-policies"
152 | label={
153 |
154 | Tôi chấp nhận điều khoản và dịch vụ của Finnkino.
155 |
156 | }
157 | />
158 |
161 |
162 |
163 | Đã có tài khoản? Đăng nhập
164 |
165 |
166 |
167 | );
168 | };
169 |
170 | export default RegisterPage;
171 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/MovieDetailsPage/style.scss:
--------------------------------------------------------------------------------
1 | .movie-detail__top-info {
2 | position: relative;
3 | padding-bottom: 50px;
4 | padding-top: 40px;
5 |
6 | &:before {
7 | position: absolute;
8 | content: "";
9 | top: 0;
10 | right: 0;
11 | bottom: 0;
12 | left: 0;
13 | background-color: rgba(0, 0, 0, 0.5);
14 | }
15 |
16 | .row {
17 | position: relative;
18 | }
19 | }
20 |
21 | .movie-detail__top-background {
22 | > img {
23 | position: absolute;
24 | top: 0;
25 | width: 100%;
26 | left: 0;
27 | height: 100%;
28 | object-fit: cover;
29 | object-position: top;
30 | }
31 |
32 | &:before {
33 | position: absolute;
34 | content: "";
35 | top: 0;
36 | right: 0;
37 | bottom: 0;
38 | left: 0;
39 | background-color: rgba(0, 0, 0, 0.5);
40 | z-index: 1;
41 | }
42 | }
43 |
44 | .movie-detail__top-info-wrapper {
45 | position: relative;
46 | display: flex;
47 | align-items: flex-end;
48 | height: 100%;
49 | z-index: 5;
50 |
51 | & > div {
52 | align-items: flex-end;
53 | }
54 | }
55 |
56 | .top-info__img {
57 | width: 30%;
58 |
59 | img {
60 | width: 90%;
61 | }
62 | }
63 |
64 | .top-info__content {
65 | width: 70%;
66 | text-align: start;
67 |
68 | .top-info__btn {
69 | font-size: 18px;
70 | padding: 10px 16px;
71 | min-width: unset;
72 | }
73 |
74 | .top-info__btn + .top-info__btn {
75 | margin-left: 20px;
76 | }
77 | }
78 |
79 | .top-info__content-title {
80 | font-size: 26px;
81 | font-weight: 700;
82 | color: var(--primary);
83 | }
84 |
85 | .top-info__btn.filled {
86 | &:hover {
87 | background-color: #caa100;
88 | border-color: #c09900;
89 | transition: all 0.2s ease;
90 | }
91 | }
92 |
93 | .movie-detail__top-desc {
94 | display: flex;
95 | justify-content: space-between;
96 | align-items: center;
97 | padding: 15px 100px;
98 | font-size: 13px;
99 | background: var(--gray);
100 |
101 | p {
102 | margin: 0;
103 | }
104 |
105 | p + p {
106 | margin-left: 20px;
107 | }
108 |
109 | .movie-detail__desc-icon {
110 | margin-right: 10px;
111 | }
112 |
113 | .movie-detail__desc-btn {
114 | color: var(--white);
115 | padding: 5px 15px 5px 6px;
116 | margin: 0 3px;
117 | }
118 | }
119 |
120 | .movie-detail__desc-left {
121 | display: flex;
122 | color: var(--light-gray);
123 | }
124 |
125 | .movie-detail__desc-btn {
126 | &:hover {
127 | text-decoration: underline;
128 | }
129 |
130 | .movie-detail__desc-btn-icon {
131 | font-size: 12px;
132 | }
133 |
134 | &.desc-btn--facebook {
135 | background-color: #3b5998;
136 | &:hover {
137 | background-color: #2d4373;
138 | }
139 | }
140 |
141 | &.desc-btn--twitter {
142 | background-color: #00aced;
143 | &:hover {
144 | background-color: #0087ba;
145 | }
146 | }
147 |
148 | &.desc-btn--whatsapp {
149 | background-color: #29a628;
150 | &:hover {
151 | background-color: #1f7d1e;
152 | }
153 | }
154 |
155 | &.desc-btn--email {
156 | background-color: #3490f3;
157 | &:hover {
158 | background-color: #0e76e6;
159 | }
160 | }
161 | }
162 |
163 | .movie-detail__content-wrapper {
164 | padding: 50px 0 35px 0;
165 | background-color: var(--white);
166 | color: var(--black);
167 |
168 | .movie-detail__content-title {
169 | font-size: 24px;
170 | font-weight: 700;
171 | margin-bottom: 10px;
172 | }
173 |
174 | p {
175 | font-size: 14px;
176 | text-align: justify;
177 | }
178 | }
179 |
180 | .movie-detail__content {
181 | flex-direction: row;
182 | }
183 |
184 | .movie-detail__syno,
185 | .movie-detail__details {
186 | padding-bottom: 15px;
187 | }
188 |
189 | .movie-detail__ticket {
190 | padding-top: 48px;
191 | background-color: var(--dark-gray);
192 | color: var(--white);
193 |
194 | .movie-detail__ticket-title {
195 | font-size: 24px;
196 | color: var(--primary);
197 | }
198 | }
199 |
200 | @media (max-width: 1023px) {
201 | .movie-detail__top-info-wrapper {
202 | flex-direction: column;
203 | justify-content: flex-end;
204 | align-items: center;
205 | }
206 |
207 | .top-info__img {
208 | padding: 20px 0 50px 0;
209 | }
210 |
211 | .top-info__btn {
212 | position: absolute;
213 | transform: translateY(50%);
214 | }
215 |
216 | .movie-detail__top-desc {
217 | padding: 18px;
218 |
219 | .movie-detail__desc-btn {
220 | padding: 5px 4px;
221 | min-width: unset;
222 | }
223 |
224 | .movie-detail__desc-btn-icon {
225 | margin-left: 11px !important;
226 | }
227 | }
228 |
229 | .movie-detail__content {
230 | flex-direction: column-reverse;
231 | }
232 | }
233 |
234 | @media (min-width: 740px) and (max-width: 1023px) {
235 | .movie-detail__top-info {
236 | padding-bottom: 0;
237 | padding-top: 0;
238 | }
239 | .top-info__img {
240 | img {
241 | width: 240px;
242 | height: 363px;
243 | object-fit: cover;
244 | }
245 | }
246 | }
247 |
248 | @media (max-width: 739px) {
249 | .movie-detail__top-info-wrapper {
250 | justify-content: center;
251 | }
252 |
253 | .movie-detail__top-info {
254 | padding-bottom: 0;
255 | padding-top: 0;
256 | }
257 |
258 | .top-info__img {
259 | img {
260 | width: 94px;
261 | height: 142px;
262 | object-fit: cover;
263 | }
264 | }
265 |
266 | .top-info__btn {
267 | bottom: 0;
268 | }
269 |
270 | .movie-detail__top-desc {
271 | padding: 13px 0;
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/ProfilePage/TransactionHistory/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useSelector } from "react-redux";
3 | import PropTypes from "prop-types";
4 |
5 | // Material UI
6 | import { useTheme } from "@mui/material/styles";
7 | import {
8 | Box,
9 | Table,
10 | TableBody,
11 | TableHead,
12 | TableCell,
13 | TableContainer,
14 | TableFooter,
15 | TablePagination,
16 | TableRow,
17 | Paper,
18 | IconButton,
19 | } from "@mui/material";
20 | import { FirstPage, KeyboardArrowLeft, KeyboardArrowRight, LastPage } from "@mui/icons-material";
21 |
22 | // Format date
23 | import moment from "moment";
24 |
25 | // Constants
26 | import { ALPHABET } from "@/constants";
27 |
28 | // Scss
29 | import "./style.scss";
30 |
31 | function TablePaginationActions(props) {
32 | const theme = useTheme();
33 | const { count, page, rowsPerPage, onPageChange } = props;
34 |
35 | const handleFirstPageButtonClick = (event) => {
36 | onPageChange(event, 0);
37 | };
38 |
39 | const handleBackButtonClick = (event) => {
40 | onPageChange(event, page - 1);
41 | };
42 |
43 | const handleNextButtonClick = (event) => {
44 | onPageChange(event, page + 1);
45 | };
46 |
47 | const handleLastPageButtonClick = (event) => {
48 | onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
49 | };
50 |
51 | return (
52 |
53 |
58 | {theme.direction === "rtl" ? : }
59 |
60 |
61 | {theme.direction === "rtl" ? : }
62 |
63 | = Math.ceil(count / rowsPerPage) - 1}
66 | aria-label="next page"
67 | >
68 | {theme.direction === "rtl" ? : }
69 |
70 | = Math.ceil(count / rowsPerPage) - 1}
73 | aria-label="last page"
74 | >
75 | {theme.direction === "rtl" ? : }
76 |
77 |
78 | );
79 | }
80 |
81 | TablePaginationActions.propTypes = {
82 | count: PropTypes.number.isRequired,
83 | onPageChange: PropTypes.func.isRequired,
84 | page: PropTypes.number.isRequired,
85 | rowsPerPage: PropTypes.number.isRequired,
86 | };
87 |
88 | const columns = [
89 | { id: "ticket-id", label: "Mã vé", align: "center", minWidth: 80 },
90 | { id: "movie-name", label: "Tên phim", align: "center", minWidth: 100 },
91 | { id: "showtime", label: "Ngày chiếu", align: "center", minWidth: 170 },
92 | { id: "movie-duration", label: "Thời lượng", align: "center", minWidth: 120 },
93 | {
94 | id: "cinema",
95 | label: "Rạp",
96 | align: "center",
97 | minWidth: 100,
98 | },
99 | { id: "ticket-cost", label: "Giá vé", align: "center", minWidth: 120 },
100 | {
101 | id: "selected-seats",
102 | label: "Ghế đã đặt",
103 | align: "center",
104 | minWidth: 195,
105 | },
106 | ];
107 |
108 | const TransactionHistory = () => {
109 | const { content } = useSelector((rootReducer) => rootReducer.userProfile.data);
110 | const [page, setPage] = useState(0);
111 | const [rowsPerPage, setRowsPerPage] = useState(5);
112 |
113 | const rows = content?.thongTinDatVe;
114 |
115 | const handleChangePage = (event, newPage) => setPage(newPage);
116 |
117 | const handleChangeRowsPerPage = (event) => {
118 | setRowsPerPage(parseInt(event.target.value, 10));
119 | setPage(0);
120 | };
121 |
122 | const renderTableBody = () =>
123 | (rowsPerPage > 0
124 | ? rows?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
125 | : rows
126 | )?.map((row) => {
127 | let seats = row?.danhSachGhe;
128 | const { tenCumRap, tenHeThongRap } = seats[0];
129 |
130 | seats = seats.map((seat, idx) => {
131 | // Number of seat rows in ticket booking page
132 | const nRow = 16;
133 | const seatIndicator = ALPHABET[Math.floor((+seat.tenGhe - 1) / nRow)];
134 | const seatIdx = ((+seat.tenGhe - 1) % nRow) + 1;
135 | const seatCode = seatIndicator + seatIdx;
136 |
137 | const isLastSeat = idx === seats.length - 1;
138 |
139 | return (
140 |
141 | {seatCode}
142 | {isLastSeat ? "" : ", "}
143 |
144 | );
145 | });
146 |
147 | return (
148 |
149 | {row?.maVe}
150 | {row?.tenPhim}
151 |
152 | {moment(row?.ngayDat).format("HH:mm")}, {moment(row?.ngayDat).format("DD/MM/YYY")}
153 |
154 | {row.thoiLuongPhim} phút
155 |
156 | {tenHeThongRap}, {tenCumRap}
157 |
158 | {row.giaVe.toLocaleString()} VNĐ
159 | {seats}
160 |
161 | );
162 | });
163 |
164 | return (
165 |
166 |
167 |
168 |
169 | {columns.map((column) => (
170 |
176 | {column.label}
177 |
178 | ))}
179 |
180 |
181 | {renderTableBody()}
182 |
183 |
184 |
200 |
201 |
202 |
203 |
204 | );
205 | };
206 |
207 | export default TransactionHistory;
208 |
--------------------------------------------------------------------------------
/src/containers/AdminTemplate/MovieDashBoard/ScheduleModal/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import Box from "@mui/material/Box";
3 | import Button from "@mui/material/Button";
4 | import Typography from "@mui/material/Typography";
5 | import Modal from "@mui/material/Modal";
6 | import { Formik, useFormik } from "formik";
7 | import {
8 | Alert,
9 | FormControl,
10 | FormHelperText,
11 | FormLabel,
12 | MenuItem,
13 | Select,
14 | TextField,
15 | } from "@mui/material";
16 | import { SubmitButton } from "../../components/Buttons";
17 | import { DateTimePicker } from "@mui/x-date-pickers";
18 |
19 | //moment
20 | import moment from "moment";
21 |
22 | import { cinemaApi, ticketBookingApi } from "@/api";
23 | import { movieScheduleSchema } from "@/validators";
24 | const style = {
25 | position: "absolute",
26 | top: "50%",
27 | left: "50%",
28 | transform: "translate(-50%, -50%)",
29 | width: 500,
30 | bgcolor: "background.paper",
31 | border: "2px solid #000",
32 | boxShadow: 24,
33 | p: 4,
34 | };
35 |
36 | function ScheduleModal({ movieId, openScheduleModal, setOpenScheduleModal }) {
37 | const [value, setValue] = useState(new Date("2014-08-18T21:11:54"));
38 | const [serverError, setServerError] = useState("");
39 | const [cineValue, setCineValue] = useState({
40 | cineSystem: [],
41 | cineGroup: [],
42 | });
43 |
44 | useEffect(() => {
45 | const fetchData = async () => {
46 | try {
47 | const result = await cinemaApi.getCinemaSystemList();
48 | setCineValue({ ...cineValue, cineSystem: result });
49 | } catch (error) {
50 | console.log(error);
51 | }
52 | };
53 | fetchData();
54 | }, []);
55 |
56 | const { errors, touched, setFieldValue, handleSubmit, handleChange, handleBlur } = useFormik({
57 | initialValues: {
58 | ngayChieuGioChieu: "",
59 | maRap: "",
60 | giaVe: -1,
61 | },
62 | validationSchema: movieScheduleSchema,
63 | onSubmit: (values) => {
64 | values.maPhim = movieId;
65 |
66 | const fetchShowTime = async () => {
67 | try {
68 | await ticketBookingApi.createShowtime(values);
69 | setOpenScheduleModal(false);
70 | } catch (error) {
71 | setServerError(error);
72 | }
73 | };
74 |
75 | fetchShowTime();
76 | },
77 | });
78 |
79 | const handleClose = () => setOpenScheduleModal(false);
80 |
81 | const handleChangeCineSystem = async (e) => {
82 | const cineSystem = e.target.value;
83 | try {
84 | const result = await cinemaApi.getCinemaGroupBySystem(cineSystem);
85 | setCineValue({ ...cineValue, cineGroup: result });
86 | } catch (error) {
87 | setServerError(error);
88 | }
89 | };
90 |
91 | const handleChangeCineGroup = (e) => {
92 | setFieldValue("maRap", e.target.value);
93 | };
94 |
95 | const handleChangeDateTime = (newValue) => {
96 | setValue(newValue);
97 | let dateTime = moment(newValue).format("DD/MM/YYYY hh:mm:ss");
98 | setFieldValue("ngayChieuGioChieu", dateTime);
99 | };
100 |
101 | const renderCineSystem = () => {
102 | return cineValue?.cineSystem.map((item) => (
103 |
106 | ));
107 | };
108 |
109 | const renderCineGroup = () => {
110 | return cineValue?.cineGroup.map((item) => (
111 |
114 | ));
115 | };
116 |
117 | return (
118 |
119 |
125 |
126 |
127 | Tạo lịch chiếu
128 |
129 | {serverError ? (
130 |
131 | {serverError}
132 |
133 | ) : (
134 | ""
135 | )}
136 |
137 |
138 |
139 | Hệ thống rạp
140 |
149 | {errors.maRap && {errors.maRap}}
150 |
151 |
152 | Cụm thống rạp
153 |
162 | {errors.maRap && {errors.maRap}}
163 |
164 |
165 | Ngày chiếu giờ chiếu
166 | }
173 | error={errors.ngayChieuGioChieu && touched.ngayChieuGioChieu ? true : false}
174 | />
175 | {errors.ngayChieuGioChieu && touched.ngayChieuGioChieu && (
176 | {errors.ngayChieuGioChieu}
177 | )}
178 |
179 |
180 | Giá vé
181 |
192 | {errors.giaVe && touched.giaVe && (
193 | {errors.giaVe}
194 | )}
195 |
196 | Tạo lịch chiếu
197 |
198 |
199 |
200 |
201 |
202 | );
203 | }
204 |
205 | export default ScheduleModal;
206 |
--------------------------------------------------------------------------------
/src/containers/HomeTemplate/MovieDetailsPage/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Link, useParams } from "react-router-dom";
4 | import moment from "moment";
5 |
6 | //FontAwesome
7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
8 | import { faAnglesRight, faAt, faHeart, faPlay } from "@fortawesome/free-solid-svg-icons";
9 | import { faFacebookF, faTwitter, faWhatsapp } from "@fortawesome/free-brands-svg-icons";
10 |
11 | //Material UI
12 | import { Box, Button, Container, Grid, Typography } from "@mui/material";
13 |
14 | //Components
15 | import Image from "@/components/Image";
16 | import actFetchMovieDetails from "@/store/actions/movieDetails";
17 | import Loader from "@/components/Loader";
18 |
19 | import "./style.scss";
20 |
21 | function MovieDetailsPage() {
22 | const dispatch = useDispatch();
23 | const data = useSelector((state) => state.movieDetails.data);
24 | const loading = useSelector((state) => state.movieDetails.loading);
25 |
26 | const movieID = useParams();
27 |
28 | useEffect(() => {
29 | dispatch(actFetchMovieDetails(movieID.id));
30 | }, []);
31 |
32 | const socialList = [
33 | {
34 | name: "Share",
35 | icon: (
36 |
40 | ),
41 | className: "desc-btn--facebook",
42 | },
43 | {
44 | name: "Tweet",
45 | icon: (
46 |
50 | ),
51 | className: "desc-btn--twitter",
52 | },
53 | {
54 | name: "WhatsApp",
55 | icon: (
56 |
60 | ),
61 | className: "desc-btn--whatsapp",
62 | },
63 | {
64 | name: "E-mail",
65 | icon: (
66 |
70 | ),
71 | className: "desc-btn--email",
72 | },
73 | ];
74 |
75 | const renderSocialBtn = () => {
76 | return socialList.map((item, index) => (
77 |
88 | ));
89 | };
90 | const renderLoader = () => {
91 | if (loading) return ;
92 | };
93 |
94 | return (
95 | <>
96 | {renderLoader()}
97 | {data && (
98 |
99 |
100 |
104 | {/* Background */}
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | {/* Top info for PC screen */}
115 |
116 |
117 | {data.tenPhim}
118 |
119 | }
124 | href={data.trailer}
125 | >
126 | Play Trailer
127 |
128 | }
133 | >
134 | Tickets
135 |
136 |
137 |
138 | {/* Top info for tablet + mobile screens */}
139 | }
144 | >
145 | Tickets
146 |
147 |
148 |
149 |
150 | {/* Top info for pc */}
151 |
152 |
153 |
154 |
155 | Rating: {data.danhGia}
156 |
157 | {data.hot && "Hot"}
158 |
159 | {renderSocialBtn()}
160 |
161 |
162 |
163 |
164 |
170 | {data.tenPhim}
171 |
172 |
173 |
174 | Mô tả
175 | {data.moTa}
176 |
177 |
178 | Chi tiết
179 | Ngày công chiếu: {moment(data.ngayKhoiChieu).format("D/M/YYYY hh:mm")}
180 |
181 |
182 |
183 |
184 | {/* Top info for tablet + mobile screens */}
185 |
186 |
187 |
188 |
189 | Rating: {data.danhGia}
190 |
191 | {data.hot && "Hot"}
192 |
193 | {renderSocialBtn()}
194 |
195 |
196 | )}
197 | >
198 | );
199 | }
200 |
201 | export default MovieDetailsPage;
202 |
--------------------------------------------------------------------------------