Skip to content

Commit

Permalink
✨ Add product details page and navigation to it
Browse files Browse the repository at this point in the history
  • Loading branch information
zubko committed Jun 4, 2022
1 parent 1d9465f commit 0e5c44b
Show file tree
Hide file tree
Showing 36 changed files with 643 additions and 132 deletions.
12 changes: 11 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
# List of next steps

- add switch to product with re-fetching product data in background
- add fetching one product for details
- disable remove button if there is no item in the cart
- change title of add buttons if there are such items in the cart
- display cart
- checkout
- decide to use `getSmth` or just `smth`

# Later

- add E2E tests for existing features
- fix warning in storybook about React 18 (Storybook itself still uses React 17, so it will take time to remove the warning completely)
- show update status on the page without shifting the content down
- host FE/BE somewhere
- keep the currency knowledge somewhere in a formatter

# Maybe

- add date of last cart update and reset the cart on load if too much time have passed
- share data types between BE & FE
- data migration or reset on the client (when stored entities aren't correct anymore)
- entity description data with formatting including some React components
18 changes: 18 additions & 0 deletions backend/src/products/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,60 @@ export const products: Product[] = [
id: "1",
category: "pizza",
name: "Margarita",
slug: "margarita",
price: 10,
image: "",
description:
"Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, “and what is the use of a book,” thought Alice “without pictures or conversations?”",
},
{
id: "2",
category: "pizza",
name: "Pepperoni",
slug: "pepperoni",
price: 15,
image: "",
description:
"So she was considering in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her.",
},
{
id: "3",
category: "pizza",
name: "Salami",
slug: "salami",
price: 20,
image: "",
description:
"There was nothing so very remarkable in that; nor did Alice think it so very much out of the way to hear the Rabbit say to itself, “Oh dear! Oh dear! I shall be late!” (when she thought it over afterwards, it occurred to her that she ought to have wondered at this, but at the time it all seemed quite natural);",
},
{
id: "4",
category: "drink",
name: "Cola",
slug: "cola",
price: 5,
image: "",
description:
"but when the Rabbit actually took a watch out of its waistcoat-pocket, and looked at it, and then hurried on, Alice started to her feet, for it flashed across her mind that she had never before seen a rabbit with either a waistcoat-pocket, or a watch to take out of it, and burning with curiosity, she ran across the field after it, and fortunately was just in time to see it pop down a large rabbit-hole under the hedge.",
},
{
id: "5",
category: "drink",
name: "Juice",
slug: "juice",
price: 7,
image: "",
description:
"In another moment down went Alice after it, never once considering how in the world she was to get out again.",
},
{
id: "6",
category: "side",
name: "Sauce",
slug: "sauce",
price: 2,
image: "",
description:
"The rabbit-hole went straight on like a tunnel for some way, and then dipped suddenly down, so suddenly that Alice had not a moment to think about stopping herself before she found herself falling down a very deep well.",
},
];
2 changes: 2 additions & 0 deletions backend/src/products/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ export type ProductCategory = "pizza" | "drink" | "side";

