Skip to content

Commit

Permalink
Fix/shop (#379)
Browse files Browse the repository at this point in the history
* fix: purchases

* fix: payment design

* fix: loader style
  • Loading branch information
DevNono authored Sep 13, 2024
1 parent 6266278 commit 1022279
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 53 deletions.
2 changes: 1 addition & 1 deletion src/app/(dashboard)/dashboard/payment/callback/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const Payment = () => {
}, [stripe, clientSecret]);

return (
<div className={styles.callbackLoader}>
<div className={styles.loader}>
<Loader />
</div>
);
Expand Down
153 changes: 133 additions & 20 deletions src/app/(dashboard)/dashboard/payment/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,120 @@
'use client';

import { Button } from '@/components/UI';
import { Button, Loader, Title } from '@/components/UI';
import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
import { StripePaymentElementOptions } from '@stripe/stripe-js';
import { useState } from 'react';
import { useEffect, useState, useMemo } from 'react';
import styles from './style.module.scss';
import Cart from '@/components/dashboard/Cart';
import { useAppDispatch, useAppSelector } from '@/lib/hooks';
import { CartItem, Item, User, UserType } from '@/types';
import { fetchItems } from '@/modules/items';
import { getTicketPrice } from '@/modules/users';
import { fetchCurrentTeam } from '@/modules/team';
import { useSearchParams } from 'next/navigation';

