React keywords: action({…}), configureStore({…}), createBrowserRouter([…]), Link, loader(), Outlet, useActionData(), useDispatch(), useEffect, useFetcher()useLoaderData(), useNavigation(), useRouteError(), useSelector(…), useState, <RouterProvider… />, createAsyncThunk(…), createSlice({…}),

Project overview:

Project shows: use ‘npm create vite@latest‘, and not ‘npm create-react-app’. Vite has a simple and intuitive configuration system that allows you to get up and running quickly, without spending hours configuring your build process . Create React App, on the other hand, has a more complex configuration system that can be overwhelming for developers who are new to web development. Vite supports many popular front-end frameworks, such as ReactVue, and Angular, making it a great choice for developers who work with multiple frameworks. Create React App, as its name suggests, is specifically designed for React projects.

Routes are now defined using:

import { RouterProvider, createBrowserRouter } from ‘react-router-dom’;

const router = createBrowserRouter([
  {
    element: <AppLayout />,
    errorElement: <Error />,

   children: [

      {
        path: ‘/’,
        element: <Home />,
      },
      {
        path: ‘/menu’,
        element: <Menu />,
        loader: menuLoader,
        errorElement: <Error />,
      },
      {
        path:,
      },
    ],
  },
]);
function App() {
  return <RouterProvider router={router} />;
}
export default App;

instead of using:

        import { BrowserRouter, Routes, Route, … } from “react-router-dom”;