export interface Product {
id: string;
slug: string;
category: ProductCategory;
name: string;
price: number;
image: string;
description: string;
}
2 changes: 1 addition & 1 deletion frontend/docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
- All stories must use mocked props (no network calls etc)
- View models from presenters must be unit tested at least with snapshots
- Wireframes must not do any layout or styling, only composition and connecting view models to views
- Views must not do any business or formatting logic, only layout, styling and 1-1 rendering of data from view models
- Views must not do any business or formatting logic, only layout, styling and 1-1 rendering of data from view models, ideally they should not have any conditional operators (so there will be no need to unit test them)
- Presenters are doing formatting and preparing the data to be displayed
- Business logic must be placed to use cases
- The choice of storage must be abstracted by repository
Expand Down
6 changes: 5 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@
"@types/node": "^17.0.36",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/react-kawaii": "^0.17.0",
"@types/styled-components": "^5.1.25",
"axios": "^0.27.2",
"axios-observable": "^1.4.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-kawaii": "^0.17.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"rxjs": "^7.5.5",
"storybook-addon-pseudo-states": "^1.14.0",
Expand All @@ -49,7 +52,8 @@
"lint": "yarn eslint .",
"eject": "craco eject",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public"
"build-storybook": "build-storybook -s public",
"clear-cache": "rm -rf node_modules/.cache"
},
"eslintConfig": {
"extends": [
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/core/debug/logger-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FC, PropsWithChildren } from "react";
import { DEV_ENV } from "../env";

export const LoggerComponent: FC<PropsWithChildren<Record<string, unknown>>> = (
props
) => {
if (DEV_ENV) {
console.log("LoggerComponent:", props);
}
return <>{props.children ?? null}</>;
};
2 changes: 2 additions & 0 deletions frontend/src/core/entities/Product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ export type ProductCategory = "pizza" | "drink" | "side";

export interface Product {
id: string;
slug: string;
category: ProductCategory;
name: string;
price: number;
image: string;
description: string;
}
2 changes: 2 additions & 0 deletions frontend/src/core/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DEV_ENV: boolean =
!process.env.NODE_ENV || process.env.NODE_ENV === "development";
4 changes: 4 additions & 0 deletions frontend/src/core/repository/products.repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createStore } from "@ngneat/elf";
import {
selectEntityByPredicate,
selectManyByPredicate,
setEntities,
withEntities,
Expand Down Expand Up @@ -42,6 +43,9 @@ export const productsRequestStatus$ = store.pipe(
export const productsByCategory$ = (category: ProductCategory) =>
store.pipe(selectManyByPredicate((product) => product.category === category));

export const productWithSlug$ = (slug: string) =>
store.pipe(selectEntityByPredicate((p) => p.slug === slug));

export const setProducts = (products: Product[]) =>
store.update(
setEntities(products),
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/core/test/mock-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FC, PropsWithChildren } from "react";

export const MockLink: FC<PropsWithChildren<{}>> = ({ children }) => (
<a href="#">{children}</a>
);
27 changes: 27 additions & 0 deletions frontend/src/core/ui/components/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { HTMLProps } from "react";
import { Button as Component } from "./button";

export default {
component: Component,
} as ComponentMeta<typeof Component>;

const Template: ComponentStory<typeof Component> = (args) => (
<Component {...args} />
);

const Props: HTMLProps<HTMLButtonElement> = {
onClick: () => alert("CLICK"),
children: "Title",
};

export const Normal = Template.bind({});
Normal.args = {
...Props,
};

export const Hover = Template.bind({});
Hover.parameters = { pseudo: { hover: true } };
Hover.args = {
...Props,
};
19 changes: 19 additions & 0 deletions frontend/src/core/ui/components/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { color, gradient } from "@app/core/ui/theme/api";
import styled from "styled-components";

export const Button = styled.button`
padding: 8px 12px;
background: ${gradient("buttonBg")};
margin: 8px;
border: 0;
font-size: 16px;
font-weight: 600;
color: black;
border: 2px solid black;
cursor: pointer;
&:hover {
border-color: ${color("hoverButtonText")};
color: ${color("hoverButtonText")};
background: ${gradient("buttonBgHover")};
}
`;
5 changes: 4 additions & 1 deletion frontend/src/core/ui/theme/theme.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
export const Theme = {
colors: {
text: "black",
menuBg1: "#2ff75b",
menuBg2: "#26c94a",
pageBg: "white",
hover: "#ffff00",
hoverText: "#26c94a",
hoverButtonText: "#ffff00",
logo: "#26c94a",
logoNeon: "#00ff00",
notFoundPizza: "#f8ff00",
},
gradients: {
buttonBg: "linear-gradient(180deg, #f8ff00 0%, #3ad59f 100%)",
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/features/cart/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export { CartStatus } from "./components/cart-status/cart-status.view";
export { CartStatusWireframe } from "./components/cart-status/cart-status.wireframe";
export { addProductToCart } from "./use-cases/add-product-to-cart";
export {
addProductToCart,
removeProductFromCart,
} from "./use-cases/update-product-in-cart";
22 changes: 22 additions & 0 deletions frontend/src/features/cart/repository/cart.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,25 @@ export const upsertProductIdToCart = (productId: Product["id"]) => {

export const setCartItems = (items: CartItem[]) =>
store.update(setEntities(items));

export const updateOrRemoveProductIdFromCart = (productId: Product["id"]) => {
store.update((state) => {
const entity = state.entities[productId];
if (!entity) {
return state;
}
if (entity.count > 1) {
const newEntity: typeof entity = { ...entity, count: entity.count - 1 };
return {
...state,
entities: { ...state.entities, [entity.productId]: newEntity },
};
} else {
const newEntities = { ...state.entities };
Reflect.deleteProperty(newEntities, entity.productId);
const newIds = [...state.ids];
newIds.splice(newIds.indexOf(entity.productId), 1);
return { ...state, entities: newEntities, ids: newIds };
}
});
};
6 changes: 0 additions & 6 deletions frontend/src/features/cart/use-cases/add-product-to-cart.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import { Product } from "@app/core/entities/Product";
import { resetAllStores } from "@app/core/repository/repository";
import { getFirstValue } from "@app/core/test/observable-test-utils";
import { cartItems$, setCartItems } from "../repository/cart.repository";
import { addProductToCart } from "./add-product-to-cart";
import { addProductToCart } from "./update-product-in-cart";

const MockProduct: Product = {
id: "1",
category: "drink",
name: "product",
slug: "product",
price: 10,
image: "",
description: "description",
};

describe("Add Products To Cart", () => {
describe("Add products to cart", () => {
afterEach(() => {
resetAllStores();
});
Expand Down Expand Up @@ -42,3 +44,34 @@ describe("Add Products To Cart", () => {
`);
});
});

describe("Removes products from cart", () => {
afterEach(() => {
resetAllStores();
});

it("removes an item if the count is 1", () => {
addProductToCart(MockProduct);
expect(getFirstValue(cartItems$)).toMatchInlineSnapshot(`
Array [
Object {
"count": 1,
"productId": "1",
},
]
`);
});

it("decrements the count if the item is present", () => {
setCartItems([{ productId: "1", count: 5 }]);
addProductToCart(MockProduct);
expect(getFirstValue(cartItems$)).toMatchInlineSnapshot(`
Array [
Object {
"count": 6,
"productId": "1",
},
]
`);
});
});
13 changes: 13 additions & 0 deletions frontend/src/features/cart/use-cases/update-product-in-cart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Product } from "@app/core/entities/Product";
import {
updateOrRemoveProductIdFromCart,
upsertProductIdToCart,
} from "../repository/cart.repository";

export const addProductToCart = (product: Product) => {
upsertProductIdToCart(product.id);
};

export const removeProductFromCart = (product: Product) => {
updateOrRemoveProductIdFromCart(product.id);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { CartStatusWireframe } from "@app/features/cart";
import { RestaurantMenuWireframe } from "@app/features/restaurant-menu";
import {
ProductDetailsWireframe,
RestaurantMenuWireframe,
} from "@app/features/restaurant-menu";
import { FC } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { NotFound } from "../not-found/not-found.view";
import { TopBar } from "../top-bar/top-bar.view";

export const NavigationWireframe: FC<{}> = () => (
<>
<TopBar CartStatus={CartStatusWireframe} />
<RestaurantMenuWireframe />
<BrowserRouter>
<Routes>
<Route index element={<RestaurantMenuWireframe />} />
<Route path="menu">
<Route index element={<RestaurantMenuWireframe />} />
<Route path=":productSlug" element={<ProductDetailsWireframe />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { NotFound as Component } from "./not-found.view";

export default {
component: Component,
} as ComponentMeta<typeof Component>;

const Template: ComponentStory<typeof Component> = (args) => (
<Component {...args} />
);

export const NotFound = Template.bind({});
Loading

0 comments on commit 0e5c44b

Please sign in to comment.