const Payment = () => {
const stripe = useStripe();
const elements = useElements();

const dispatch = useAppDispatch();

const user = useAppSelector((state) => state.login.user) as User;

const [message, setMessage] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const cartStr = useSearchParams().get('cart');
const cart = useMemo(() => (cartStr ? JSON.parse(cartStr) : null), [cartStr]);

const [items, setItems] = useState<Item[] | null>(null);
// The team the player is in
const team = useAppSelector((state) => state.team.team);
// The members of the team are the players and the coaches
const [teamMembers, setTeamMembers] = useState<User[] | null>(null);

// Contains the ticket items for each ticket in the cart. This is an object, keys are userIds and values are the items
const [tickets, setTickets] = useState<
| {
[userId: string]: CartItem;
}
| undefined
>(undefined);

// Fetch items, team and checks if user already have an attendant
useEffect(() => {
if (user.teamId) {
dispatch(fetchCurrentTeam());
} else if (user.type === UserType.spectator) {
// Organizers should not be able to buy tickets if they are not in a team
setTeamMembers([
{
id: user.id,
hasPaid: user.hasPaid,
username: user.username,
age: user.age,
attendant: user.attendant,
type: user.type,
} as User,
]);
} else {
setTeamMembers([]);
}

(async () => {
setItems(await fetchItems());
})();
}, []);

// Initializing teamMembers
useEffect(() => {
if (!team) {
return;
}
setTeamMembers(team.players.concat(team.coaches));
}, [team]);

// Checks if the place of the user is already in the cart
// Checks if the user already have an attendant
// Initializes teamMembersWithoutTicket
// Fills tickets
useEffect(() => {
if (!cart || !teamMembers || !items) return;

(async () => {
// First, we make all the requests
const ticketsArray = (
await Promise.allSettled(
cart.tickets.userIds.map((userId: string) =>
userId === user.id ? items.find((item) => item.id === `ticket-${user.type}`) : getTicketPrice(userId),
),
)
)
.filter((val): val is PromiseFulfilledResult<Item> => val.status === 'fulfilled')
// Then, we only keep the return value of the Promises
.map((result) => result.value)
// And finally, we remove failed Promises
.filter((ticket, i) => {
if (!ticket) {
// toast.error(
// `Une erreur est survenue en cherchant le prix du ticket de l'utilisateur avec l'identifiant ${cart.tickets.userIds[i]}. Si ce problème persiste, contacte le support`,
// );
const newCart = { ...cart };
newCart.tickets.userIds.splice(i, 1);
return false;
}
return true;
});
setTickets(ticketsArray.reduce((prev, curr, i) => ({ ...prev, [cart.tickets.userIds[i]]: curr }), {}));
})();
}, [items, cart, teamMembers]);

if (!items || !teamMembers || !tickets || !cart) {
return (
<div className={styles.loader}>
<Loader />
</div>
);
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
Expand Down Expand Up @@ -46,24 +149,34 @@ const Payment = () => {
} as StripePaymentElementOptions;

return (
<div className={styles.stripePaymentContainer}>
<form id="payment-form" onSubmit={handleSubmit} className={styles.stripeForm}>
<PaymentElement id="payment-element" options={paymentElementOptions} />
<Button primary type="submit" disabled={isLoading || !stripe || !elements}>
<span id="button-text">{isLoading ? <div className="spinner" id="spinner"></div> : 'Payer'}</span>
</Button>
{/* Show any error or success messages */}
{message && <div id="payment-message">{message}</div>}
</form>
{/* <Cart
cart={cart}
tickets={tickets}
items={items}
teamMembers={teamMembers}
onItemRemoved={onRemoveItem}
onTicketRemoved={onRemoveTicket}
onCartReset={onCartReset}
/> */}
<div className={styles.paymentContainer}>
<div className={styles.stripePaymentContainer}>
<form id="payment-form" onSubmit={handleSubmit} className={styles.stripeForm}>
<Title level={2} align="center">
Paiement
</Title>
<PaymentElement id="payment-element" options={paymentElementOptions} />
<Button primary type="submit" disabled={isLoading || !stripe || !elements}>
<span id="button-text">{isLoading ? <div className="spinner" id="spinner"></div> : 'Payer'}</span>
</Button>
{/* Show any error or success messages */}
{message && <div id="payment-message">{message}</div>}
</form>
<div className={styles.bill}>
<Cart
cart={cart}
tickets={tickets!}
items={items!}
teamMembers={teamMembers!}
onItemRemoved={null}
onTicketRemoved={null}
onCartReset={null}
/>
</div>
</div>
<p>
Paiement géré par <a href="https://stripe.com/fr">Stripe Payments Europe, Ltd.</a>
</p>
</div>
);
};
Expand Down
66 changes: 54 additions & 12 deletions src/app/(dashboard)/dashboard/payment/style.module.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,57 @@
.stripeForm {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
width: 100%;
padding: 4rem;
@import '@/variables.scss';

.loader {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}

.callbackLoader {
.paymentContainer {
.stripePaymentContainer {
display: flex;
justify-content: center;
align-items: center;
}
padding: 2rem;

.bill {
display: flex;
flex-direction: column;
justify-content: space-between;
position: sticky;
top: 90px;
background-color: $secondary-background;
padding: 1.25rem;
border-radius: 0.5rem;
width: 40%;
}

.stripeForm {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
width: 60%;
padding: 4rem;
}
}

p {
text-align: center;
}
}

@media screen and (max-width: 900px) {
.paymentContainer {
.stripePaymentContainer {
flex-direction: column;

.stripeForm {
width: 100%;
}

.bill {
width: 100%;
}
}
}
}
8 changes: 4 additions & 4 deletions src/app/(dashboard)/dashboard/purchases/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Purchases = () => {
const carts = useAppSelector((state) =>
state.carts.allCarts.filter(
(cart) =>
cart.transactionState === TransactionState.paid || cart.transactionState === TransactionState.authorization,
cart.transactionState === TransactionState.paid || cart.transactionState === TransactionState.processing,
),
);
const [items, setItems] = useState<Item[] | null>(null);
Expand Down Expand Up @@ -67,16 +67,16 @@ const Purchases = () => {
return (
<div
className={`${styles.cardCart} ${
cart.transactionState === TransactionState.authorization ? styles.authorization : ''
cart.transactionState === TransactionState.processing ? styles.authorization : ''
}`}
key={cart.id}>
<>
<Title level={2} align="center" className={styles.primaryTitle}>
<Title level={2} type={3} align="center" className={styles.primaryTitle}>
Achat #{cart.id}
</Title>
<p>
Date: {date.toLocaleDateString('fr-FR', { year: 'numeric', month: 'numeric', day: 'numeric' })}{' '}
{cart.transactionState === TransactionState.authorization ? '(Paiement en cours de traitement)' : ''}
{cart.transactionState === TransactionState.processing ? '(Paiement en cours de traitement)' : ''}
</p>
<Table columns={columns} dataSource={dataSource} className={styles.cart} />
<p className={styles.cartTotal}>
Expand Down
11 changes: 10 additions & 1 deletion src/app/(dashboard)/dashboard/purchases/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}

.cardCart {
width: 100%;
width: calc(100% - 4rem);
margin: 0 2rem;
background-color: $secondary-background;
padding: 1rem;
Expand All @@ -39,3 +39,12 @@
}
}
}

@media screen and (min-width: 768px) {
.dashboardPurchases {
.cardCart {
width: calc(100% - 2rem);
margin: 0 1rem;
}
}
}
2 changes: 1 addition & 1 deletion src/app/(dashboard)/dashboard/shop/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ const Shop = () => {
setHasRequestedPayment(true);
deleteCart();
const token = await cartPay(cart);
dispatch(setRedirect(`/dashboard/payment?stripeToken=${token}`));
dispatch(setRedirect(`/dashboard/payment?stripeToken=${token}&cart=${JSON.stringify(cart)}`));
};

// Hide the places section if user can't buy any places
Expand Down
5 changes: 3 additions & 2 deletions src/components/UI/Title.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ $radius: 8px;

&.title-3 {
font-weight: 600;
font-size: 1.5rem;
font-size: 2rem;
font-family: $kanit;
}

&.title-4 {
font-weight: 600;
font-size: 1.25rem;
font-size: 1.75rem;
}

.textDivider {
Expand Down
24 changes: 14 additions & 10 deletions src/components/dashboard/Cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,24 @@ const Cart = ({
/** The team members */
teamMembers: User[];
/** The function to call when an item is removed */
onItemRemoved: (itemId: string) => void;
onItemRemoved: ((itemId: string) => void) | null;
/** The function to call when a ticket is removed */
onTicketRemoved: (user: User | undefined, i: number | undefined) => void;
onTicketRemoved: ((user: User | undefined, i: number | undefined) => void) | null;
/** The function to call when the cart is reset */
onCartReset: () => void;
onCartReset: (() => void) | null;
}) => {
const attendantTicket = items.find((ticket) => ticket.id === 'ticket-attendant');
return (
<div className={styles.cart}>
<div className={styles.cartHeader}>
<h2 className={styles.mainTitle}>Panier</h2>
<Button primary outline onClick={onCartReset}>
Vider le panier
</Button>
<h2 className={styles.mainTitle}>
{!onItemRemoved && !onTicketRemoved && !onCartReset ? 'Recapitulatif Panier' : 'Panier'}
</h2>
{onCartReset && (
<Button primary outline onClick={onCartReset}>
Vider le panier
</Button>
)}
</div>
{Object.entries(tickets).map(([userId, ticket]) => {
const i = cart.tickets.userIds.findIndex((id: string) => id === userId);
Expand All @@ -47,7 +51,7 @@ const Cart = ({
quantity={1}
unitPrice={ticket.price}
reducedUnitPrice={ticket.reducedPrice}
onRemove={() => onTicketRemoved(user!, i)}
onRemove={onTicketRemoved ? () => onTicketRemoved(user!, i) : null}
/>
);
})}
Expand All @@ -57,7 +61,7 @@ const Cart = ({
itemName={`${attendantTicket!.name} (${cart.tickets.attendant.firstname} ${cart.tickets.attendant.lastname})`}
quantity={1}
unitPrice={attendantTicket!.price}
onRemove={() => onTicketRemoved(undefined, undefined)}
onRemove={onTicketRemoved ? () => onTicketRemoved(undefined, undefined) : null}
/>
)}
{cart.supplements.map((supplement) => {
Expand All @@ -69,7 +73,7 @@ const Cart = ({
itemName={item!.name + (item!.attribute ? ` - Taille ${item!.attribute.toUpperCase()}` : '')}
quantity={supplement.quantity}
unitPrice={item!.price}
onRemove={() => onItemRemoved(item!.id)}
onRemove={onItemRemoved ? () => onItemRemoved(item!.id) : null}
/>
);
})}
Expand Down
Loading

0 comments on commit 1022279

Please sign in to comment.