Skip to content

Navbar My order page in mobile view #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e70320e
copy data.json from main branch
murtazatankiwala456 Jun 11, 2024
598bfa5
completed till fetching products from dummy api (from folder-static)
murtazatankiwala456 Jun 12, 2024
9e60977
fetching products through json-server using redux toolkit
murtazatankiwala456 Jun 12, 2024
37a15cb
adding brands and categories section extracting from json (using map)
murtazatankiwala456 Jun 12, 2024
a92b677
filter feature added using redux and json server api
murtazatankiwala456 Jun 13, 2024
138d9f2
adding sorting functionality(desc according to new ruleJSON Server's …
murtazatankiwala456 Jun 14, 2024
049dc8d
fixing filter in mobile layout (add onClick func in mobile input)
murtazatankiwala456 Jun 14, 2024
be55c81
dividing product list into components (Pagination,Mobile/DesktopFilte…
murtazatankiwala456 Jun 14, 2024
c297593
checked functionality/common dispatch(AllProducts,filter)
murtazatankiwala456 Jun 15, 2024
82c96b3
makine array of objects for filter/ making sort as another function a…
murtazatankiwala456 Jun 15, 2024
d507391
pagination code done but not reflecting products on UI(have to fix it)
murtazatankiwala456 Jun 17, 2024
ac3fa71
fixed pagination issues:key instead of pagination:pagination[key]/ext…
murtazatankiwala456 Jun 17, 2024
3316e43
unable to render totalItems in UI pagination (have to fix)
murtazatankiwala456 Jun 17, 2024
40a7fce
fixed totalItems by extracting direct from json obj (data.items)/ imp…
murtazatankiwala456 Jun 17, 2024
a06e910
calling categories and brand through API
murtazatankiwala456 Jun 18, 2024
a591305
previous/next button is now working
murtazatankiwala456 Jun 19, 2024
0f7b724
product detail page added (dynamically)
murtazatankiwala456 Jun 20, 2024
0fb1030
form validation completed
murtazatankiwala456 Jun 21, 2024
b50cc6e
signup/login functionality add
murtazatankiwala456 Jun 21, 2024
5a68db0
fix path issue (Protected)
murtazatankiwala456 Jun 21, 2024
4e8afb6
add to cart API created
murtazatankiwala456 Jun 22, 2024
2291936
add to cart/fetch items in UI (Cart)
murtazatankiwala456 Jun 22, 2024
71b78c3
add to cart/update quantity
murtazatankiwala456 Jun 22, 2024
6300124
add to cart/delete item
murtazatankiwala456 Jun 22, 2024
6f0e5a1
checkout page functionality (grabing form values) done
murtazatankiwala456 Jun 24, 2024
9071af6
order API added
murtazatankiwala456 Jun 24, 2024
9b18c85
checkout validation/404page/OrderSuccessPage
murtazatankiwala456 Jun 25, 2024
266b7d7
successfully navigate to orderSuccessPage after clicking on order now
murtazatankiwala456 Jun 25, 2024
d89caed
empty card and currentOrder from the UI and API
murtazatankiwala456 Jun 25, 2024
51072c9
UserOrder created successfully/ (Math.ceil removed for exact calc)
murtazatankiwala456 Jun 25, 2024
f6bcaec
fix navbar menu issue in mobile layout
murtazatankiwala456 Jun 26, 2024
9a7f78d
My profile page added/fettchLoogedInUser API created for full details…
murtazatankiwala456 Jun 26, 2024
beab140
edit/remove address functionality in my profile page
murtazatankiwala456 Jun 26, 2024
834bc67
Add new Address functionality added
murtazatankiwala456 Jun 26, 2024
67348b7
signout functionality/forgot password page
murtazatankiwala456 Jun 27, 2024
22b1dab
admin route created
murtazatankiwala456 Jun 28, 2024
1a5a4b2
new product functionality added in admin route
murtazatankiwala456 Jun 28, 2024
418d60e
edit product functionality added in admin route
murtazatankiwala456 Jun 28, 2024
e1960cd
clean up the code (discard counter app actions)
murtazatankiwala456 Jun 29, 2024
7be4998
invoke reset button in checkout page
murtazatankiwala456 Jun 29, 2024
a589aa7
setValue fix in ProductForm.js
murtazatankiwala456 Jun 29, 2024
86bacfa
AdminOrderPage created (static)
murtazatankiwala456 Jul 1, 2024
290a6f9
make constant of discountedPrice()/ cancle buttons works in product form
murtazatankiwala456 Jul 1, 2024
88cd7bc
edit functionality in adminOrderPage/fix route of AdminDetailPage
murtazatankiwala456 Jul 2, 2024
303bc93
pagination added it AdminOrder Page (by making pagination a common co…
murtazatankiwala456 Jul 2, 2024
92a66e4
fix pagination logic in AdminOrder
murtazatankiwala456 Jul 2, 2024
acf0ed0
UI Changes + toggle show hide password button in login page
murtazatankiwala456 Jul 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,818 changes: 3,818 additions & 0 deletions data.json

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
"@testing-library/user-event": "^14.4.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.10.0",
"react-scripts": "5.0.1",
154 changes: 131 additions & 23 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,155 @@
import { Counter } from './features/counter/Counter';
import './App.css';
import Home from './pages/Home';
import LoginPage from './pages/LoginPage';
import SignupPage from './pages/SignupPage';
import { Counter } from "./features/counter/Counter";
import "./App.css";
import Home from "./pages/Home";
import LoginPage from "./pages/LoginPage";
import SignupPage from "./pages/SignupPage";

import {
createBrowserRouter,
RouterProvider,
Route,
Link,
} from 'react-router-dom';
import Cart from './features/cart/Cart';
import CartPage from './pages/CartPage';
import Checkout from './pages/Checkout';
import ProductDetailPage from './pages/ProductDetailPage';
} from "react-router-dom";

import CartPage from "./pages/CartPage";
import Checkout from "./pages/Checkout";
import ProductDetailPage from "./pages/ProductDetailPage";
import Protected from "./features/auth/components/Protected";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { selectLoggedInUser } from "./features/auth/authSlice";
import { fetchItemsByUserIdAsync } from "./features/cart/cartSlice";
import PageNotFound from "./pages/404";
import OrderSuccessPage from "./pages/OrderSuccessPage";
import UserOrderPage from "./pages/UserOrderPage";
import UserProfilePage from "./pages/UserProfilePage";
import { fetchLoggedInUserAsync } from "./features/user/userSlice";
import LogOut from "./features/auth/components/LogOut";
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
import ProtectedAdmin from "./features/auth/components/ProtectedAdmin";
import AdminHome from "./pages/AdminHome";
import AdminProductDetail from "./features/admin/components/AdminProductDetail";
import ProductFormPage from "./pages/ProductFormPage";
import AdminOrderPage from "./pages/AdminOrderPage";
const router = createBrowserRouter([
{
path: '/',
element: <Home></Home>,
path: "/",

element: (
<Protected>
<Home></Home>
</Protected>
),
},
{
path: "/admin",

element: (
<ProtectedAdmin>
<AdminHome></AdminHome>
</ProtectedAdmin>
),
},
{
path: '/login',
path: "/login",
element: <LoginPage></LoginPage>,
},
{
path: '/signup',
path: "/signup",
element: <SignupPage></SignupPage>,
},
{
path: '/cart',
element: <CartPage></CartPage>,
{
path: "/cart",
element: (
<Protected>
<CartPage></CartPage>
</Protected>
),
},
{
path: '/checkout',
element: <Checkout></Checkout>,
{
path: "/checkout",
element: (
<Protected>
<Checkout></Checkout>
</Protected>
),
},
{
path: '/product-detail',
element: <ProductDetailPage></ProductDetailPage>,
{
path: "/product-detail/:id", // :id provided by react-router
element: (
<Protected>
<ProductDetailPage></ProductDetailPage>
</Protected>
),
},
{
path: "/admin/product-detail/:id", // :id provided by react-router
element: (
<ProtectedAdmin>
<AdminProductDetail></AdminProductDetail>
</ProtectedAdmin>
),
},
{
path: "/admin/product-form",
element: (
<ProtectedAdmin>
<ProductFormPage></ProductFormPage>
</ProtectedAdmin>
),
},
{
path: "/admin/orders",
element: (
<ProtectedAdmin>
<AdminOrderPage></AdminOrderPage>
</ProtectedAdmin>
),
},
{
path: "/admin/product-form/edit/:id",
element: (
<ProtectedAdmin>
<ProductFormPage></ProductFormPage>
</ProtectedAdmin>
),
},
{
path: "/order-success/:id",
element: <OrderSuccessPage></OrderSuccessPage>,
},
{
path: "/orders",
element: <UserOrderPage></UserOrderPage>,
},
{
path: "/profile",
element: <UserProfilePage></UserProfilePage>,
},
{
path: "/logout",
element: <LogOut></LogOut>,
},
{
path: "/forgot-paasword",
element: <ForgotPasswordPage></ForgotPasswordPage>,
},
{
path: "*",
element: <PageNotFound></PageNotFound>,
},
]);

function App() {
const dispatch = useDispatch();
const user = useSelector(selectLoggedInUser);

useEffect(() => {
if (user) {
dispatch(fetchItemsByUserIdAsync(user.id));
dispatch(fetchLoggedInUserAsync(user.id));
}
}, [dispatch, user]);
return (
<div className="App">
<RouterProvider router={router} />
5 changes: 5 additions & 0 deletions src/app/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const ITEMS_PER_PAGE = 6;
export function discountedPrice(item) {
const discountPercentage = item.discountPercentage || 0; // Ensure discountPercentage is defined or default to 0
return Math.round(item.price * (1 - item.discountPercentage / 100));
}
14 changes: 11 additions & 3 deletions src/app/store.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import { configureStore } from "@reduxjs/toolkit";
import productReducer from "../features/product/productSlice";
import authReducer from "../features/auth/authSlice";
import cartReducer from "../features/cart/cartSlice";
import orderReducer from "../features/order/orderSlice";
import userReducer from "../features/user/userSlice";

export const store = configureStore({
reducer: {
counter: counterReducer,
product: productReducer,
auth: authReducer,
cart: cartReducer,
order: orderReducer,
user: userReducer,
},
});
177 changes: 177 additions & 0 deletions src/features/admin/components/AdminOrder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { useDispatch, useSelector } from "react-redux";
import { ITEMS_PER_PAGE, discountedPrice } from "../../../app/constants";
import { useEffect, useState } from "react";
import { EyeIcon, PencilIcon } from "@heroicons/react/24/outline";
import {
fetchAllOrdersAsync,
selectOrders,
selectTotalOrders,
updateOrderAsync,
} from "../../order/orderSlice";
import Pagination from "../../common/Pagination";

function AdminOrder() {
const [page, setPage] = useState(1);
const dispatch = useDispatch();
const orders = useSelector(selectOrders);
const totalOrders = useSelector(selectTotalOrders);
const [editableOrderId, setEditableOrderId] = useState(-1);

const handleEdit = (order) => {
console.log("Editing order ID:", order.id);
setEditableOrderId(order.id);
};

const handleShow = () => {
console.log("show");
};

const handleUpdate = (e, order) => {
const updatedOrder = { ...order, status: e.target.value };
dispatch(updateOrderAsync(updatedOrder));
setEditableOrderId(-1); // Optionally reset the editable order ID after update
};
const chooseColor = (status) => {
switch (status) {
case "pending":
return "bg-purple-200 ms-4 text-purple-600 ";
case "dispatched":
return "bg-yellow-200 ms-4 text-yellow-600 ";
case "delivered":
return "bg-green-200 ms-4 text-green-600 ";
case "pending":
return "bg-red-200 ms-4 text-red-600 ";
case "cancelled":
return "bg-red-200 ms-4 text-red-600 ";
default:
return "bg-purple-200 ms-4 text-purple-600 ";
}
};
const handlePage = (page) => {
setPage(page);
const pagination = { _page: page, _per_page: ITEMS_PER_PAGE };
dispatch(fetchAllOrdersAsync(pagination));
};

useEffect(() => {
const pagination = { _page: page, _per_page: ITEMS_PER_PAGE };
dispatch(fetchAllOrdersAsync(pagination));
}, [dispatch, page]);

return (
<div className="overflow-x-auto w-full">
<div className="bg-gray-100 flex items-center justify-center bg-gray-100 font-sans overflow-hidden w-full">
<div className="w-full">
<div className="bg-white shadow-md rounded my-6 w-full">
<table className="min-w-max w-full table-auto">
<thead>
<tr className="bg-gray-200 text-gray-600 uppercase text-sm leading-normal">
<th className="py-3 px-6 text-left">Order#</th>
<th className="py-3 px-6 text-left">Items</th>
<th className="py-3 px-6 text-center">Total Amount</th>
<th className="py-3 px-6 text-center">Shipping Address</th>
<th className="py-3 px-6 text-center">Status</th>
<th className="py-3 px-6 text-center">Actions</th>
</tr>
</thead>
<tbody className="text-gray-600 text-sm font-light">
{orders.map((order) => (
<tr
className="border-b border-gray-200 hover:bg-gray-100"
key={order.id}
>
<td className="py-3 px-6 text-left whitespace-nowrap">
<div className="flex items-center">
<span className="font-medium">{order.id}</span>
</div>
</td>
<td className="py-3 px-6 text-left">
{order.items.map((item) => (
<div className="flex items-center" key={item.id}>
<div className="mr-2">
<img
className="w-6 h-6 rounded-full"
src={item.thumbnail}
alt={item.title}
/>
</div>
<span>
{item.title} - #{item.quantity} - $
{discountedPrice(item)}
</span>
</div>
))}
</td>
<td className="py-3 px-6 text-center">
<div className="flex items-center justify-center">
${order.totalAmount}
</div>
</td>
<td className="py-3 px-6 text-center">
<div>
<div>
<strong>{order.selectedAddress.name}</strong>
</div>
<div>{order.selectedAddress.street}</div>
<div>{order.selectedAddress.city}</div>
<div>{order.selectedAddress.pinCode}</div>
<div>{order.selectedAddress.phone}</div>
</div>
</td>
<td className=" text-center">
{order.id === editableOrderId ? (
<div className="flex items-center justify-center">
<select
onChange={(e) => handleUpdate(e, order)}
value={order.status}
>
<option value="pending">Pending</option>
<option value="dispatched">Dispatched</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
) : (
<span
className={`${chooseColor(
order.status
)}py-1 px-3 rounded-full text-xs`}
>
{order.status}
</span>
)}
</td>
<td className="py-3 px-6 text-center">
<div className="flex items-center justify-center">
<div className="w-2 mr-6 transform hover:text-purple-500 hover:scale-110">
<EyeIcon
className="w-6 h-6"
onClick={() => handleShow(order)}
/>
</div>
<div className="w-2 transform hover:text-purple-500 hover:scale-110">
<PencilIcon
className="w-6 h-6"
onClick={() => handleEdit(order)}
/>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
<Pagination
page={page}
setPage={setPage}
handlePage={handlePage}
totalItems={totalOrders}
/>
</div>
);
}

export default AdminOrder;
355 changes: 355 additions & 0 deletions src/features/admin/components/AdminProductDetail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
import { useEffect, useState } from "react";
import { StarIcon } from "@heroicons/react/20/solid";
import { RadioGroup } from "@headlessui/react";
import { useSelector, useDispatch } from "react-redux";
import {
fetchProductByIdAsync,
selectProductById,
} from "../../product/productSlice";
import { useParams } from "react-router-dom";
import { addToCartAsync } from "../../cart/cartSlice";
import { selectLoggedInUser } from "../../auth/authSlice";
import { discountedPrice } from "../../../app/constants";

// TODO: In server data we will add sizes,colors, highlights to each product

const colors = [
{ name: "White", class: "bg-white", selectedClass: "ring-gray-400" },
{ name: "Gray", class: "bg-gray-200", selectedClass: "ring-gray-400" },
{ name: "Black", class: "bg-gray-900", selectedClass: "ring-gray-900" },
];
const sizes = [
{ name: "XXS", inStock: false },
{ name: "XS", inStock: true },
{ name: "S", inStock: true },
{ name: "M", inStock: true },
{ name: "L", inStock: true },
{ name: "XL", inStock: true },
{ name: "2XL", inStock: true },
{ name: "3XL", inStock: true },
];
const highlights = [
"Hand cut and sewn locally",
"Dyed with our proprietary colors",
"Pre-washed & pre-shrunk",
"Ultra-soft 100% cotton",
];

function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}

export default function AdminProductDetail() {
const [selectedColor, setSelectedColor] = useState(colors[0]);
const [selectedSize, setSelectedSize] = useState(sizes[2]);
const user = useSelector(selectLoggedInUser);
const product = useSelector(selectProductById);
const dispatch = useDispatch();
const params = useParams(); // hook provided by react-router to fetch parameters

const handleCart = (e) => {
e.preventDefault();
const newItem = { ...product, quantity: 1, user: user.id };
delete newItem["id"]; // to avoid creating same id on adding same product to cart....
dispatch(addToCartAsync(newItem));
};

useEffect(() => {
dispatch(fetchProductByIdAsync(params.id)); // :id from path in app.js
}, [dispatch, params.id]);
return (
<div className="bg-white">
{product && (
<div className="pt-6">
<nav aria-label="Breadcrumb">
<ol
role="list"
className="mx-auto flex max-w-2xl items-center space-x-2 px-4 sm:px-6 lg:max-w-7xl lg:px-8"
>
{product.breadcrumbs &&
product.breadcrumbs.map((breadcrumb) => (
<li key={breadcrumb.id}>
<div className="flex items-center">
<a
href={breadcrumb.href}
className="mr-2 text-sm font-medium text-gray-900"
>
{breadcrumb.name}
</a>
<svg
width={16}
height={20}
viewBox="0 0 16 20"
fill="currentColor"
aria-hidden="true"
className="h-5 w-4 text-gray-300"
>
<path d="M5.697 4.34L8.98 16.532h1.327L7.025 4.341H5.697z" />
</svg>
</div>
</li>
))}
<li className="text-sm">
<a
href={product.href}
aria-current="page"
className="font-medium text-gray-500 hover:text-gray-600"
>
{product.title}
</a>
</li>
</ol>
</nav>

{/* Image gallery */}
<div className="mx-auto mt-6 max-w-2xl sm:px-6 lg:grid lg:max-w-7xl lg:grid-cols-3 lg:gap-x-8 lg:px-8">
<div className="aspect-h-4 aspect-w-3 hidden overflow-hidden rounded-lg lg:block">
<img
src={product.images[0]}
alt={product.title}
className="h-full w-full object-cover object-center"
/>
</div>
<div className="hidden lg:grid lg:grid-cols-1 lg:gap-y-8">
<div className="aspect-h-2 aspect-w-3 overflow-hidden rounded-lg">
<img
src={product.images[0]}
alt={product.title}
className="h-full w-full object-cover object-center"
/>
</div>
<div className="aspect-h-2 aspect-w-3 overflow-hidden rounded-lg">
<img
src={product.images[0]}
alt={product.title}
className="h-full w-full object-cover object-center"
/>
</div>
</div>
<div className="aspect-h-5 aspect-w-4 lg:aspect-h-4 lg:aspect-w-3 sm:overflow-hidden sm:rounded-lg">
<img
src={product.images[0]}
alt={product.title}
className="h-full w-full object-cover object-center"
/>
</div>
</div>

{/* Product info */}
<div className="mx-auto max-w-2xl px-4 pb-16 pt-10 sm:px-6 lg:grid lg:max-w-7xl lg:grid-cols-3 lg:grid-rows-[auto,auto,1fr] lg:gap-x-8 lg:px-8 lg:pb-24 lg:pt-16">
<div className="lg:col-span-2 lg:border-r lg:border-gray-200 lg:pr-8">
<h1 className="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">
{product.title}
</h1>
</div>

{/* Options */}
<div className="mt-4 lg:row-span-3 lg:mt-0">
<h2 className="sr-only">Product information</h2>
<p className="text-3xl line-through tracking-tight text-gray-900">
${product.price}
</p>
<p className="text-3xl tracking-tight text-gray-900">
${discountedPrice(product)}
</p>

{/* Reviews */}
<div className="mt-6">
<h3 className="sr-only">Reviews</h3>
<div className="flex items-center">
<div className="flex items-center">
{[0, 1, 2, 3, 4].map((rating) => (
<StarIcon
key={rating}
className={classNames(
product.rating > rating
? "text-gray-900"
: "text-gray-200",
"h-5 w-5 flex-shrink-0"
)}
aria-hidden="true"
/>
))}
</div>
<p className="sr-only">{product.rating} out of 5 stars</p>
</div>
</div>

<form className="mt-10">
{/* Colors */}
<div>
<h3 className="text-sm font-medium text-gray-900">Color</h3>

<RadioGroup
value={selectedColor}
onChange={setSelectedColor}
className="mt-4"
>
<RadioGroup.Label className="sr-only">
Choose a color
</RadioGroup.Label>
<div className="flex items-center space-x-3">
{colors.map((color) => (
<RadioGroup.Option
key={color.name}
value={color}
className={({ active, checked }) =>
classNames(
color.selectedClass,
active && checked ? "ring ring-offset-1" : "",
!active && checked ? "ring-2" : "",
"relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 focus:outline-none"
)
}
>
<RadioGroup.Label as="span" className="sr-only">
{color.name}
</RadioGroup.Label>
<span
aria-hidden="true"
className={classNames(
color.class,
"h-8 w-8 rounded-full border border-black border-opacity-10"
)}
/>
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</div>

{/* Sizes */}
<div className="mt-10">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900">Size</h3>
<a
href="#"
className="text-sm font-medium text-gray-600 hover:text-gray-500"
>
Size guide
</a>
</div>

<RadioGroup
value={selectedSize}
onChange={setSelectedSize}
className="mt-4"
>
<RadioGroup.Label className="sr-only">
Choose a size
</RadioGroup.Label>
<div className="grid grid-cols-4 gap-4 sm:grid-cols-8 lg:grid-cols-4">
{sizes.map((size) => (
<RadioGroup.Option
key={size.name}
value={size}
disabled={!size.inStock}
className={({ active }) =>
classNames(
size.inStock
? "cursor-pointer bg-white text-gray-900 shadow-sm"
: "cursor-not-allowed bg-gray-50 text-gray-200",
active ? "ring-2 ring-gray-500" : "",
"group relative flex items-center justify-center rounded-md border py-3 px-4 text-sm font-medium uppercase hover:bg-gray-50 focus:outline-none sm:flex-1 sm:py-6"
)
}
>
{({ active, checked }) => (
<>
<RadioGroup.Label as="span">
{size.name}
</RadioGroup.Label>
{size.inStock ? (
<span
className={classNames(
active ? "border" : "border-2",
checked
? "border-gray-500"
: "border-transparent",
"pointer-events-none absolute -inset-px rounded-md"
)}
aria-hidden="true"
/>
) : (
<span
aria-hidden="true"
className="pointer-events-none absolute -inset-px rounded-md border-2 border-gray-200"
>
<svg
className="absolute inset-0 h-full w-full stroke-2 text-gray-200"
viewBox="0 0 100 100"
preserveAspectRatio="none"
stroke="currentColor"
>
<line
x1={0}
y1={100}
x2={100}
y2={0}
vectorEffect="non-scaling-stroke"
/>
</svg>
</span>
)}
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</div>

<button
onClick={(e) => {
handleCart(e);
}}
type="submit"
className="mt-10 flex w-full items-center justify-center rounded-md border border-transparent bg-gray-600 px-8 py-3 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
>
Add to Cart
</button>
</form>
</div>

<div className="py-10 lg:col-span-2 lg:col-start-1 lg:border-r lg:border-gray-200 lg:pb-16 lg:pr-8 lg:pt-6">
{/* Description and details */}
<div>
<h3 className="sr-only">Description</h3>

<div className="space-y-6">
<p className="text-base text-gray-900">
{product.description}
</p>
</div>
</div>

<div className="mt-10">
<h3 className="text-sm font-medium text-gray-900">
Highlights
</h3>

<div className="mt-4">
<ul role="list" className="list-disc space-y-2 pl-4 text-sm">
{highlights.map((highlight) => (
<li key={highlight} className="text-gray-400">
<span className="text-gray-600">{highlight}</span>
</li>
))}
</ul>
</div>
</div>

<div className="mt-10">
<h2 className="text-sm font-medium text-gray-900">Details</h2>

<div className="mt-4 space-y-6">
<p className="text-sm text-gray-600">{product.description}</p>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}
548 changes: 548 additions & 0 deletions src/features/admin/components/AdminProductList.js

Large diffs are not rendered by default.

389 changes: 389 additions & 0 deletions src/features/admin/components/ProductForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,389 @@
import { useDispatch, useSelector } from "react-redux";
import {
clearSelectedProduct,
createProductAsync,
fetchProductByIdAsync,
selectBrands,
selectCategories,
selectProductById,
updateProductAsync,
} from "../../product/productSlice";
import { PhotoIcon, UserCircleIcon } from "@heroicons/react/24/solid";
import { useForm } from "react-hook-form";
import { useEffect } from "react";
import { Link, useParams } from "react-router-dom";
function ProductForm() {
const categories = useSelector(selectCategories);
const brands = useSelector(selectBrands);
const dispatch = useDispatch();
const params = useParams();
const selectedProduct = useSelector(selectProductById);
const {
register,
handleSubmit,
setValue,
reset,
formState: { errors },
} = useForm();

useEffect(() => {
if (params.id) {
dispatch(fetchProductByIdAsync(params.id));
} else {
dispatch(clearSelectedProduct());
}
}, [params.id, dispatch]);

useEffect(() => {
if (selectedProduct && params.id) {
setValue("title", selectedProduct.title);
setValue("description", selectedProduct.description);
setValue("brand", selectedProduct.brand);
setValue("category", selectedProduct.category);
setValue("price", selectedProduct.price);
setValue("discountPercentage", selectedProduct.discountPercentage);
setValue("stock", selectedProduct.stock);
setValue("thumbnail", selectedProduct.thumbnail);
setValue("image", selectedProduct.images[0]);
}
}, [setValue, params.id, selectedProduct]);

const handleDelete = () => {
const product = { ...selectedProduct };
product.deleted = true;
dispatch(updateProductAsync(product));
};
return (
<form
noValidate
onSubmit={handleSubmit((data) => {
console.log(data);
const product = { ...data };
product.images = [product.image, product.thumbnail];
delete product["image"];
product.rating = 0;

product.price = +product.price;

product.discountPercentage = +product.discountPercentage;
product.stock = +product.stock;
product.discount = +product.discount;
console.log(product);

if (params.id) {
product.id = params.id;
product.rating = selectedProduct.rating || 0;
dispatch(updateProductAsync(product));
reset();
} else {
dispatch(createProductAsync(product));
reset();
}
})}
>
<div className="space-y-12 bg-white p-12">
<div className="border-b border-gray-900/10 pb-12">
<h2 className="text-base font-semibold leading-7 text-gray-900">
Add Product
</h2>

<div className="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div className="sm:col-span-6">
<label
htmlFor="title"
className="block text-sm font-medium leading-6 text-gray-900"
>
Product Name
</label>
<div className="mt-2">
<div className="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-gray-600">
<input
type="text"
{...register("title", {
required: "title is required",
})}
id="title"
className="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>

<div className="col-span-full">
<label
htmlFor="description"
className="block text-sm font-medium leading-6 text-gray-900"
>
Description
</label>
<div className="mt-2">
<textarea
id="description"
{...register("description", {
required: "description is required",
})}
rows={3}
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6"
defaultValue={""}
/>
</div>
<p className="mt-3 text-sm leading-6 text-gray-600">
Write a few sentences about product.
</p>
</div>
<div className="col-span-full">
<label
htmlFor="brand"
className="block text-sm font-medium leading-6 text-gray-900"
>
Brand
</label>
<div className="mt-2">
<select
{...register("brand", {
required: "brand is required",
})}
>
<option>--choose brand--</option>
{brands.map((brand) => (
<option value={brand.value}>{brand.label}</option>
))}
</select>
</div>
</div>
<div className="col-span-full">
<label
htmlFor="Category"
className="block text-sm font-medium leading-6 text-gray-900"
>
Category
</label>
<div className="mt-2">
<select
{...register("category", {
required: "catergory is required",
})}
>
<option>--choose category--</option>
{categories.map((category) => (
<option value={category.value}>{category.label}</option>
))}
</select>
</div>
</div>
<div className="sm:col-span-2">
<label
htmlFor="price"
className="block text-sm font-medium leading-6 text-gray-900"
>
Price
</label>
<div className="mt-2">
<div className="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-gray-600">
<input
type="number"
{...register("price", {
required: "price is required",
min: 1,
max: 10000,
})}
id="price"
className="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>
<div className="sm:col-span-2">
<label
htmlFor="discountPercentage"
className="block text-sm font-medium leading-6 text-gray-900"
>
Discount
</label>
<div className="mt-2">
<div className="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-gray-600">
<input
type="number"
{...register("discountPercentage", {
required: "discountPercentage is required",
min: 0,
max: 100,
})}
id="discountPercentage"
className="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>
<div className="sm:col-span-2">
<label
htmlFor="stock"
className="block text-sm font-medium leading-6 text-gray-900"
>
Stock
</label>
<div className="mt-2">
<div className="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-gray-600">
<input
type="number"
{...register("stock", {
required: "stock is required",
min: 0,
})}
id="stock"
className="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>
<div className="sm:col-span-6">
<label
htmlFor="thumbnail"
className="block text-sm font-medium leading-6 text-gray-900"
>
Thumbnail
</label>
<div className="mt-2">
<div className="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-gray-600">
<input
type="text"
{...register("thumbnail", {
required: "thumbnail is required",
})}
id="thumbnail"
className="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>

<div className="sm:col-span-6">
<label
htmlFor="image"
className="block text-sm font-medium leading-6 text-gray-900"
>
Image
</label>
<div className="mt-2">
<div className="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-gray-600">
<input
type="text"
{...register("image", {
required: "image is required",
})}
id="image"
className="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>
</div>
</div>

<div className="border-b border-gray-900/10 pb-12">
<h2 className="text-base font-semibold leading-7 text-gray-900">
Extra
</h2>

<div className="mt-10 space-y-10">
<fieldset>
<legend className="text-sm font-semibold leading-6 text-gray-900">
By Email
</legend>
<div className="mt-6 space-y-6">
<div className="relative flex gap-x-3">
<div className="flex h-6 items-center">
<input
id="comments"
name="comments"
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-gray-600 focus:ring-gray-600"
/>
</div>
<div className="text-sm leading-6">
<label
htmlFor="comments"
className="font-medium text-gray-900"
>
Comments
</label>
<p className="text-gray-500">
Get notified when someones posts a comment on a posting.
</p>
</div>
</div>
<div className="relative flex gap-x-3">
<div className="flex h-6 items-center">
<input
id="candidates"
name="candidates"
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-gray-600 focus:ring-gray-600"
/>
</div>
<div className="text-sm leading-6">
<label
htmlFor="candidates"
className="font-medium text-gray-900"
>
Candidates
</label>
<p className="text-gray-500">
Get notified when a candidate applies for a job.
</p>
</div>
</div>
<div className="relative flex gap-x-3">
<div className="flex h-6 items-center">
<input
id="offers"
name="offers"
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-gray-600 focus:ring-gray-600"
/>
</div>
<div className="text-sm leading-6">
<label
htmlFor="offers"
className="font-medium text-gray-900"
>
Offers
</label>
<p className="text-gray-500">
Get notified when a candidate accepts or rejects an offer.
</p>
</div>
</div>
</div>
</fieldset>
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-x-6">
<Link
to="/admin"
type="button"
className="text-sm font-semibold leading-6 text-gray-900"
>
Cancel
</Link>
{selectedProduct && (
<button
onClick={handleDelete}
className="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
>
Delete
</button>
)}
<button
type="submit"
className="rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
>
Save
</button>
</div>
</div>
</form>
);
}

export default ProductForm;
48 changes: 41 additions & 7 deletions src/features/auth/authAPI.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,42 @@
export function fetchCount(amount = 1) {
return new Promise(async (resolve) =>{
const response = await fetch('http://localhost:8080')
const data = await response.json()
resolve({data})
}
);
// create user
export function createUser(userData) {
return new Promise(async (resolve) => {
const response = await fetch("http://localhost:8080/users", {
method: "POST",
body: JSON.stringify(userData),
headers: { "content-type": "application/json" },
});
const data = await response.json();
// TODO:on server it will only return some info of user (not password)
resolve({ data });
});
}
// check user
export function checkUser(loginInfo) {
return new Promise(async (resolve, reject) => {
const email = loginInfo.email;
const password = loginInfo.password;
const response = await fetch("http://localhost:8080/users?email=" + email);
const data = await response.json();
console.log({ data });
if (data.length) {
// data[0] to extract first match..
if (password === data[0].password) {
resolve({ data: data[0] });
} else {
reject({ message: "wrong credentials" });
}
} else {
reject({ message: "User not found" });
}

// TODO:on server it will only return some info of user (not password)
});
}
// remove user
export function signOut(userId) {
return new Promise(async (resolve) => {
// TODO: on server we will remove user session info
resolve({ data: "success" });
});
}
91 changes: 67 additions & 24 deletions src/features/auth/authSlice.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,85 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './authAPI';
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { checkUser, createUser, signOut } from "./authAPI";
import { updateUser } from "../user/userAPI";

const initialState = {
value: 0,
status: 'idle',
loggedInUser: null,
status: "idle",
erorr: null,
};

export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount) => {
const response = await fetchCount(amount);
export const createUserAsync = createAsyncThunk(
"user/createUser",
async (userData) => {
const response = await createUser(userData);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const updateUserAsync = createAsyncThunk(
"user/updateUser",
async (update) => {
const response = await updateUser(update);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const checkUserAsync = createAsyncThunk(
"user/checkUser",
async (loginInfo) => {
const response = await checkUser(loginInfo);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const signOutAsync = createAsyncThunk("user/signOut", async (userId) => {
const response = await signOut(userId);
// The value we return becomes the `fulfilled` action payload
return response.data;
});

export const counterSlice = createSlice({
name: 'counter',
export const authSlice = createSlice({
name: "user",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
.addCase(createUserAsync.pending, (state) => {
state.status = "loading";
})
.addCase(createUserAsync.fulfilled, (state, action) => {
state.status = "idle";
state.loggedInUser = action.payload;
})
.addCase(checkUserAsync.pending, (state) => {
state.status = "loading";
})
.addCase(checkUserAsync.fulfilled, (state, action) => {
state.status = "idle";
state.loggedInUser = action.payload;
})
.addCase(checkUserAsync.rejected, (state, action) => {
state.status = "idle";
state.erorr = action.error;
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
.addCase(updateUserAsync.pending, (state) => {
state.status = "loading";
})
.addCase(updateUserAsync.fulfilled, (state, action) => {
state.status = "idle";
state.loggedInUser = action.payload;
})
.addCase(signOutAsync.pending, (state) => {
state.status = "loading";
})
.addCase(signOutAsync.fulfilled, (state, action) => {
state.status = "idle";
state.loggedInUser = null;
});
},
});

export const { increment } = counterSlice.actions;

export const selectCount = (state) => state.counter.value;
export const selectLoggedInUser = (state) => state.auth.loggedInUser;
export const selectError = (state) => state.auth.erorr;

export default counterSlice.reducer;
export default authSlice.reducer;
84 changes: 84 additions & 0 deletions src/features/auth/components/ForgotPassword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useForm } from "react-hook-form";
import { Link } from "react-router-dom";

export default function ForgotPassword() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();

console.log(errors);
return (
<>
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<img
className="mx-auto h-10 w-auto"
src="https://e7.pngegg.com/pngimages/282/123/png-clipart-retail-business-computer-icons-e-commerce-online-shopping-business-computer-network-angle.png"
alt="Your Company"
/>
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
Enter Email to Reset Password
</h2>
</div>

<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form
noValidate
className="space-y-6"
onSubmit={handleSubmit((data) => {
console.log(data);
// TODO: implementation on backend with email
})}
>
<div>
<label
htmlFor="email"
className="block text-sm font-medium leading-6 text-gray-900"
>
Email address
</label>
<div className="mt-2">
<input
id="email"
{...register("email", {
required: "email is required",
pattern: {
value: /\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/gi,
message: "email is not valid",
},
})}
type="email"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6"
/>
{errors.email && (
<p className="text-red-500">{errors.email.message}</p>
)}
</div>
</div>

<div>
<button
type="submit"
className="flex w-full justify-center rounded-md bg-gray-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
>
Log in
</button>
</div>
</form>

<p className="mt-10 text-center text-sm text-gray-500">
Send me back to{" "}
<Link
to="/login"
className="font-semibold leading-6 text-gray-600 hover:text-gray-500"
>
Login
</Link>
</p>
</div>
</div>
</>
);
}
16 changes: 16 additions & 0 deletions src/features/auth/components/LogOut.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect } from "react";
import { selectLoggedInUser, signOutAsync } from "../authSlice";
import { useDispatch, useSelector } from "react-redux";
import { Navigate } from "react-router-dom";

function LogOut() {
const dispatch = useDispatch();
const user = useSelector(selectLoggedInUser);
useEffect(() => {
dispatch(signOutAsync());
});
//but useEffect runs after render, so we have to delay
return <>{!user && <Navigate to="/login" replace={true}></Navigate>}</>;
}

export default LogOut;
110 changes: 81 additions & 29 deletions src/features/auth/components/Login.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
increment,
incrementAsync,
selectCount,
} from '../authSlice';
import { Link } from 'react-router-dom';
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { checkUserAsync, selectError, selectLoggedInUser } from "../authSlice";
import { Link, Navigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline"; // Import EyeSlashIcon for the toggle

export default function Login() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const error = useSelector(selectError);
const user = useSelector(selectLoggedInUser);
const [showPassword, setShowPassword] = useState(true);
const {
register,
handleSubmit,
formState: { errors },
} = useForm();

const togglePasswordVisibility = () => {
setShowPassword((showPassword) => !showPassword);
};

console.log(errors);
return (
<>

{user && <Navigate to="/" replace={true}></Navigate>}
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<img
className="mx-auto h-10 w-auto"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
src="https://e7.pngegg.com/pngimages/282/123/png-clipart-retail-business-computer-icons-e-commerce-online-shopping-business-computer-network-angle.png"
alt="Your Company"
/>
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
@@ -28,59 +37,102 @@ export default function Login() {
</div>

<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form className="space-y-6" action="#" method="POST">
<form
noValidate
className="space-y-6"
onSubmit={handleSubmit((data) => {
dispatch(
checkUserAsync({ email: data.email, password: data.password })
);
console.log(data);
})}
>
<div>
<label htmlFor="email" className="block text-sm font-medium leading-6 text-gray-900">
<label
htmlFor="email"
className="block text-sm font-medium leading-6 text-gray-900"
>
Email address
</label>
<div className="mt-2">
<input
id="email"
name="email"
{...register("email", {
required: "email is required",
pattern: {
value: /\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/gi,
message: "email is not valid",
},
})}
type="email"
autoComplete="email"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6"
/>
{errors.email && (
<p className="text-red-500">{errors.email.message}</p>
)}
</div>
</div>

<div>
<div className="flex items-center justify-between">
<label htmlFor="password" className="block text-sm font-medium leading-6 text-gray-900">
<label
htmlFor="password"
className="block text-sm font-medium leading-6 text-gray-900"
>
Password
</label>
<div className="text-sm">
<a href="#" className="font-semibold text-indigo-600 hover:text-indigo-500">
<Link
to="/forgot-password"
className="font-semibold text-gray-600 hover:text-gray-500"
>
Forgot password?
</a>
</Link>
</div>
</div>
<div className="mt-2">
<div className="mt-2 relative">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
{...register("password", {
required: "password is required",
})}
type={showPassword ? "password" : "text"}
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6 pr-10"
/>
<button
type="button"
className=" inset-y-0 right-0 pr-3 absolute flex items-center text-black-400"
onClick={togglePasswordVisibility}
>
{showPassword ? (
<EyeSlashIcon className="h-6 w-6" aria-hidden="true" />
) : (
<EyeIcon className="h-6 w-6" aria-hidden="true" />
)}
</button>
{errors.password && (
<p className="text-red-500">{errors.password.message}</p>
)}
{error && <p className="text-red-500">{error.message}</p>}
</div>
</div>

<div>
<button
type="submit"
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
className="flex w-full justify-center rounded-md bg-gray-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
>
Log in
</button>
</div>
</form>

<p className="mt-10 text-center text-sm text-gray-500">
Not a member?{' '}
<Link to="/signup" className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">
Not a member?{" "}
<Link
to="/signup"
className="font-semibold leading-6 text-gray-600 hover:text-gray-500"
>
Create an Account
</Link>
</p>
12 changes: 12 additions & 0 deletions src/features/auth/components/Protected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useSelector } from "react-redux";
import { Navigate } from "react-router-dom";
import { selectLoggedInUser } from "../authSlice";
function Protected({ children }) {
const user = useSelector(selectLoggedInUser);
if (!user) {
return <Navigate to="/login" replace={true}></Navigate>;
}
return children;
}

export default Protected;
15 changes: 15 additions & 0 deletions src/features/auth/components/ProtectedAdmin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useSelector } from "react-redux";
import { Navigate } from "react-router-dom";
import { selectLoggedInUser } from "../authSlice";
function ProtectedAdmin({ children }) {
const user = useSelector(selectLoggedInUser);
if (!user) {
return <Navigate to="/login" replace={true}></Navigate>;
}
if (user && user.role !== "admin") {
return <Navigate to="/" replace={true}></Navigate>;
}
return children;
}

export default ProtectedAdmin;
124 changes: 90 additions & 34 deletions src/features/auth/components/Signup.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
increment,
incrementAsync,
selectCount,
} from '../authSlice';
import { Link } from 'react-router-dom';

import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { selectLoggedInUser, createUserAsync } from "../authSlice";
import { Link, Navigate } from "react-router-dom";
import { useForm } from "react-hook-form";
export default function Signup() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const user = useSelector(selectLoggedInUser);
const {
register,
handleSubmit,
formState: { errors },
} = useForm();

console.log(errors);

return (
<>

{user && <Navigate to="/" replace={true}></Navigate>}
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<img
className="mx-auto h-10 w-auto"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
src="https://e7.pngegg.com/pngimages/282/123/png-clipart-retail-business-computer-icons-e-commerce-online-shopping-business-computer-network-angle.png"
alt="Your Company"
/>
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
@@ -28,77 +30,131 @@ export default function Signup() {
</div>

<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form className="space-y-6" action="#" method="POST">
<form
noValidate
className="space-y-6"
onSubmit={handleSubmit((data) => {
dispatch(
createUserAsync({
email: data.email,
password: data.password,
addresses: [],
role: "user",
// TODO:this role can be directly given to the backend
})
);
console.log(data);
})}
>
<div>
<label htmlFor="email" className="block text-sm font-medium leading-6 text-gray-900">
<label
htmlFor="email"
className="block text-sm font-medium leading-6 text-gray-900"
>
Email address
</label>
<div className="mt-2">
<input
id="email"
name="email"
{...register("email", {
required: "email is required",
pattern: {
value: /\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/gi,
message: "email is not valid",
},
})}
type="email"
autoComplete="email"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6"
/>
{errors.email && (
<p className="text-red-500">{errors.email.message}</p>
)}
</div>
</div>

<div>
<div className="flex items-center justify-between">
<label htmlFor="password" className="block text-sm font-medium leading-6 text-gray-900">
<label
htmlFor="password"
className="block text-sm font-medium leading-6 text-gray-900"
>
Password
</label>
<div className="text-sm">
<a href="#" className="font-semibold text-indigo-600 hover:text-indigo-500">
<Link
to="/forgot-paasword"
className="font-semibold text-gray-600 hover:text-gray-500"
>
Forgot password?
</a>
</Link>
</div>
</div>
<div className="mt-2">
<input
id="password"
name="password"
{...register("password", {
required: "password is required",
pattern: {
value:
/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/gm,
message: `- at least 8 characters\n
- must contain at least 1 uppercase letter, 1 lowercase letter, and 1 number\n
- Can contain special characters`,
},
})}
type="password"
autoComplete="current-password"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6"
/>
{errors.password && (
<p className="text-red-500">{errors.password.message}</p>
)}
</div>
</div>

<div>
<div className="flex items-center justify-between">
<label htmlFor="password" className="block text-sm font-medium leading-6 text-gray-900">
<label
htmlFor="password"
className="block text-sm font-medium leading-6 text-gray-900"
>
Confirm Password
</label>

</div>
<div className="mt-2">
<input
id="confirm-password"
name="confirm-password"
id="confirmPassword"
{...register("confirmPassword", {
required: "confirm password is required",
validate: (value, formValues) =>
value === formValues.password || "password not matching",
})}
type="password"
required
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6"
/>
{errors.confirmPassword && (
<p className="text-red-500">
{errors.confirmPassword.message}
</p>
)}
</div>
</div>

<div>
<button
type="submit"
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
className="flex w-full justify-center rounded-md bg-gray-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
>
Sign Up
</button>
</div>
</form>

<p className="mt-10 text-center text-sm text-gray-500">
Already a Member?{' '}
<Link to="/login" className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">
Already a Member?{" "}
<Link
to="/login"
className="font-semibold leading-6 text-gray-600 hover:text-gray-500"
>
Log In
</Link>
</p>
115 changes: 60 additions & 55 deletions src/features/cart/Cart.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,35 @@
import React, { useState, Fragment } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, incrementAsync, selectCount } from './cartSlice';
import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { Link } from 'react-router-dom';

const products = [
{
id: 1,
name: 'Throwback Hip Bag',
href: '#',
color: 'Salmon',
price: '$90.00',
quantity: 1,
imageSrc:
'https://tailwindui.com/img/ecommerce-images/shopping-cart-page-04-product-01.jpg',
imageAlt:
'Salmon orange fabric pouch with match zipper, gray zipper pull, and adjustable hip belt.',
},
{
id: 2,
name: 'Medium Stuff Satchel',
href: '#',
color: 'Blue',
price: '$32.00',
quantity: 1,
imageSrc:
'https://tailwindui.com/img/ecommerce-images/shopping-cart-page-04-product-02.jpg',
imageAlt:
'Front of satchel with blue canvas body, black straps and handle, drawstring top, and front zipper pouch.',
},
// More products...
];
import React, { useState, Fragment } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
deleteItemFromCartAsync,
selectItems,
updateCartAsync,
} from "./cartSlice";
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { Link } from "react-router-dom";
import { Navigate } from "react-router-dom";
import { discountedPrice } from "../../app/constants";

export default function Cart() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const [open, setOpen] = useState(true);
const items = useSelector(selectItems);
const totalAmount = items.reduce(
(amount, item) => discountedPrice(item) * item.quantity + amount,
0
);
const totalItems = items.reduce((total, item) => item.quantity + total, 0);
const handleQuantity = (e, item) => {
dispatch(updateCartAsync({ ...item, quantity: +e.target.value }));
};

const handleRemove = (e, id) => {
dispatch(deleteItemFromCartAsync(id));
};
return (
<>
{!items.length && <Navigate to="/" replace={true}></Navigate>}
<div>
<div className="mx-auto mt-12 bg-white max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="border-t border-gray-200 px-4 py-6 sm:px-6">
@@ -48,12 +38,12 @@ export default function Cart() {
</h1>
<div className="flow-root">
<ul role="list" className="-my-6 divide-y divide-gray-200">
{products.map((product) => (
<li key={product.id} className="flex py-6">
{items.map((item) => (
<li key={item.id} className="flex py-6">
<div className="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200">
<img
src={product.imageSrc}
alt={product.imageAlt}
src={item.thumbnail}
alt={item.title}
className="h-full w-full object-cover object-center"
/>
</div>
@@ -62,12 +52,12 @@ export default function Cart() {
<div>
<div className="flex justify-between text-base font-medium text-gray-900">
<h3>
<a href={product.href}>{product.name}</a>
<a href={item.href}>{item.title}</a>
</h3>
<p className="ml-4">{product.price}</p>
<p className="ml-4">${discountedPrice(item)}</p>
</div>
<p className="mt-1 text-sm text-gray-500">
{product.color}
{item.brand}
</p>
</div>
<div className="flex flex-1 items-end justify-between text-sm">
@@ -78,16 +68,27 @@ export default function Cart() {
>
Qty
</label>
<select>
<select
onChange={(e) => {
handleQuantity(e, item);
}}
value={item.quantity}
>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>

<div className="flex">
<button
onClick={(e) => {
handleRemove(e, item.id);
}}
type="button"
className="font-medium text-indigo-600 hover:text-indigo-500"
className="font-medium text-gray-600 hover:text-gray-500"
>
Remove
</button>
@@ -101,17 +102,21 @@ export default function Cart() {
</div>

<div className="border-t border-gray-200 px-4 py-6 sm:px-6">
<div className="flex justify-between text-base font-medium text-gray-900">
<div className="flex justify-between my-2 text-base font-medium text-gray-900">
<p>Subtotal</p>
<p>$262.00</p>
<p>${totalAmount}</p>
</div>
<div className="flex justify-between my-2 text-base font-medium text-gray-900">
<p>Total Items in Cart</p>
<p>{totalItems} Items</p>
</div>
<p className="mt-0.5 text-sm text-gray-500">
Shipping and taxes calculated at checkout.
</p>
<div className="mt-6">
<Link
<Link
to="/checkout"
className="flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-indigo-700"
className="flex items-center justify-center rounded-md border border-transparent bg-gray-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-gray-700"
>
Checkout
</Link>
@@ -120,13 +125,13 @@ export default function Cart() {
<p>
or
<Link to="/">
<button
type="button"
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Continue Shopping
<span aria-hidden="true"> &rarr;</span>
</button>
<button
type="button"
className="font-medium text-gray-600 hover:text-gray-500"
>
Continue Shopping
<span aria-hidden="true"> &rarr;</span>
</button>
</Link>
</p>
</div>
65 changes: 58 additions & 7 deletions src/features/cart/cartAPI.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,59 @@
export function fetchCount(amount = 1) {
return new Promise(async (resolve) =>{
const response = await fetch('http://localhost:8080')
const data = await response.json()
resolve({data})
}
);
// Create
export function addToCart(item) {
return new Promise(async (resolve) => {
const response = await fetch("http://localhost:8080/cart", {
method: "POST",
body: JSON.stringify(item),
headers: { "content-type": "application/json" },
});
const data = await response.json();
// TODO:on server it will only return some info of user (not password)
resolve({ data });
});
}
// Read
export function fetchItemsByUserId(userId) {
return new Promise(async (resolve) => {
// TODO: we will not hard coded server url here...
const response = await fetch("http://localhost:8080/cart?user=" + userId);
const data = await response.json();
resolve({ data });
});
}
// Update
export function updateCart(update) {
return new Promise(async (resolve) => {
const response = await fetch("http://localhost:8080/cart/" + update.id, {
method: "PATCH",
body: JSON.stringify(update),
headers: { "content-type": "application/json" },
});
const data = await response.json();
// TODO:on server it will only return some info of user (not password)
resolve({ data });
});
}
// Delete
export function deleteItemFromCart(itemId) {
return new Promise(async (resolve) => {
const response = await fetch("http://localhost:8080/cart/" + itemId, {
method: "DELETE",
headers: { "content-type": "application/json" },
});
const data = await response.json();
// TODO:on server it will only return some info of user (not password)
resolve({ data: { id: itemId } });
});
}
// Reset Cart
export function resetCart(userId) {
// get all the item of user's cart and delete rach
return new Promise(async (resolve) => {
const response = await fetchItemsByUserId(userId);
const items = response.data;
for (let item of items) {
await deleteItemFromCart(item.id);
}
resolve({ status: "success" });
});
}
114 changes: 90 additions & 24 deletions src/features/cart/cartSlice.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,108 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './cartAPI';
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import {
addToCart,
fetchItemsByUserId,
updateCart,
deleteItemFromCart,
resetCart,
} from "./cartAPI";

const initialState = {
value: 0,
status: 'idle',
items: [],
status: "idle",
};

export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount) => {
const response = await fetchCount(amount);
export const addToCartAsync = createAsyncThunk(
"cart/addToCart",
async (item) => {
const response = await addToCart(item);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const fetchItemsByUserIdAsync = createAsyncThunk(
"cart/fetchItemsByUserId",
async (userId) => {
const response = await fetchItemsByUserId(userId);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const updateCartAsync = createAsyncThunk(
"cart/updateCart",
async (update) => {
const response = await updateCart(update);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const deleteItemFromCartAsync = createAsyncThunk(
"cart/deleteItemFromCart",
async (itemId) => {
const response = await deleteItemFromCart(itemId);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const resetCartAsync = createAsyncThunk(
"cart/resetCart",
async (userId) => {
const response = await resetCart(userId);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);

export const counterSlice = createSlice({
name: 'counter',
export const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
.addCase(addToCartAsync.pending, (state) => {
state.status = "loading";
})
.addCase(addToCartAsync.fulfilled, (state, action) => {
state.status = "idle";
state.items.push(action.payload);
})
.addCase(fetchItemsByUserIdAsync.pending, (state) => {
state.status = "loading";
})
.addCase(fetchItemsByUserIdAsync.fulfilled, (state, action) => {
state.status = "idle";
state.items = action.payload;
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
.addCase(updateCartAsync.pending, (state) => {
state.status = "loading";
})
.addCase(updateCartAsync.fulfilled, (state, action) => {
state.status = "idle";
const index = state.items.findIndex(
(item) => item.id === action.payload.id
);
state.items[index] = action.payload;
})
.addCase(deleteItemFromCartAsync.pending, (state) => {
state.status = "loading";
})
.addCase(deleteItemFromCartAsync.fulfilled, (state, action) => {
state.status = "idle";
const index = state.items.findIndex(
(item) => item.id === action.payload.id
);
state.items.splice(index, 1);
})
.addCase(resetCartAsync.pending, (state) => {
state.status = "loading";
})
.addCase(resetCartAsync.fulfilled, (state, action) => {
state.status = "idle";
state.items = [];
});
},
});

export const { increment } = counterSlice.actions;

export const selectCount = (state) => state.counter.value;
export const selectItems = (state) => state.cart.items;

export default counterSlice.reducer;
export default cartSlice.reducer;
81 changes: 81 additions & 0 deletions src/features/common/Pagination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { ITEMS_PER_PAGE } from "../../app/constants";

export default function Pagination({ page, setPage, handlePage, totalItems }) {
const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
return (
<div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div className="flex flex-1 justify-between sm:hidden">
<a
onClick={() => handlePage(page > 1 ? page - 1 : page)}
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
>
Previous
</a>
<div
onClick={() => handlePage(page < totalPages ? page + 1 : page)}
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
>
Next
</div>
</div>
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing {/* (3-1) * 6 + 1 =13 */}
<span className="font-medium">
{(page - 1) * ITEMS_PER_PAGE + 1}
</span>{" "}
{/* 3 * 6 = 18 */}
to{" "}
<span className="font-medium">
{page * ITEMS_PER_PAGE > totalItems
? totalItems
: page * ITEMS_PER_PAGE}{" "}
</span>{" "}
of <span className="font-medium">{totalItems}</span> results
</p>
</div>
<div>
<nav
className="isolate inline-flex -space-x-px rounded-md shadow-sm"
aria-label="Pagination"
>
<div
onClick={() => handlePage(page > 1 ? page - 1 : page)}
className="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
>
<span className="sr-only">Previous</span>
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
</div>
{/* Current: "z-10 bg-gray-600 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600", Default: "text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-offset-0" */}
{Array.from({ length: totalPages }).map(
//extracting all the indexes of array
(el, index) => (
<div
onClick={() => handlePage(index + 1)}
aria-current="page"
className={`relative z-10 inline-flex items-center ${
index + 1 === page
? "bg-gray-600 text-white"
: "text-gray-400 "
} px-4 py-2 text-sm font-semibold cursor-pointer focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600`}
>
{index + 1}
</div>
)
)}

<div
onClick={() => handlePage(page < totalPages ? page + 1 : page)}
className="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
>
<span className="sr-only">Next</span>
<ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
</div>
</nav>
</div>
</div>
</div>
);
}
142 changes: 77 additions & 65 deletions src/features/navbar/Navbar.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
import { Fragment } from 'react';
import { Disclosure, Menu, Transition } from '@headlessui/react';
import { Fragment } from "react";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import {
Bars3Icon,
ShoppingCartIcon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import { Link } from 'react-router-dom';
} from "@heroicons/react/24/outline";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectItems } from "../cart/cartSlice";
import { fetchLoggedInUser } from "../user/userAPI";
import { selectLoggedInUser } from "../auth/authSlice";

const user = {
name: 'Tom Cook',
email: 'tom@example.com',
imageUrl:
'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
};
const navigation = [
{ name: 'Dashboard', href: '#', current: true },
{ name: 'Team', href: '#', current: false },
{ name: "Dashboard", link: "#", user: true },
{ name: "Team", link: "#", user: true },
{ name: "Admin", link: "/admin", admin: true },
{ name: "Orders", link: "/admin/orders", admin: true },
];
const userNavigation = [
{ name: 'Your Profile', href: '#' },
{ name: 'Settings', href: '#' },
{ name: 'Sign out', href: '#' },
{ name: "My Profile", link: "/profile" },
{ name: "My Orders", link: "/orders" },
{ name: "Sign out", link: "/logout" },
];

function classNames(...classes) {
return classes.filter(Boolean).join(' ');
return classes.filter(Boolean).join(" ");
}

function NavBar({ children }) {
const items = useSelector(selectItems);
const user = useSelector(selectLoggedInUser);
return (
<>
<div className="min-h-full">
@@ -38,29 +40,33 @@ function NavBar({ children }) {
<div className="flex h-16 items-center justify-between">
<div className="flex items-center">
<div className="flex-shrink-0">
<img
className="h-8 w-8"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500"
alt="Your Company"
/>
<Link to="/">
<img
className="h-8 w-8"
src="https://e7.pngegg.com/pngimages/282/123/png-clipart-retail-business-computer-icons-e-commerce-online-shopping-business-computer-network-angle.png"
alt="Your Company"
/>
</Link>
</div>
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className={classNames(
item.current
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
'rounded-md px-3 py-2 text-sm font-medium'
)}
aria-current={item.current ? 'page' : undefined}
>
{item.name}
</a>
))}
{navigation.map((item) =>
item[user.role] ? (
<Link
key={item.name}
to={item.link}
className={classNames(
item.current
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white",
"rounded-md px-3 py-2 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
{item.name}
</Link>
) : null
)}
</div>
</div>
</div>
@@ -78,9 +84,11 @@ function NavBar({ children }) {
/>
</button>
</Link>
<span className="inline-flex items-center rounded-md mb-7 -ml-3 bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10">
3
</span>
{items.length > 0 && (
<span className="inline-flex items-center rounded-md mb-7 -ml-3 bg-yellow-50 px-2 py-1 text-xs font-medium text-dark-700 ring-1 ring-inset ring-yellow-600/10">
{items.length}
</span>
)}

{/* Profile dropdown */}
<Menu as="div" className="relative ml-3">
@@ -107,15 +115,15 @@ function NavBar({ children }) {
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<a
href={item.href}
<Link
to={item.link}
className={classNames(
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm text-gray-700'
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700"
)}
>
{item.name}
</a>
</Link>
)}
</Menu.Item>
))}
@@ -146,22 +154,24 @@ function NavBar({ children }) {

<Disclosure.Panel className="md:hidden">
<div className="space-y-1 px-2 pb-3 pt-2 sm:px-3">
{navigation.map((item) => (
<Disclosure.Button
key={item.name}
as="a"
href={item.href}
className={classNames(
item.current
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
'block rounded-md px-3 py-2 text-base font-medium'
)}
aria-current={item.current ? 'page' : undefined}
>
{item.name}
</Disclosure.Button>
))}
{navigation.map((item) =>
item[user.role] ? (
<Disclosure.Button
key={item.name}
as={Link}
to={item.link}
className={classNames(
item.current
? "bg-gray-900 text-white"
: "text-gray-300 hover:bg-gray-700 hover:text-white",
"block rounded-md px-3 py-2 text-base font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
{item.name}
</Disclosure.Button>
) : null
)}
</div>
<div className="border-t border-gray-700 pb-3 pt-4">
<div className="flex items-center px-5">
@@ -191,16 +201,18 @@ function NavBar({ children }) {
/>
</button>
</Link>
<span className="inline-flex items-center rounded-md bg-red-50 mb-7 -ml-3 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10">
3
</span>
{items.length > 0 && (
<span className="inline-flex items-center rounded-md bg-red-50 mb-7 -ml-3 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10">
{items.length}
</span>
)}
</div>
<div className="mt-3 space-y-1 px-2">
{userNavigation.map((item) => (
<Disclosure.Button
key={item.name}
as="a"
href={item.href}
as={Link} // change as=a to as=Link to use Disclosure.Button as a Link (react-router-dom)
to={item.link} // {link: "/orders"}
className="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-700 hover:text-white"
>
{item.name}
13 changes: 13 additions & 0 deletions src/features/order/Order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, incrementAsync } from "./orderSlice";

export default function Order() {
const dispatch = useDispatch();

return (
<div>
<div></div>
</div>
);
}
37 changes: 37 additions & 0 deletions src/features/order/orderAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function createOrder(order) {
return new Promise(async (resolve) => {
const response = await fetch("http://localhost:8080/orders", {
method: "POST",
body: JSON.stringify(order),
headers: { "content-type": "application/json" },
});
const data = await response.json();

resolve({ data });
});
}
export function updateOrder(order) {
return new Promise(async (resolve) => {
const response = await fetch("http://localhost:8080/orders/" + order.id, {
method: "PATCH",
body: JSON.stringify(order),
headers: { "content-type": "application/json" },
});
const data = await response.json();

resolve({ data });
});
}
export function fetchAllOrders(pagination) {
let queryString = "";
for (let key in pagination) {
queryString += `${key}=${pagination[key]}&`; //_page:3
}
return new Promise(async (resolve) => {
// TODO: we will not hard coded server url here...
const response = await fetch("http://localhost:8080/orders?" + queryString);
const data = await response.json();

resolve({ data: { orders: data.data, totalOrders: data.items } });
});
}
82 changes: 82 additions & 0 deletions src/features/order/orderSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { createOrder, fetchAllOrders, updateOrder } from "./orderAPI";
import { resetCart } from "../cart/cartAPI";

const initialState = {
orders: [],
status: "idle",
currentOrder: null,
totalOrders: 0,
};

export const createOrderAsync = createAsyncThunk(
"order/createOrder",
async (order) => {
const response = await createOrder(order);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const updateOrderAsync = createAsyncThunk(
"order/updateOrder",
async (order) => {
const response = await updateOrder(order);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const fetchAllOrdersAsync = createAsyncThunk(
"order/fetchAllOrders",
async (pagination) => {
const response = await fetchAllOrders(pagination);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);

export const orderSlice = createSlice({
name: "order",
initialState,
reducers: {
resetOrder: (state) => {
state.currentOrder = null;
},
},
extraReducers: (builder) => {
builder
.addCase(createOrderAsync.pending, (state) => {
state.status = "loading";
})
.addCase(createOrderAsync.fulfilled, (state, action) => {
state.status = "idle";
state.orders.push(action.payload);
state.currentOrder = action.payload;
})
.addCase(updateOrderAsync.pending, (state) => {
state.status = "loading";
})
.addCase(updateOrderAsync.fulfilled, (state, action) => {
state.status = "idle";
const index = state.orders.findIndex(
(order) => order.id === action.payload.id
);
state.orders[index] = action.payload;
})
.addCase(fetchAllOrdersAsync.pending, (state) => {
state.status = "loading";
})
.addCase(fetchAllOrdersAsync.fulfilled, (state, action) => {
state.status = "idle";
state.orders = action.payload.orders;
state.totalOrders = action.payload.totalOrders;
});
},
});

export const { resetOrder } = orderSlice.actions;

export const selectCurrentOrder = (state) => state.order.currentOrder;
export const selectOrders = (state) => state.order.orders;
export const selectTotalOrders = (state) => state.order.totalOrders;

export default orderSlice.reducer;
Loading