Skip to content

Commit

Permalink
Add basket UI stuff (and a bunch of scaffolding) (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkachel authored Dec 17, 2024
1 parent 795cb6f commit b7e71b0
Show file tree
Hide file tree
Showing 27 changed files with 3,328 additions and 453 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ First, ensure that you have the [Unified Ecommerce Bakend](https://github.com/mi

Environment variables are described in detail in `env/env.defaults.public`; all env vars should have functional defaults. However, a few dependencies to note:

- In the Unified Ecommerce backend, `MITOL_UE_PAYMENT_BASKET_ROOT` and `MITOL_UE_PAYMENT_BASKET_CHOOSER` should point to the this repo's frontend. (e.g., `https://uefe.odl.local:8072`)
- In the Unified Ecommerce backend, `MITOL_UE_PAYMENT_BASKET_ROOT` and `MITOL_UE_PAYMENT_BASKET_CHOOSER` should point to the this repo's frontend. (e.g., `https://ue.odl.local:8072`)

### Run the app

#### With Docker

With `docker compose up`, you should be up and running. Visit the application at http://uefe.odl.local:8072
With `docker compose up`, you should be up and running. Visit the application at http://ue.odl.local:8072

#### Without Docker

Expand All @@ -25,8 +25,10 @@ You can run the app outside of docker. This may be faster and more convenient. T
1. Some way to load environment variables. [direnv](https://direnv.net/) is a great tool for this; a sample `.envrc` file is committed in the repo.
2. A NodeJS runtime; [`nvm`](https://github.com/nvm-sh/nvm) is a simple tool for managing NodeJS versions.

With that done, `yarn start`, `yarn install`, and visit http://uefe.odl.local:8072
With that done, `yarn start`, `yarn install`, and visit http://ue.odl.local:8072

## Accessing the Application

The Unified Ecommerce backend uses same-site cookies for authentication. Therefore, the frontend client must run on the "same site" as the backend. Therefore, if the backend runs on `ue.odl.local:8073`, you **must** access the frontend on at a hostname such as `uefe.odl.local` (or `*.odl.local`), _not_ `localhost`.
The Unified Ecommerce backend uses same-site cookies for authentication. Therefore, the frontend client must run on the "same site" as the backend. Therefore, if the backend runs on `ue.odl.local:8073`, you **must** access the frontend on at a hostname such as `ue.odl.local` (or `*.odl.local`), _not_ `localhost`.

To prevent CORS and CSRF errors, set the frontend and backend URLs to be either the same hostname (`ue.odl.local`) or set the backend to be a subdomain of the frontend (`api.ue.odl.local` and `ue.odl.local` - this is closer to the actual deployment).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@faker-js/faker": "^9.0.0",
"@mitodl/smoot-design": "^1.0.1",
"@mitodl/smoot-design": "^1.1.1",
"@mui/material": "^6.1.8",
"@mui/material-nextjs": "^6.1.8",
"@remixicon/react": "^4.2.0",
Expand Down
25 changes: 0 additions & 25 deletions src/app/cart/page.tsx

This file was deleted.

9 changes: 0 additions & 9 deletions src/app/checkout/page.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import Header from "@/page-components/Header/Header";
import Header from "@/components/Header/Header";
import EnsureSession from "@/page-components/EnsureSession/EnsureSession";
import "./global.css";
import Providers from "./providers";
Expand Down
157 changes: 154 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,159 @@
"use client";
import React from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useMetaIntegratedSystemsList } from "@/services/ecommerce/meta/hooks";
import { styled } from "@mitodl/smoot-design";
import { Typography } from "@mui/material";
import Container from "@mui/material/Container";
import { getCurrentSystem } from "@/utils/system";
import { Card } from "@/components/Card/Card";
import CartItem from "@/page-components/CartItem/CartItem";
import CartSummary from "@/page-components/CartSummary/CartSummary";
import StyledCard from "@/components/Card/StyledCard";
import { UseQueryResult } from "@tanstack/react-query";
import type {
PaginatedBasketWithProductList,
BasketWithProduct,
} from "@/services/ecommerce/generated/v0";

import {
usePaymentsBasketList,
usePaymentsBasketRetrieve,
} from "@/services/ecommerce/payments/hooks";
import {
BasketItemWithProduct,
IntegratedSystem,
} from "@/services/ecommerce/generated/v0";

type CartProps = {
system: string;
};

type CartBodyProps = {
systemId: number;
};

const SelectSystemContainer = styled.div`
margin: 16px 0;
`;

const SelectSystem: React.FC = () => {
const systems = useMetaIntegratedSystemsList();
const router = useRouter();

const hndSystemChange = (ev: React.ChangeEvent<HTMLSelectElement>) => {
router.push(`/?system=${ev.target.value}`);
};

return (
<SelectSystemContainer>
{systems.isFetched && systems.data ? (
<>
<label htmlFor="system">Select a system:</label>
<select name="system" id="system" onChange={hndSystemChange}>
<option value="">Select a system</option>
{systems.data.results.map((system) => (
<option key={system.id} value={system.slug || ""}>
{system.name}
</option>
))}
</select>
</>
) : (
<p>Loading systems...</p>
)}
</SelectSystemContainer>
);
};

const CartContainer = styled.div``;

const CartBodyContainer = styled.div`
width: 100%;
display: flex;
gap: 48px;
align-items: start;
`;

const CartHeader = styled.div`
width: 100%;
flex-grow: 1;
margin-bottom: 16px;
`;

const CartItemsContainer = styled.div`
width: auto;
flex-grow: 1;
`;

const CartBody: React.FC<CartBodyProps> = ({ systemId }) => {
const basket = usePaymentsBasketList({
integrated_system: systemId,
}) as UseQueryResult<PaginatedBasketWithProductList>;
const basketDetails = usePaymentsBasketRetrieve(
basket.data?.results[0]?.id || 0,
{ enabled: !!basket.data?.count },
) as UseQueryResult<BasketWithProduct>;

return basketDetails.isFetched &&
basketDetails?.data?.basket_items &&
basketDetails.data.basket_items.length > 0 ? (
<CartBodyContainer>
<CartItemsContainer>
{basketDetails.data.basket_items.map((item: BasketItemWithProduct) => (
<CartItem item={item} key={`ue-basket-item-${item.id}`} />
))}
</CartItemsContainer>
<CartSummary cartId={basketDetails.data.id} />
</CartBodyContainer>
) : (
<CartBodyContainer>
<StyledCard>
<Card.Content>
<p>Your cart is empty.</p>
</Card.Content>
</StyledCard>
</CartBodyContainer>
);
};

const Cart: React.FC<CartProps> = ({ system }) => {
const systems = useMetaIntegratedSystemsList();
const selectedSystem = systems.data?.results.find(
(integratedSystem: IntegratedSystem) => integratedSystem.slug === system,
);

return (
selectedSystem && (
<CartContainer>
<CartHeader>
<Typography variant="h3">
You are about to purchase the following:
</Typography>
</CartHeader>
{selectedSystem && <CartBody systemId={selectedSystem.id} />}
</CartContainer>
)
);
};

const Home = () => {
const searchParams = useSearchParams();
const specifiedSystem = getCurrentSystem(searchParams);

return (
<div>
<h1>Home</h1>
</div>
<Container>
{specifiedSystem === "" && (
<StyledCard>
<Card.Content>
We can't determine what system you're trying to access. Please
choose a system to continue.
<SelectSystem />
</Card.Content>
</StyledCard>
)}
{specifiedSystem !== "" && <Cart system={specifiedSystem} />}
</Container>
);
};

Expand Down
31 changes: 31 additions & 0 deletions src/components/Card/Card.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { render } from "@testing-library/react";
import { Card } from "./Card";

describe("Card", () => {
test("has class MitCard-root on root element", () => {
const { container } = render(
<Card className="Foo">
<Card.Title>Title</Card.Title>
<Card.Image src="https://via.placeholder.com/150" alt="placeholder" />
<Card.Info>Info</Card.Info>
<Card.Footer>Footer</Card.Footer>
<Card.Actions>Actions</Card.Actions>
</Card>,
);
const card = container.firstChild as HTMLElement;
const title = card.querySelector(".MitCard-title");
const image = card.querySelector(".MitCard-image");
const info = card.querySelector(".MitCard-info");
const footer = card.querySelector(".MitCard-footer");
const actions = card.querySelector(".MitCard-actions");

expect(card).toHaveClass("MitCard-root");
expect(card).toHaveClass("Foo");
expect(title).toHaveTextContent("Title");
expect(image).toHaveAttribute("src", "https://via.placeholder.com/150");
expect(image).toHaveAttribute("alt", "placeholder");
expect(info).toHaveTextContent("Info");
expect(footer).toHaveTextContent("Footer");
expect(actions).toHaveTextContent("Actions");
});
});
Loading

0 comments on commit b7e71b0

Please sign in to comment.