From 0224c5a9f031dbfe9ed311d9d273807500ab631c Mon Sep 17 00:00:00 2001 From: Boming Zhang Date: Tue, 7 May 2024 14:57:08 -0400 Subject: [PATCH 1/6] feat: whole booking process --- src/pages/Checkout/index.tsx | 41 +++++--- src/pages/MovieManage/index.tsx | 4 +- src/pages/MovieSeatSelect/index.tsx | 45 ++++++--- src/pages/MovieSelect/index.tsx | 54 +++++++--- src/pages/OrderConfirmation/index.tsx | 79 ++++++++++++--- src/pages/OrderHistory/index.tsx | 4 +- src/pages/OrderSummary/index.tsx | 137 ++++++++++++++++---------- 7 files changed, 257 insertions(+), 107 deletions(-) diff --git a/src/pages/Checkout/index.tsx b/src/pages/Checkout/index.tsx index d39cbc3..aa7068a 100644 --- a/src/pages/Checkout/index.tsx +++ b/src/pages/Checkout/index.tsx @@ -1,29 +1,20 @@ import './style.module.css' import { useRequest } from 'ahooks' import type { SchemaCard } from 'client' +import type { ErrorResponse } from 'client/error' import Card from 'components/Card' import PageContainer from 'components/PageContainer' import type React from 'react' import { useState } from 'react' import { Form, Button, Col, Row, Alert } from 'react-bootstrap' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useSearchParams } from 'react-router-dom' import Backend from 'utils/service' const RegistrationForm = () => { const navigate = useNavigate() + const [searchParams] = useSearchParams() const [error, setError] = useState('') const [cards, setCards] = useState([]) - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - setError('TODO: Implement Checkout') - navigate('/order/confirm') - } - useRequest(async () => Backend.user.v1UsersMeList(), { - onSuccess: res => { - setCards(res.data.cards) - } - }) const [card, setCard] = useState({ id: '', number: '', @@ -36,6 +27,32 @@ const RegistrationForm = () => { type: '' }) + const { run: checkoutOrder } = useRequest( + async () => + Backend.order.v1OrdersCheckoutCreate( + searchParams.get('order') ?? '', + card + ), + { + manual: true, + onSuccess: data => { + navigate(`/order/confirm?order=${data.data.id}`) + }, + onError: err => { + setError((err as ErrorResponse).error.msg) + } + } + ) + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + checkoutOrder() + } + useRequest(async () => Backend.user.v1UsersMeList(), { + onSuccess: res => { + setCards(res.data.cards) + } + }) + return ( <>
diff --git a/src/pages/MovieManage/index.tsx b/src/pages/MovieManage/index.tsx index 844ea27..c5e6b41 100644 --- a/src/pages/MovieManage/index.tsx +++ b/src/pages/MovieManage/index.tsx @@ -60,7 +60,9 @@ const MovieRow: React.FC<{ {movie.producer} {movie.rating_code} {movie.reviews} - {movie.show_time} + + {new Date(movie.show_time).toLocaleString()} + {movie.synopsis} {movie.trailer_picture} {movie.trailer_video} diff --git a/src/pages/MovieSeatSelect/index.tsx b/src/pages/MovieSeatSelect/index.tsx index d3ab706..94a5939 100644 --- a/src/pages/MovieSeatSelect/index.tsx +++ b/src/pages/MovieSeatSelect/index.tsx @@ -1,9 +1,11 @@ import styles from './style.module.css' +import { useRequest } from 'ahooks' import PageContainer from 'components/PageContainer' import { useState } from 'react' import Button from 'react-bootstrap/Button' import Form from 'react-bootstrap/Form' -import { Link, useSearchParams } from 'react-router-dom' +import { useNavigate, useSearchParams } from 'react-router-dom' +import Backend from 'utils/service' interface Seat { id: string @@ -13,11 +15,9 @@ interface Seat { } enum TicketType { - Adult = 'Adult', - Senior = 'Senior', - Children = 'Children', - Veteran = 'Veteran', - Student = 'Student' + Adult = 'adult', + Senior = 'senior', + Child = 'child' } const Index: React.FC = () => { @@ -27,7 +27,24 @@ const Index: React.FC = () => { TicketType.Adult ) const [promotion, setPromotion] = useState('') - + const navigate = useNavigate() + const { run: createOrder } = useRequest( + async () => + Backend.order.v1OrdersCreate({ + promotion_code: promotion, + show_id: searchParams.get('show') ?? '', + tickets: selectedSeats.map(seat => ({ + seat: seat.id, + type: seat.ticketType?.toString() ?? '' + })) + }), + { + manual: true, + onSuccess: data => { + navigate(`/order/summary?order=${data.data.id}`) + } + } + ) const totalSelectedSeats = selectedSeats.length const handleSeatClick = (seat: Seat) => { @@ -136,11 +153,15 @@ const Index: React.FC = () => { /> - - - + {/* + */} +
) diff --git a/src/pages/MovieSelect/index.tsx b/src/pages/MovieSelect/index.tsx index 9e43534..1fc1175 100644 --- a/src/pages/MovieSelect/index.tsx +++ b/src/pages/MovieSelect/index.tsx @@ -1,5 +1,5 @@ import { useRequest } from 'ahooks' -import type { SchemaMovie } from 'client' +import type { SchemaShow, SchemaMovie } from 'client' import PageContainer from 'components/PageContainer' import { useState } from 'react' import Button from 'react-bootstrap/Button' @@ -25,7 +25,8 @@ const Index: React.FC = () => { trailer_video: '', show_time: '' }) - const { loading } = useRequest( + const [shows, setShows] = useState([]) + const { loading: loadingMovie } = useRequest( async () => Backend.movie.v1MoviesDetail(movieId ?? ''), { onSuccess: res => { @@ -33,7 +34,14 @@ const Index: React.FC = () => { } } ) - const showTimes = ['1:30 PM', '6:45 PM', '7:45 PM'] // TODO: load real show times + const { loading: loadingShows } = useRequest( + async () => Backend.movie.v1MoviesShowsDetail(movieId ?? ''), + { + onSuccess: res => { + setShows(res.data.data ?? []) + } + } + ) return (
@@ -41,7 +49,7 @@ const Index: React.FC = () => {
- {loading ? null : ( + {loadingMovie ? null : ( { Director: {movie.director}
- {showTimes.map((time, timeIndex) => ( - - ))} + {loadingShows + ? null + : shows.map((show, index) => ( + + ))}
diff --git a/src/pages/OrderConfirmation/index.tsx b/src/pages/OrderConfirmation/index.tsx index 9b631d5..53268e2 100644 --- a/src/pages/OrderConfirmation/index.tsx +++ b/src/pages/OrderConfirmation/index.tsx @@ -1,13 +1,60 @@ +import { useRequest } from 'ahooks' +import type { SchemaShow, SchemaOrder } from 'client' import PageContainer from 'components/PageContainer' +import { useAuth } from 'hooks/useAuth' +import { useState } from 'react' import { Container, Row, Col } from 'react-bootstrap' +import { Link, useSearchParams } from 'react-router-dom' +import Backend from 'utils/service' const OrderConfirmation: React.FC = () => { - const userProfile = { - email: 'example@email.com', - movie: 'The Bee Movie', - date: '3/3/21', - location: 'MovieLand ATL' - } + const [searchParams] = useSearchParams() + const { user } = useAuth() + const [order, setOrder] = useState({ + booking_fee_price: 0, + card_id: '', + check_out: false, + created_at: '', + id: '', + movie_title: '', + promotion_id: '', + promotion_price: 0, + sales_tax_price: 0, + show_id: '', + ticket_price: 0, + tickets: [], + total_price: 0, + user_id: '' + }) + const [show, setShow] = useState({ + adult_ticket_price: 0, + booking_fee: 0, + child_ticket_price: 0, + end_time: '', + id: '', + movie_id: '', + senior_ticket_price: 0, + start_time: '', + theater_location: '' + }) + const { loading: loadingShow, run: loadShow } = useRequest( + async () => Backend.show.v1ShowsDetail(order.show_id), + { + onSuccess: res => { + setShow(res.data) + }, + manual: true + } + ) + const { loading: loadingOrder } = useRequest( + async () => Backend.order.v1OrdersDetail(searchParams.get('order') ?? ''), + { + onSuccess: res => { + setOrder(res.data) + loadShow() + } + } + ) return ( @@ -17,15 +64,21 @@ const OrderConfirmation: React.FC = () => {

We've received your order!

Look out for your confirmation email at{' '} - {userProfile.email} + {user?.email}

🖼️

-
- We can't wait to see you at{' '} - {userProfile.location} for - {userProfile.movie} on{' '} - {userProfile.date}! -
+ {loadingOrder ? null :
Order ID:{order.id}
} + {loadingShow || loadingOrder ? null : ( +
+ We can't wait to see you at{' '} + {show.theater_location} for{' '} + {order.movie_title} on{' '} + {new Date(show.start_time).toLocaleString()}! +
+ )} +

+ View all orders history here! +

diff --git a/src/pages/OrderHistory/index.tsx b/src/pages/OrderHistory/index.tsx index 3ed6cba..69459d0 100644 --- a/src/pages/OrderHistory/index.tsx +++ b/src/pages/OrderHistory/index.tsx @@ -11,7 +11,9 @@ const OrderRow: React.FC<{ }> = ({ key, order }) => ( {order.movie_title} - {order.created_at} + + {new Date(order.created_at).toLocaleString()} + {order.ticket_price} {order.booking_fee_price} {order.promotion_price} diff --git a/src/pages/OrderSummary/index.tsx b/src/pages/OrderSummary/index.tsx index 171de6f..1b9a6d2 100644 --- a/src/pages/OrderSummary/index.tsx +++ b/src/pages/OrderSummary/index.tsx @@ -1,62 +1,95 @@ +import { useRequest } from 'ahooks' +import type { SchemaOrder } from 'client' import PageContainer from 'components/PageContainer' -import { Row, Col } from 'react-bootstrap' +import { useState } from 'react' +import { Card, Row, Col } from 'react-bootstrap' import Button from 'react-bootstrap/Button' -import { Link, useLocation } from 'react-router-dom' +import { Link, useSearchParams } from 'react-router-dom' +import Backend from 'utils/service' const OrderSummary: React.FC = () => { - // TODO: transfer state to order - const location = useLocation() - const state = location.state as unknown[] - const order = { - movie: 'The Bee Movie', - date: '3/3/21', - time: '3:00pm', - TotalPrice: '$21.00', - adultTickets: '12.00', - numAdultTickets: state.length, - childTickets: '$4.50', - numChildTickets: state.length, - seats: 'A1, A2' - } - + const [searchParams] = useSearchParams() + const [order, setOrder] = useState({ + booking_fee_price: 0, + card_id: '', + check_out: false, + created_at: '', + id: '', + movie_title: '', + promotion_id: '', + promotion_price: 0, + sales_tax_price: 0, + show_id: '', + ticket_price: 0, + tickets: [], + total_price: 0, + user_id: '' + }) + const { loading } = useRequest( + async () => Backend.order.v1OrdersDetail(searchParams.get('order') ?? ''), + { + onSuccess: res => { + setOrder(res.data) + } + } + ) return (

Order Summary

- - -

Seat Details

-

- Seat(s): {order.seats} -

-

Ticket(s):

-
    -
  • - Adult Tickets: {order.numAdultTickets}x{' '} - {order.adultTickets} -
  • -
  • - Child Tickets: {order.numChildTickets}x{' '} - {order.childTickets} -
  • -
-

- Total Cost: {order.TotalPrice} -

- - -

Movie Details

-

- Movie: {order.movie} -

-

- Time: {order.date} at {order.time} -

-

- Movie: {order.movie} -

- -
- + {loading ? null : ( + + + Order ID: {order.id} + + Movie: {order.movie_title} + + + + +
Fees:
+
    +
  • + Booking Fee + {order.booking_fee_price.toFixed(2)} +
  • +
  • + Ticket Price + {order.ticket_price.toFixed(2)} +
  • +
  • + Sales Tax + {order.sales_tax_price.toFixed(2)} +
  • +
  • + Promotion Price + {order.promotion_price.toFixed(2)} +
  • +
  • + Total Price + {order.total_price.toFixed(2)} +
  • +
+ + +
Tickets:
+
    + {order.tickets.map((ticket, index) => ( +
  • +
    Seat: {ticket.seat}
    +
    Type: {ticket.type}
    +
  • + ))} +
+ +
+
+
+
+ )} + From a6b74539fbb6bda0074ca6531c9f96a3372f173c Mon Sep 17 00:00:00 2001 From: Boming Zhang Date: Tue, 7 May 2024 15:04:48 -0400 Subject: [PATCH 2/6] fix: remeber on login by default --- src/pages/Login/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 30dff13..b294aa2 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -17,7 +17,7 @@ const LoginForm: React.FC = () => { const defUsername = localStorage.getItem('currentUsername') const [username, setUsername] = useState(defUsername ?? '') const [password, setPassword] = useState('') - const [remember, setRemember] = useState(false) + const [remember, setRemember] = useState(true) const navigate = useNavigate() const { run: login } = useRequest( async () => { @@ -25,8 +25,8 @@ const LoginForm: React.FC = () => { return Backend.auth.v1AuthLoginCreate( { username, - password - // remember + password, + remember }, { redirect_url: `${DOMAIN_HOST}${from}` @@ -115,6 +115,7 @@ const LoginForm: React.FC = () => { { setRemember(!remember) }} From 298303ad3d8786636f1ba096e5407440bd1b6627 Mon Sep 17 00:00:00 2001 From: Boming Zhang Date: Tue, 7 May 2024 15:30:51 -0400 Subject: [PATCH 3/6] feat: add error message for user requests --- src/pages/ChangePassword/index.tsx | 8 +++++++- src/pages/ForgotPassword/index.tsx | 6 ++++++ src/pages/MovieManage/index.tsx | 13 ++++++++++++- src/pages/MovieSeatSelect/index.tsx | 12 ++++++++---- src/pages/MovieSeatSelect/style.module.css | 3 ++- src/pages/OrderSummary/index.tsx | 4 +++- src/pages/PromoManage/index.tsx | 13 ++++++++++++- src/pages/ResetPassword/index.tsx | 8 +++++++- src/pages/UserManage/index.tsx | 8 +++++++- 9 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/pages/ChangePassword/index.tsx b/src/pages/ChangePassword/index.tsx index 104f514..2f200a6 100644 --- a/src/pages/ChangePassword/index.tsx +++ b/src/pages/ChangePassword/index.tsx @@ -1,8 +1,9 @@ import { useRequest } from 'ahooks' +import type { ErrorResponse } from 'client/error' import PageContainer from 'components/PageContainer' import { useAuth } from 'hooks/useAuth' import { useState } from 'react' -import { Col, Row } from 'react-bootstrap' +import { Alert, Col, Row } from 'react-bootstrap' import Button from 'react-bootstrap/Button' import Form from 'react-bootstrap/Form' import { Navigate, useNavigate, useSearchParams } from 'react-router-dom' @@ -15,6 +16,7 @@ const ChangeForm: React.FC = () => { const [username, setUsername] = useState('') const [currentPassword, setCurrentPassword] = useState('') const [newPassword, setNewPassword] = useState('') + const [error, setError] = useState('') const navigate = useNavigate() const { run: changePasssword } = useRequest( async () => { @@ -34,6 +36,9 @@ const ChangeForm: React.FC = () => { manual: true, onSuccess: () => { navigate('/logout') // Navigate to the login page + }, + onError: err => { + setError((err as ErrorResponse).error.msg) } } ) @@ -106,6 +111,7 @@ const ChangeForm: React.FC = () => { + {error ? {error} : null} ) : ( diff --git a/src/pages/ForgotPassword/index.tsx b/src/pages/ForgotPassword/index.tsx index 06ec760..d5a60fa 100644 --- a/src/pages/ForgotPassword/index.tsx +++ b/src/pages/ForgotPassword/index.tsx @@ -1,4 +1,5 @@ import { useRequest } from 'ahooks' +import type { ErrorResponse } from 'client/error' import PageContainer from 'components/PageContainer' import { useState } from 'react' import { Col, Row, Form, Button, Alert } from 'react-bootstrap' @@ -7,12 +8,16 @@ import Backend from 'utils/service' const ForgotPassword: React.FC = () => { const [email, setEmail] = useState('') const [info, setInfo] = useState('') + const [error, setError] = useState('') const { run } = useRequest( async () => Backend.auth.v1AuthForgotpasswordCreate({ email }), { manual: true, onSuccess: () => { setInfo('Done! Check you email!') + }, + onError: err => { + setError((err as ErrorResponse).error.msg) } } ) @@ -50,6 +55,7 @@ const ForgotPassword: React.FC = () => { + {error ? {error} : null} ) diff --git a/src/pages/MovieManage/index.tsx b/src/pages/MovieManage/index.tsx index c5e6b41..e302317 100644 --- a/src/pages/MovieManage/index.tsx +++ b/src/pages/MovieManage/index.tsx @@ -1,10 +1,11 @@ import styles from './style.module.css' import { useRequest } from 'ahooks' import type { SchemaMovie } from 'client' +import type { ErrorResponse } from 'client/error' import PageContainer from 'components/PageContainer' import type React from 'react' import { useState } from 'react' -import { Button, Card, Form, Modal, Table } from 'react-bootstrap' +import { Alert, Button, Card, Form, Modal, Table } from 'react-bootstrap' import Backend from 'utils/service' const MovieRow: React.FC<{ @@ -26,6 +27,7 @@ const MovieRow: React.FC<{ const [synopsis, setSynopsis] = useState(movie.synopsis) const [trailerPicture, setTrailerPicture] = useState(movie.trailer_picture) const [trailerVideo, setTrailerVideo] = useState(movie.trailer_video) + const [error, setError] = useState('') const { run: update } = useRequest( async () => { Backend.movie.v1MoviesUpdate(movie.id, { @@ -47,6 +49,9 @@ const MovieRow: React.FC<{ onSuccess: () => { refresh() handleClose() + }, + onError: err => { + setError((err as ErrorResponse).error.msg) } } ) @@ -167,6 +172,7 @@ const MovieRow: React.FC<{ /> + {error ? {error} : null}