function App() {
  return (
     …
        <BrowserRouter>
          …
            <Routes>
              <Route index element={<Homepage />} />
              <Route path=“product” element={<Product />} />
              <Route path=“pricing” element={<Pricing />} />
              <Route path=“login” element={<Login />} />
              <Route path=“app”
                element={
                  <ProtectedRoute>
                    <AppLayout />
                  </ProtectedRoute>
                }
              >
                <Route index element={<Navigate replace to=“cities” />} />
                <Route path=“cities” element={<CityList />} />
                <Route path=“cities/:id” element={<City />} />
                <Route path=“countries” element={<CountryList />} />
                <Route path=“form” element={<Form />} />
              </Route>
              <Route path=“*” element={<PageNotFound />} />
            </Routes>
          </Suspense>
        </BrowserRouter>
   …
   }
  export default App;

Code:

import { RouterProvider, createBrowserRouter } from ‘react-router-dom’;

import Home from ‘./ui/Home’;
import Error from ‘./ui/Error’;
import Menu, { loader as menuLoader } from ‘./features/menu/Menu’;
import Cart from ‘./features/cart/Cart’;
import CreateOrder, {
  action as createOrderAction,
} from ‘./features/order/CreateOrder’;
import Order, { loader as orderLoader } from ‘./features/order/Order’;
import { action as updateOrderAction } from ‘./features/order/UpdateOrder’;

import AppLayout from ‘./ui/AppLayout’;

const router = createBrowserRouter([
  {
    element: <AppLayout />,
    errorElement: <Error />,

    children: [
      {
        path: ‘/’,
        element: <Home />,
      },
      {
        path: ‘/menu’,
        element: <Menu />,
        loader: menuLoader,
        errorElement: <Error />,
      },
      { path: ‘/cart’, element: <Cart /> },
      {
        path: ‘/order/new’,
        element: <CreateOrder />,
        action: createOrderAction,
      },
      {
        path: ‘/order/:orderId’,
        element: <Order />,
        loader: orderLoader,
        errorElement: <Error />,
        action: updateOrderAction,
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

export default App;
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .input {
    @apply rounded-full border border-stone-200 px-4 py-2 text-sm transition-all duration-300 placeholder:text-stone-400 focus:outline-none focus:ring focus:ring-yellow-400 md:px-6 md:py-3;
  }

  /* https://dev.to/afif/i-made-100-css-loaders-for-your-next-project-4eje */
  .loader {
    width: 45px;
    aspect-ratio: 0.75;
    –c: no-repeat linear-gradient(theme(colors.stone.800) 0 0);
    background: var(–c) 0% 50%, var(–c) 50% 50%, var(–c) 100% 50%;
    background-size: 20% 50%;
    animation: loading 1s infinite linear;
  }

  @keyframes loading {
    20% {
      background-position: 0% 0%, 50% 50%, 100% 50%;
    }
    40% {
      background-position: 0% 100%, 50% 0%, 100% 50%;
    }
    60% {
      background-position: 0% 50%, 50% 100%, 100% 0%;
    }
    80% {
      background-position: 0% 50%, 50% 50%, 100% 100%;
    }
  }
}
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import App from ‘./App’;
import ‘./index.css’;
import { Provider } from ‘react-redux’;
import store from ‘./store’;

ReactDOM.createRoot(document.getElementById(‘root’)).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
import { configureStore } from ‘@reduxjs/toolkit’;
import userReducer from ‘./features/user/userSlice’;
import cartReducer from ‘./features/cart/cartSlice’;

const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
  },
});

export default store;
import LinkButton from ‘../../ui/LinkButton’;
import Button from ‘../../ui/Button’;
import CartItem from ‘./CartItem’;
import EmptyCart from ‘./EmptyCart’;
import { useDispatch, useSelector } from ‘react-redux’;
import { clearCart, getCart } from ‘./cartSlice’;

 

function Cart() {
  const username = useSelector((state) => state.user.username);
  const cart = useSelector(getCart);
  const dispatch = useDispatch();

 

  if (!cart.length) return <EmptyCart />;

 

  return (
    <div className=“px-4 py-3”>
      <LinkButton to=“/menu”>&larr; Back to menu</LinkButton>

 

      <h2 className=“mt-7 text-xl font-semibold”>Your cart, {username}</h2>

 

      <ul className=“mt-3 divide-y divide-stone-200 border-b”>
        {cart.map((item) => (
          <CartItem item={item} key={item.pizzaId} />
        ))}
      </ul>

 

      <div className=“mt-6 space-x-2”>
        <Button to=“/order/new” type=“primary”>
          Order pizzas
        </Button>

 

        <Button type=“secondary” onClick={() => dispatch(clearCart())}>
          Clear cart
        </Button>
      </div>
    </div>
  );
}

 

export default Cart;
import { useSelector } from ‘react-redux’;
import { formatCurrency } from ‘../../utils/helpers’;
import DeleteItem from ‘./DeleteItem’;
import UpdateItemQuantity from ‘./UpdateItemQuantity’;
import { getCurrentQuantityById } from ‘./cartSlice’;

function CartItem({ item }) {
  const { pizzaId, name, quantity, totalPrice } = item;

  const currentQuantity = useSelector(getCurrentQuantityById(pizzaId));

  return (
    <li className=“py-3 sm:flex sm:items-center sm:justify-between”>
      <p className=“mb-1 sm:mb-0”>
        {quantity}&times; {name}
      </p>
      <div className=“flex items-center justify-between sm:gap-6”>
        <p className=“text-sm font-bold”>{formatCurrency(totalPrice)}</p>

        <UpdateItemQuantity
          pizzaId={pizzaId}
          currentQuantity={currentQuantity}
        />
        <DeleteItem pizzaId={pizzaId} />
      </div>
    </li>
  );
}

export default CartItem;
import { useSelector } from ‘react-redux’;
import { Link } from ‘react-router-dom’;
import { getTotalCartPrice, getTotalCartQuantity } from ‘./cartSlice’;
import { formatCurrency } from ‘../../utils/helpers’;

function CartOverview() {
  const totalCartQuantity = useSelector(getTotalCartQuantity);
  const totalCartPrice = useSelector(getTotalCartPrice);

  if (!totalCartQuantity) return null;

  return (
    <div className=“flex items-center justify-between bg-stone-800 px-4 py-4 text-sm uppercase text-stone-200 sm:px-6 md:text-base”>
      <p className=“space-x-4 font-semibold text-stone-300 sm:space-x-6”>
        <span>{totalCartQuantity} pizzas</span>
        <span>{formatCurrency(totalCartPrice)}</span>
      </p>
      <Link to=“/cart”>Open cart &rarr;</Link>
    </div>
  );
}

export default CartOverview;
import { createSlice } from ‘@reduxjs/toolkit’;

const initialState = {
  cart: [],

  // cart: [
  //   {
  //     pizzaId: 12,
  //     name: ‘Mediterranean’,
  //     quantity: 2,
  //     unitPrice: 16,
  //     totalPrice: 32,
  //   },
  // ],
};

const cartSlice = createSlice({
  name: ‘cart’,
  initialState,
  reducers: {
    addItem(state, action) {
      // payload = newItem
      state.cart.push(action.payload);
    },
    deleteItem(state, action) {
      // payload = pizzaId
      state.cart = state.cart.filter((item) => item.pizzaId !== action.payload);
    },
    increaseItemQuantity(state, action) {
      // payload = pizzaId
      const item = state.cart.find((item) => item.pizzaId === action.payload);

      item.quantity++;
      item.totalPrice = item.quantity * item.unitPrice;
    },
    decreaseItemQuantity(state, action) {
      // payload = pizzaId
      const item = state.cart.find((item) => item.pizzaId === action.payload);

      item.quantity;
      item.totalPrice = item.quantity * item.unitPrice;

      if (item.quantity === 0) cartSlice.caseReducers.deleteItem(state, action);
    },
    clearCart(state) {
      state.cart = [];
    },
  },
});

export const {
  addItem,
  deleteItem,
  increaseItemQuantity,
  decreaseItemQuantity,
  clearCart,
} = cartSlice.actions;

export default cartSlice.reducer;

export const getCart = (state) => state.cart.cart;

export const getTotalCartQuantity = (state) =>
  state.cart.cart.reduce((sum, item) => sum + item.quantity, 0);

export const getTotalCartPrice = (state) =>
  state.cart.cart.reduce((sum, item) => sum + item.totalPrice, 0);

export const getCurrentQuantityById = (id) => (state) =>
  state.cart.cart.find((item) => item.pizzaId === id)?.quantity ?? 0;
import { useDispatch } from ‘react-redux’;
import Button from ‘../../ui/Button’;
import { deleteItem } from ‘./cartSlice’;

function DeleteItem({ pizzaId }) {
  const dispatch = useDispatch();

  return (
    <Button type=“small” onClick={() => dispatch(deleteItem(pizzaId))}>
      Delete
    </Button>
  );
}

export default DeleteItem;
import LinkButton from ‘../../ui/LinkButton’;

function EmptyCart() {
  return (
    <div className=“px-4 py-3”>
      <LinkButton to=“/menu”>&larr; Back to menu</LinkButton>

      <p className=“mt-7 font-semibold”>
        Your cart is still empty. Start adding some pizzas 🙂
      </p>
    </div>
  );
}

export default EmptyCart;
import { useDispatch } from ‘react-redux’;
import Button from ‘../../ui/Button’;
import { decreaseItemQuantity, increaseItemQuantity } from ‘./cartSlice’;

function UpdateItemQuantity({ pizzaId, currentQuantity }) {
  const dispatch = useDispatch();

  return (
    <div className=“flex items-center gap-2 md:gap-3”>
      <Button
        type=“round”
        onClick={() => dispatch(decreaseItemQuantity(pizzaId))}
      >
        –
      </Button>
      <span className=“text-sm font-medium”>{currentQuantity}</span>
      <Button
        type=“round”
        onClick={() => dispatch(increaseItemQuantity(pizzaId))}
      >
        +
      </Button>
    </div>
  );
}

export default UpdateItemQuantity;
import { useLoaderData } from ‘react-router-dom’;
import { getMenu } from ‘../../services/apiRestaurant’;
import MenuItem from ‘./MenuItem’;

function Menu() {
  const menu = useLoaderData();

  return (
    <ul className=“divide-y divide-stone-200 px-2”>
      {menu.map((pizza) => (
        <MenuItem pizza={pizza} key={pizza.id} />
      ))}
    </ul>
  );
}

export async function loader() {
  const menu = await getMenu();
  return menu;
}

export default Menu;
import { useDispatch, useSelector } from ‘react-redux’;
import Button from ‘../../ui/Button’;
import DeleteItem from ‘../cart/DeleteItem’;
import UpdateItemQuantity from ‘../cart/UpdateItemQuantity’;
import { formatCurrency } from ‘../../utils/helpers’;
import { addItem, getCurrentQuantityById } from ‘../cart/cartSlice’;

function MenuItem({ pizza }) {
  const dispatch = useDispatch();

  const { id, name, unitPrice, ingredients, soldOut, imageUrl } = pizza;

  const currentQuantity = useSelector(getCurrentQuantityById(id));
  const isInCart = currentQuantity > 0;

  function handleAddToCart() {
    const newItem = {
      pizzaId: id,
      name,
      quantity: 1,
      unitPrice,
      totalPrice: unitPrice * 1,
    };
    dispatch(addItem(newItem));
  }

  return (
    <li className=“flex gap-4 py-2”>
      <img
        src={imageUrl}
        alt={name}
        className={`h-24 ${soldOut ? ‘opacity-70 grayscale’ : }`}
      />
      <div className=“flex grow flex-col pt-0.5”>
        <p className=“font-medium”>{name}</p>
        <p className=“text-sm capitalize italic text-stone-500”>
          {ingredients.join(‘, ‘)}
        </p>
        <div className=“mt-auto flex items-center justify-between”>
          {!soldOut ? (
            <p className=“text-sm”>{formatCurrency(unitPrice)}</p>
          ) : (
            <p className=“text-sm font-medium uppercase text-stone-500”>
              Sold out
            </p>
          )}

          {isInCart && (
            <div className=“flex items-center gap-3 sm:gap-8”>
              <UpdateItemQuantity
                pizzaId={id}
                currentQuantity={currentQuantity}
              />
              <DeleteItem pizzaId={id} />
            </div>
          )}

          {!soldOut && !isInCart && (
            <Button type=“small” onClick={handleAddToCart}>
              Add to cart
            </Button>
          )}
        </div>
      </div>
    </li>
  );
}

export default MenuItem;
import { useState } from ‘react’;
import { Form, redirect, useActionData, useNavigation } from ‘react-router-dom’;
import { createOrder } from ‘../../services/apiRestaurant’;
import Button from ‘../../ui/Button’;
import EmptyCart from ‘../cart/EmptyCart’;
import { useDispatch, useSelector } from ‘react-redux’;
import { clearCart, getCart, getTotalCartPrice } from ‘../cart/cartSlice’;
import store from ‘../../store’;
import { formatCurrency } from ‘../../utils/helpers’;
import { fetchAddress } from ‘../user/userSlice’;

// https://uibakery.io/regex-library/phone-number
const isValidPhone = (str) =>
  /^\+?\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}$/.test(
    str
  );

function CreateOrder() {
  const [withPriority, setWithPriority] = useState(false);
  const {
    username,
    status: addressStatus,
    position,
    address,
    error: errorAddress,
  } = useSelector((state) => state.user);
  const isLoadingAddress = addressStatus === ‘loading’;

  const navigation = useNavigation();
  const isSubmitting = navigation.state === ‘submitting’;

  const formErrors = useActionData();
  const dispatch = useDispatch();

  const cart = useSelector(getCart);
  const totalCartPrice = useSelector(getTotalCartPrice);
  const priorityPrice = withPriority ? totalCartPrice * 0.2 : 0;
  const totalPrice = totalCartPrice + priorityPrice;

  if (!cart.length) return <EmptyCart />;

  return (
    <div className=“px-4 py-6”>
      <h2 className=“mb-8 text-xl font-semibold”>Ready to order? Let’s go!</h2>

      {/* <Form method=”POST” action=”/order/new”> */}
      <Form method=“POST”>
        <div className=“mb-5 flex flex-col gap-2 sm:flex-row sm:items-center”>
          <label className=“sm:basis-40”>First Name</label>
          <input
            className=“input grow”
            type=“text”
            name=“customer”
            defaultValue={username}
            required
          />
        </div>

        <div className=“mb-5 flex flex-col gap-2 sm:flex-row sm:items-center”>
          <label className=“sm:basis-40”>Phone number</label>
          <div className=“grow”>
            <input className=“input w-full” type=“tel” name=“phone” required />
            {formErrors?.phone && (
              <p className=“mt-2 rounded-md bg-red-100 p-2 text-xs text-red-700”>
                {formErrors.phone}
              </p>
            )}
          </div>
        </div>

        <div className=“relative mb-5 flex flex-col gap-2 sm:flex-row sm:items-center”>
          <label className=“sm:basis-40”>Address</label>
          <div className=“grow”>
            <input
              className=“input w-full”
              type=“text”
              name=“address”
              disabled={isLoadingAddress}
              defaultValue={address}
              required
            />
            {addressStatus === ‘error’ && (
              <p className=“mt-2 rounded-md bg-red-100 p-2 text-xs text-red-700”>
                {errorAddress}
              </p>
            )}
          </div>

          {!position.latitude && !position.longitude && (
            <span className=“absolute right-[3px] top-[3px] z-50 md:right-[5px] md:top-[5px]”>
              <Button
                disabled={isLoadingAddress}
                type=“small”
                onClick={(e) => {
                  e.preventDefault();
                  dispatch(fetchAddress());
                }}
              >
                Get position
              </Button>
            </span>
          )}
        </div>

        <div className=“mb-12 flex items-center gap-5”>
          <input
            className=“h-6 w-6 accent-yellow-400 focus:outline-none focus:ring focus:ring-yellow-400 focus:ring-offset-2”
            type=“checkbox”
            name=“priority”
            id=“priority”
            value={withPriority}
            onChange={(e) => setWithPriority(e.target.checked)}
          />
          <label htmlFor=“priority” className=“font-medium”>
            Want to yo give your order priority?
          </label>
        </div>

        <div>
          <input type=“hidden” name=“cart” value={JSON.stringify(cart)} />
          <input
            type=“hidden”
            name=“position”
            value={
              position.longitude && position.latitude
                ? `${position.latitude},${position.longitude}`
                :
            }
          />

          <Button disabled={isSubmitting || isLoadingAddress} type=“primary”>
            {isSubmitting
              ? ‘Placing order….’
              : `Order now from ${formatCurrency(totalPrice)}`}
          </Button>
        </div>
      </Form>
    </div>
  );
}

export async function action({ request }) {
  const formData = await request.formData();
  const data = Object.fromEntries(formData);

  const order = {
    data,
    cart: JSON.parse(data.cart),
    priority: data.priority === ‘true’,
  };

  console.log(order);

  const errors = {};
  if (!isValidPhone(order.phone))
    errors.phone =
      ‘Please give us your correct phone number. We might need it to contact you.’;

  if (Object.keys(errors).length > 0) return errors;

  // If everything is okay, create new order and redirect
  const newOrder = await createOrder(order);

  // Do NOT overuse
  store.dispatch(clearCart());

  return redirect(`/order/${newOrder.id}`);
}

export default CreateOrder;
// Test ID: IIDSAT
import { useFetcher, useLoaderData } from ‘react-router-dom’;

import OrderItem from ‘./OrderItem’;

import { getOrder } from ‘../../services/apiRestaurant’;
import {
  calcMinutesLeft,
  formatCurrency,
  formatDate,
} from ‘../../utils/helpers’;
import { useEffect } from ‘react’;
import UpdateOrder from ‘./UpdateOrder’;

function Order() {
  const order = useLoaderData();
  const fetcher = useFetcher();

  useEffect(
    function () {
      if (!fetcher.data && fetcher.state === ‘idle’) fetcher.load(‘/menu’);
    },
    [fetcher]
  );

  // Everyone can search for all orders, so for privacy reasons we’re gonna gonna exclude names or address, these are only for the restaurant staff
  const {
    id,
    status,
    priority,
    priorityPrice,
    orderPrice,
    estimatedDelivery,
    cart,
  } = order;

  const deliveryIn = calcMinutesLeft(estimatedDelivery);

  return (
    <div className=“space-y-8 px-4 py-6”>
      <div className=“flex flex-wrap items-center justify-between gap-2”>
        <h2 className=“text-xl font-semibold”>Order #{id} status</h2>

        <div className=“space-x-2”>
          {priority && (
            <span className=“rounded-full bg-red-500 px-3 py-1 text-sm font-semibold uppercase tracking-wide text-red-50”>
              Priority
            </span>
          )}
          <span className=“rounded-full bg-green-500 px-3 py-1 text-sm font-semibold uppercase tracking-wide text-green-50”>
            {status} order
          </span>
        </div>
      </div>

      <div className=“flex flex-wrap items-center justify-between gap-2 bg-stone-200 px-6 py-5”>
        <p className=“font-medium”>
          {deliveryIn >= 0
            ? `Only ${calcMinutesLeft(estimatedDelivery)} minutes left 😃`
            : ‘Order should have arrived’}
        </p>
        <p className=“text-xs text-stone-500”>
          (Estimated delivery: {formatDate(estimatedDelivery)})
        </p>
      </div>

      <ul className=“dive-stone-200 divide-y border-b border-t”>
        {cart.map((item) => (
          <OrderItem
            item={item}
            key={item.pizzaId}
            isLoadingIngredients={fetcher.state === ‘loading’}
            ingredients={
              fetcher?.data?.find((el) => el.id === item.pizzaId)
                ?.ingredients ?? []
            }
          />
        ))}
      </ul>

      <div className=“space-y-2 bg-stone-200 px-6 py-5”>
        <p className=“text-sm font-medium text-stone-600”>
          Price pizza: {formatCurrency(orderPrice)}
        </p>
        {priority && (
          <p className=“text-sm font-medium text-stone-600”>
            Price priority: {formatCurrency(priorityPrice)}
          </p>
        )}
        <p className=“font-bold”>
          To pay on delivery: {formatCurrency(orderPrice + priorityPrice)}
        </p>
      </div>

      {!priority && <UpdateOrder order={order} />}
    </div>
  );
}

export async function loader({ params }) {
  const order = await getOrder(params.orderId);
  return order;
}

export default Order;
import { formatCurrency } from ‘../../utils/helpers’;

function OrderItem({ item, isLoadingIngredients, ingredients }) {
  const { quantity, name, totalPrice } = item;

  return (
    <li className=“space-y-1 py-3”>
      <div className=“flex items-center justify-between gap-4 text-sm”>
        <p>
          <span className=“font-bold”>{quantity}&times;</span> {name}
        </p>
        <p className=“font-bold”>{formatCurrency(totalPrice)}</p>
      </div>
      <p className=“text-sm capitalize italic text-stone-500”>
        {isLoadingIngredients ? ‘Loading…’ : ingredients.join(‘, ‘)}
      </p>
    </li>
  );
}

export default OrderItem;
import { useState } from ‘react’;
import { useNavigate } from ‘react-router-dom’;

function SearchOrder() {
  const [query, setQuery] = useState();
  const navigate = useNavigate();

  function handleSubmit(e) {
    e.preventDefault();
    if (!query) return;
    navigate(`/order/${query}`);
    setQuery();
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder=“Search order #”
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        className=“w-28 rounded-full bg-yellow-100 px-4 py-2 text-sm transition-all duration-300 placeholder:text-stone-400 focus:outline-none focus:ring focus:ring-yellow-500 focus:ring-opacity-50 sm:w-64 sm:focus:w-72”
      />
    </form>
  );
}

export default SearchOrder;
import { useFetcher } from ‘react-router-dom’;
import Button from ‘../../ui/Button’;
import { updateOrder } from ‘../../services/apiRestaurant’;

function UpdateOrder({ order }) {
  const fetcher = useFetcher();

  return (
    <fetcher.Form method=“PATCH” className=“text-right”>
      <Button type=“primary”>Make priority</Button>
    </fetcher.Form>
  );
}

export default UpdateOrder;

export async function action({ request, params }) {
  const data = { priority: true };
  await updateOrder(params.orderId, data);
  return null;
}
import { useState } from ‘react’;
import Button from ‘../../ui/Button’;
import { useDispatch } from ‘react-redux’;
import { updateName } from ‘./userSlice’;
import { useNavigate } from ‘react-router-dom’;

function CreateUser() {
  const [username, setUsername] = useState();
  const dispatch = useDispatch();
  const navigate = useNavigate();

  function handleSubmit(e) {
    e.preventDefault();

    if (!username) return;
    dispatch(updateName(username));
    navigate(‘/menu’);
  }

  return (
    <form onSubmit={handleSubmit}>
      <p className=“mb-4 text-sm text-stone-600 md:text-base”>
        👋 Welcome! Please start by telling us your name:
      </p>

      <input
        type=“text”
        placeholder=“Your full name”
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        className=“input mb-8 w-72”
      />

      {username !== && (
        <div>
          <Button type=“primary”>Start ordering</Button>
        </div>
      )}
    </form>
  );
}

export default CreateUser;
import { useSelector } from ‘react-redux’;

function Username() {
  const username = useSelector((state) => state.user.username);

  if (!username) return null;

  return (
    <div className=“hidden text-sm font-semibold md:block”>{username}</div>
  );
}

export default Username;
import { createAsyncThunk, createSlice } from ‘@reduxjs/toolkit’;
import { getAddress } from ‘../../services/apiGeocoding’;

function getPosition() {
  return new Promise(function (resolve, reject) {
    navigator.geolocation.getCurrentPosition(resolve, reject);
  });
}

export const fetchAddress = createAsyncThunk(
  ‘user/fetchAddress’,
  async function () {
    // 1) We get the user’s geolocation position
    const positionObj = await getPosition();
    const position = {
      latitude: positionObj.coords.latitude,
      longitude: positionObj.coords.longitude,
    };

    // 2) Then we use a reverse geocoding API to get a description of the user’s address, so we can display it the order form, so that the user can correct it if wrong
    const addressObj = await getAddress(position);
    const address = `${addressObj?.locality}, ${addressObj?.city} ${addressObj?.postcode}, ${addressObj?.countryName}`;

    // 3) Then we return an object with the data that we are interested in.
    // Payload of the FULFILLED state
    return { position, address };
  }
);

const initialState = {
  username: ,
  status: ‘idle’,
  position: {},
  address: ,
  error: ,
};

const userSlice = createSlice({
  name: ‘user’,
  initialState,
  reducers: {
    updateName(state, action) {
      state.username = action.payload;
    },
  },
  extraReducers: (builder) =>
    builder
      .addCase(fetchAddress.pending, (state, action) => {
        state.status = ‘loading’;
      })
      .addCase(fetchAddress.fulfilled, (state, action) => {
        state.position = action.payload.position;
        state.address = action.payload.address;
        state.status = ‘idle’;
      })
      .addCase(fetchAddress.rejected, (state, action) => {
        state.status = ‘error’;
        state.error =
          ‘There was a problem getting your address. Make sure to fill this field!’;
      }),
});

export const { updateName } = userSlice.actions;

export default userSlice.reducer;
export async function getAddress({ latitude, longitude }) {
  const res = await fetch(
    `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${latitude}&longitude=${longitude}`
  );
  if (!res.ok) throw Error(“Failed getting address”);

  const data = await res.json();
  return data;
}
const API_URL = “https://react-fast-pizza-api.onrender.com/api”;

export async function getMenu() {
  const res = await fetch(`${API_URL}/menu`);

  // fetch won’t throw error on 400 errors (e.g. when URL is wrong), so we need to do it manually. This will then go into the catch block, where the message is set
  if (!res.ok) throw Error(“Failed getting menu”);

  const { data } = await res.json();
  return data;
}

export async function getOrder(id) {
  const res = await fetch(`${API_URL}/order/${id}`);
  if (!res.ok) throw Error(`Couldn’t find order #${id}`);

  const { data } = await res.json();
  return data;
}

export async function createOrder(newOrder) {
  try {
    const res = await fetch(`${API_URL}/order`, {
      method: “POST”,
      body: JSON.stringify(newOrder),
      headers: {
        “Content-Type”: “application/json”,
      },
    });

    if (!res.ok) throw Error();
    const { data } = await res.json();
    return data;
  } catch {
    throw Error(“Failed creating your order”);
  }
}

export async function updateOrder(id, updateObj) {
  try {
    const res = await fetch(`${API_URL}/order/${id}`, {
      method: “PATCH”,
      body: JSON.stringify(updateObj),
      headers: {
        “Content-Type”: “application/json”,
      },
    });

    if (!res.ok) throw Error();
    // We don’t need the data, so we don’t return anything
  } catch (err) {
    throw Error(“Failed updating your order”);
  }
}
export function formatCurrency(value) {
  return new Intl.NumberFormat(“en”, {
    style: “currency”,
    currency: “EUR”,
  }).format(value);
}

export function formatDate(dateStr) {
  return new Intl.DateTimeFormat(“en”, {
    day: “numeric”,
    month: “short”,
    hour: “2-digit”,
    minute: “2-digit”,
  }).format(new Date(dateStr));
}

export function calcMinutesLeft(dateStr) {
  const d1 = new Date().getTime();
  const d2 = new Date(dateStr).getTime();
  return Math.round((d2 d1) / 60000);
}
import Header from ‘./Header’;
import LoaderIcon from ‘./LoaderIcon’;
import CartOverview from ‘../features/cart/CartOverview’;
import { Outlet, useNavigation } from ‘react-router-dom’;

function AppLayout() {
  const navigation = useNavigation();
  const isLoading = navigation.state === ‘loading’;

  return (
    <div className=“grid h-screen grid-rows-[auto_1fr_auto]”>
      {isLoading && <LoaderIcon />}

      <Header />

      <div className=“overflow-scroll”>
        <main className=“mx-auto max-w-3xl”>
          <Outlet />
        </main>
      </div>

      <CartOverview />
    </div>
  );
}

export default AppLayout;
import { Link } from ‘react-router-dom’;

function Button({ children, disabled, to, type, onClick }) {
  const base =
    ‘inline-block text-sm rounded-full bg-yellow-400 font-semibold uppercase tracking-wide text-stone-800 transition-colors duration-300 hover:bg-yellow-300 focus:bg-yellow-300 focus:outline-none focus:ring focus:ring-yellow-300 focus:ring-offset-2 disabled:cursor-not-allowed’;

  const styles = {
    primary: base + ‘ px-4 py-3 md:px-6 md:py-4’,
    small: base + ‘ px-4 py-2 md:px-5 md:py-2.5 text-xs’,
    round: base + ‘ px-2.5 py-1 md:px-3.5 md:py-2 text-sm’,
    secondary:
      ‘inline-block text-sm rounded-full border-2 border-stone-300 font-semibold uppercase tracking-wide text-stone-400 transition-colors duration-300 hover:bg-stone-300 hover:text-stone-800 focus:bg-stone-300 focus:text-stone-800 focus:outline-none focus:ring focus:ring-stone-200 focus:ring-offset-2 disabled:cursor-not-allowed px-4 py-2.5 md:px-6 md:py-3.5’,
  };

  if (to)
    return (
      <Link to={to} className={styles[type]}>
        {children}
      </Link>
    );

  if (onClick)
    return (
      <button onClick={onClick} disabled={disabled} className={styles[type]}>
        {children}
      </button>
    );

  return (
    <button disabled={disabled} className={styles[type]}>
      {children}
    </button>
  );
}

export default Button;
import { useRouteError } from ‘react-router-dom’;
import LinkButton from ‘./LinkButton’;

function Error() {
  const error = useRouteError();
  console.log(error);

  return (
    <div>
      <h1>Something went wrong 😢</h1>
      <p>{error.data || error.message}</p>

      <LinkButton to=“-1”>&larr; Go back</LinkButton>
    </div>
  );
}

export default Error;
import { Link } from ‘react-router-dom’;
import SearchOrder from ‘../features/order/SearchOrder’;
import Username from ‘../features/user/Username’;

function Header() {
  return (
    <header className=“flex items-center justify-between border-b border-stone-200 bg-yellow-400 px-4 py-3 uppercase sm:px-6”>
      <Link to=“/” className=“tracking-widest”>
        Fast React Pizza Co.
      </Link>

      <SearchOrder />
      <Username />
    </header>
  );
}

export default Header;
import { useSelector } from ‘react-redux’;
import CreateUser from ‘../features/user/CreateUser’;
import Button from ‘./Button’;

function Home() {
  const username = useSelector((state) => state.user.username);

  return (
    <div className=“my-10 px-4 text-center sm:my-16”>
      <h1 className=“mb-8  text-xl font-semibold md:text-3xl”>
        The best pizza.
        <br />
        <span className=“text-yellow-500”>
          Straight out of the oven, straight to you.
        </span>
      </h1>

      {username === ? (
        <CreateUser />
      ) : (
        <Button to=“/menu” type=“primary”>
          Continue ordering, {username}
        </Button>
      )}
    </div>
  );
}

export default Home;
import { Link, useNavigate } from ‘react-router-dom’;

function LinkButton({ children, to }) {
  const navigate = useNavigate();
  const className = ‘text-sm text-blue-500 hover:text-blue-600 hover:underline’;

  if (to === ‘-1’)
    return (
      <button className={className} onClick={() => navigate(1)}>
        {children}
      </button>
    );

  return (
    <Link to={to} className={className}>
      {children}
    </Link>
  );
}

export default LinkButton;
function LoaderIcon() {
  return (
    <div className=“absolute inset-0 flex items-center justify-center bg-slate-200/20 backdrop-blur-sm”>
      <div className=“loader”></div>
    </div>
  );
}

export default LoaderIcon;