diff --git a/.env b/.env new file mode 100644 index 0000000..210d000 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +VITE_API_TOKEN=98346afa737a70b6f720daf2db5372457c0a5a5717018769fc2e530058193c55cc47ec4229c18cbba173745e3a3b60b59ea17c8ffd7f4602a76569d610d0e9affffdca9975996a944acc3864ee14cd562c852fab0932472935d45a3c71b3b43cba0a12cfd3296c50c5087e6eab4afb05d8cde6158b6b61d5bde8b73ac470fa77 +VITE_BASE_URL=http://localhost:1337/api +VITE_IMAGE_URL=http://localhost:1337 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a828645 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_API_TOKEN= +VITE_BASE_URL= +VITE_IMAGE_URL= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 05d70fd..06e4d1e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,41 +1,50 @@ { + "root": true, "env": { "browser": true, - "es2021": true, + "es2020": true, "node": true, "jest": true }, - "plugins": ["react", "unused-imports"], "extends": [ "eslint:recommended", - "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", "plugin:prettier/recommended", "prettier" ], - "overrides": [], - "parser": "@babel/eslint-parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module", - "requireConfigFile": false, - "babelOptions": { - "presets": ["@babel/preset-react"] - } - }, + "ignorePatterns": ["dist", ".eslintrc.cjs"], + "parser": "@typescript-eslint/parser", + "plugins": ["react-refresh", "unused-imports", "simple-import-sort"], + // "overrides": [], + // "parserOptions": { + // "ecmaVersion": "latest", + // "sourceType": "module", + // "requireConfigFile": false, + // "babelOptions": { + // "presets": ["@babel/preset-react"] + // } + // }, "rules": { + "react-refresh/only-export-components": [ + "warn", + { "allowConstantExport": true } + ], "semi": "warn", - "no-unused-vars": "off", + "no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }], + "@typescript-eslint/explicit-module-boundary-types": "off", "no-console": "warn", "react/prop-types": "off", "react/react-in-jsx-scope": "off", "react/no-unescaped-entities": "off", "react/display-name": "off", - "react/jsx-curly-brace-presence": [ - "warn", - { "props": "never", "children": "never" } - ], + // "react/tsx-curly-brace-presence": [ + // "warn", + // { "props": "never", "children": "never" } + // ], //*=========== Unused Import =========== + "@typescript-eslint/no-unused-vars": "off", "unused-imports/no-unused-imports": "warn", "unused-imports/no-unused-vars": [ "warn", @@ -45,6 +54,54 @@ "args": "after-used", "argsIgnorePattern": "^_" } + ], + "simple-import-sort/exports": "warn", + "simple-import-sort/imports": [ + "warn", + { + "groups": [ + // Side effect imports. + ["^\\u0000"], + + // Packages. + // Things that start with a letter (or digit or underscore), or `@` followed by a letter. + ["^@?\\w"], + + // components + ["^@components"], + + // pages + ["^@pages"], + + // context & api + ["^@context", "^@store", "^@api"], + + // hooks & utils + ["^@hooks", "^@utils"], + + // Other imports + // ['^@/'], + ["^@public", "^@assets"], + + // {s}css files + ["^.+\\.s?css$"], + + // relative paths up until 3 level + [ + "^\\./?$", + "^\\.(?!/?$)", + "^\\.\\./?$", + "^\\.\\.(?!/?$)", + "^\\.\\./\\.\\./?$", + "^\\.\\./\\.\\.(?!/?$)", + "^\\.\\./\\.\\./\\.\\./?$", + "^\\.\\./\\.\\./\\.\\.(?!/?$)" + ], + + ["^types"], + ["^"] + ] + } ] }, "settings": { diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index ef3a671..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 2, - "semi": true, - "singleQuote": true, - "jsxSingleQuote": true -} diff --git a/index.html b/index.html new file mode 100644 index 0000000..4dd1ce1 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + Urban Fashion + + + + +
+ + + diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 5875dc5..0000000 --- a/jsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": "src" - }, - "include": ["src"] -} diff --git a/package.json b/package.json index fbd36ba..7c57a41 100644 --- a/package.json +++ b/package.json @@ -1,71 +1,69 @@ { "name": "urban-fashion-shop", - "version": "1.1.4", "private": true, + "version": "1.1.4", + "type": "module", "engines": { "node": ">= 16", "yarn": ">= 1.22.0", "npm": "please-use-yarn" }, - "dependencies": { - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^14.4.3", - "axios": "^1.4.0", - "flowbite": "^1.7.0", - "flowbite-react": "^0.4.11", - "jwt-decode": "^3.1.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-icons": "^4.10.1", - "react-router-dom": "^6.14.1", - "react-scripts": "5.0.1", - "react-select": "^5.7.4", - "react-select-country-list": "^2.2.3", - "react-toggle-dark-mode": "^1.1.1", - "web-vitals": "^3.4.0" - }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject", - "lint": "eslint .", + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint . --fix && yarn format", "lint:strict": "eslint --max-warnings=0 .", - "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc.json", + "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./prettier.config.cjs", "format:check": "prettier -c './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc.json", "test-coverage": "yarn test --coverage --watchAll --collectCoverageFrom='src/components/**/*.{ts,tsx,js,jsx}' --collectCoverageFrom='!src/components/**/*.{types,stories,constants,test,spec}.{ts,tsx,js,jsx}'", "prepare": "husky install" }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "dependencies": { + "@hookform/resolvers": "^3.1.1", + "@reduxjs/toolkit": "^1.9.5", + "@stripe/react-stripe-js": "^2.1.1", + "@stripe/stripe-js": "^2.0.0", + "@uiball/loaders": "^1.3.0", + "axios": "^1.4.0", + "clsx": "^2.0.0", + "flowbite": "^1.8.1", + "flowbite-react": "^0.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.2", + "react-hot-toast": "^2.4.1", + "react-icons": "^4.10.1", + "react-redux": "^8.1.2", + "react-router-dom": "^6.14.2", + "react-toggle-dark-mode": "^1.1.1", + "redux-persist": "^6.0.0", + "zod": "^3.21.4" }, "devDependencies": { - "@babel/eslint-parser": "^7.22.9", - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@babel/preset-react": "^7.22.5", + "@types/node": "^20.4.6", + "@types/react": "^18.2.18", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.2.1", + "@typescript-eslint/parser": "^6.2.1", + "@vitejs/plugin-react": "^4.0.4", "autoprefixer": "^10.4.14", - "eslint": "^8.45.0", - "eslint-config-prettier": "^8.8.0", + "eslint": "^8.46.0", + "eslint-config-prettier": "^8.9.0", "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^3.0.0", "husky": "^8.0.3", "lint-staged": "^13.2.3", - "postcss": "^8.4.26", + "postcss": "^8.4.27", "prettier": "^3.0.0", "prettier-plugin-tailwindcss": "^0.4.1", - "tailwindcss": "^3.3.3" + "tailwindcss": "^3.3.3", + "typescript": "^5.1.6", + "vite": "^4.4.8" }, "lint-staged": { "src/**/*.{js,jsx,ts,tsx}": [ diff --git a/postcss.config.js b/postcss.config.cjs similarity index 50% rename from postcss.config.js rename to postcss.config.cjs index c38b7a6..75bb2a6 100644 --- a/postcss.config.js +++ b/postcss.config.cjs @@ -3,7 +3,7 @@ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, - // eslint-disable-next-line no-undef - ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}), + // // eslint-disable-next-line no-undef + // ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}), }, }; diff --git a/prettier.config.cjs b/prettier.config.cjs new file mode 100644 index 0000000..1cf2002 --- /dev/null +++ b/prettier.config.cjs @@ -0,0 +1,12 @@ +const config = { + trailingComma: 'es5', + tabWidth: 2, + semi: true, + singleQuote: true, + jsxSingleQuote: true, + tailwindConfig: './tailwind.config.cjs', + tailwindFunctions: ['clsx'], + plugins: ['prettier-plugin-tailwindcss'], +}; + +module.exports = config; diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index a11777c..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/index.html b/public/index.html deleted file mode 100644 index b88ae10..0000000 --- a/public/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - Urban Shop - - - - - -
- - diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 080d6c7..0000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index e9e57dc..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 2827f5e..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Routes, Route } from 'react-router-dom'; - -import Login from 'pages/LoginPage'; -import Register from 'pages/RegisterPage'; -import Home from 'pages/HomePage'; -import Shop from 'pages/ProductPage'; -import Cart from 'pages/CartPage'; -import ProductDetails from 'pages/ProductDetailsPage'; -import MyAccount from 'pages/MyAccountPage'; -import RequireAuth from 'components/Auth/RequireAuth'; -import Wishlist from 'components/Wishlist/Wishlist'; -import Layout from 'components/Layout/Layout'; -import { useAuth } from 'hooks/useStoreContext'; - -const App = () => { - const { isAuth } = useAuth(); - - return ( - - }> - } /> - } /> - } /> - } /> - : } /> - : } /> - }> - } /> - } /> - - - - ); -}; - -export default App; diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..8a25928 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,36 @@ +import { Route, Routes } from 'react-router-dom'; + +import RequireAuth from '@components/Auth/RequireAuth'; +import Layout from '@components/Layout/Layout'; +import Wishlist from '@components/Wishlist/Wishlist'; + +import Cart from '@pages/CartPage'; +import Home from '@pages/HomePage'; +import MyAccount from '@pages/MyAccountPage'; +import ProductDetails from '@pages/ProductDetailsPage'; +import Shop from '@pages/ProductPage'; +import SignIn from '@pages/SignInPage'; +import SignUp from '@pages/SignUpPage'; + +import { useAppSelector } from '@hooks/useReduxT'; + +export default function App() { + const isAuth = useAppSelector((state) => state.auth.isAuthenticated); + + return ( + + }> + } /> + } /> + } /> + } /> + : } /> + : } /> + }> + } /> + } /> + + + + ); +} diff --git a/src/api/api.tsx b/src/api/api.tsx new file mode 100644 index 0000000..fb7bce7 --- /dev/null +++ b/src/api/api.tsx @@ -0,0 +1,104 @@ +import axios, { AxiosResponse, GenericAbortSignal } from 'axios'; + +import { + ICategoriesData, + ILogin, + INewProductToCart, + IProductData, + IProductsData, + IRegister, + IUser, +} from 'types/types'; + +export const url = axios.create({ + baseURL: import.meta.env.VITE_BASE_URL, + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Authorization: `bearer ${import.meta.env.VITE_API_TOKEN}`, + }, +}); + +export async function register( + { username, email, password }: IRegister, + signal?: GenericAbortSignal | undefined +) { + const res: AxiosResponse = await url.post( + '/auth/local/register', + { + username, + email, + password, + }, + { + signal, + } + ); + + return res; +} + +export async function login( + { email, password }: ILogin, + signal?: GenericAbortSignal | undefined +) { + const res: AxiosResponse = await url.post( + '/auth/local', + { + password, + identifier: email, + }, + { + signal, + } + ); + + return res; +} + +export async function getProducts(signal?: GenericAbortSignal | undefined) { + const res: AxiosResponse = await url.get( + '/products?populate=*', + { + signal, + } + ); + return res; +} + +export async function getProduct( + productId: string, + signal?: GenericAbortSignal | undefined +) { + const res: AxiosResponse = await url.get( + `/products/${productId}?populate=*`, + { + signal, + } + ); + return res; +} + +export async function getCategories(signal?: GenericAbortSignal | undefined) { + const res: AxiosResponse = await url.get('/categories', { + signal, + }); + return res; +} + +export async function postPayment( + products: INewProductToCart[], + signal?: GenericAbortSignal | undefined +) { + const res: AxiosResponse<{ + stripeSession: { + id: string; + }; + }> = await url.post( + '/orders', + { products }, + { + signal, + } + ); + return res; +} diff --git a/src/assets/image/logo/logo-no-background.svg b/src/assets/image/logo/logo-no-background.svg new file mode 100644 index 0000000..4ba66ec --- /dev/null +++ b/src/assets/image/logo/logo-no-background.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/image/user.svg b/src/assets/image/user.svg new file mode 100644 index 0000000..83a200f --- /dev/null +++ b/src/assets/image/user.svg @@ -0,0 +1,27 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Auth/RequireAuth.jsx b/src/components/Auth/RequireAuth.jsx deleted file mode 100644 index 15f8b28..0000000 --- a/src/components/Auth/RequireAuth.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { useLocation, Navigate, Outlet } from 'react-router-dom'; - -import { useAuth } from 'hooks/useStoreContext'; - -const RequireAuth = () => { - const { isAuth } = useAuth(); - const location = useLocation(); - - return isAuth ? ( - - ) : ( - - ); -}; - -export default RequireAuth; diff --git a/src/components/Auth/RequireAuth.tsx b/src/components/Auth/RequireAuth.tsx new file mode 100644 index 0000000..24f102b --- /dev/null +++ b/src/components/Auth/RequireAuth.tsx @@ -0,0 +1,14 @@ +import { Navigate, Outlet, useLocation } from 'react-router-dom'; + +import { useAppSelector } from '@hooks/useReduxT'; + +export default function RequireAuth() { + const isAuth = useAppSelector((state) => state.auth.isAuthenticated); + const location = useLocation(); + + return isAuth ? ( + + ) : ( + + ); +} diff --git a/src/components/Cart/CartBadge.jsx b/src/components/Cart/CartBadge.jsx deleted file mode 100644 index e26c5ca..0000000 --- a/src/components/Cart/CartBadge.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; - -import { CgShoppingCart } from 'react-icons/cg'; - -const CartBadge = ({ onCartItems, className }) => { - const [badgePulse, setBadgePulse] = useState(false); - - useEffect(() => { - if (onCartItems) { - setBadgePulse(true); - } - const timer = setTimeout(() => { - setBadgePulse(false); - }, 700); - - return () => { - clearTimeout(timer); - }; - }, [onCartItems]); - - return ( - - - {onCartItems} - - - - ); -}; - -export default CartBadge; diff --git a/src/components/Cart/CartBadge.tsx b/src/components/Cart/CartBadge.tsx new file mode 100644 index 0000000..650b659 --- /dev/null +++ b/src/components/Cart/CartBadge.tsx @@ -0,0 +1,49 @@ +import clsx from 'clsx'; +import { useEffect, useState } from 'react'; +import { CgShoppingCart } from 'react-icons/cg'; +import { Link } from 'react-router-dom'; + +interface ICartBadgeProps { + onCartItems: number; + className?: string; +} + +export default function CartBadge({ onCartItems, className }: ICartBadgeProps) { + const [badgePulse, setBadgePulse] = useState(false); + + useEffect(() => { + if (onCartItems) { + setBadgePulse(true); + } + const timer = setTimeout(() => { + setBadgePulse(false); + }, 700); + + return () => { + clearTimeout(timer); + }; + }, [onCartItems]); + + return ( + + 99 ? 'w-8' : 'w-7', + badgePulse && 'animate-pulse', + 'absolute -right-3 -top-1 flex h-5 items-center justify-center rounded-full bg-dark-brown text-xs font-medium text-white-bone', + 'dark:bg-white-bone dark:text-dark-brown', + 'sm:-top-3' + )} + > + {onCartItems > 99 ? '99+' : onCartItems} + + + + ); +} diff --git a/src/components/Cart/CartItem.jsx b/src/components/Cart/CartItem.jsx deleted file mode 100644 index f9e656f..0000000 --- a/src/components/Cart/CartItem.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; - -import { Button, Image, Input } from 'components/UI'; -import { formatCurrencyToFixed } from 'utils/formatCurrency'; - -const CartItem = ({ - id, - image, - title, - amount, - price, - onDecrease, - onIncrease, - onRemove, -}) => { - return ( -
  • - {title} -
    -

    - {title} -

    -
    - - - -
    - - @ Rp. {formatCurrencyToFixed(price)} x {amount} - - -
    -
  • - ); -}; - -export default CartItem; diff --git a/src/components/Cart/CartItem.tsx b/src/components/Cart/CartItem.tsx new file mode 100644 index 0000000..43cdf58 --- /dev/null +++ b/src/components/Cart/CartItem.tsx @@ -0,0 +1,97 @@ +import clsx from 'clsx'; + +import { Image } from '@components/UI'; + +import { formatCurrencyToFixed } from '@utils/formatted'; + +import { INewProductToCart } from 'types/types'; + +interface ICartItemProps extends INewProductToCart { + onDecrease: (_id: number) => void; + onIncrease: (_id: number) => void; + onRemove: (_id: number) => void; +} + +export default function CartItem({ + product_id, + quantity, + price, + product, + onDecrease, + onIncrease, + onRemove, +}: ICartItemProps) { + return ( +
  • + {product?.attributes.name} +
    +

    + {product?.attributes.name} +

    +
    + + + +
    + + {formatCurrencyToFixed(+price)} x {quantity} + + +
    +
  • + ); +} diff --git a/src/components/Cart/CartList.jsx b/src/components/Cart/CartList.jsx deleted file mode 100644 index f831164..0000000 --- a/src/components/Cart/CartList.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import CartItem from './CartItem'; -import { useCart } from 'hooks/useStoreContext'; -import { Button } from 'components/UI'; - -const CartList = () => { - const { items, addItem, decreaseItem, deleteItem } = useCart(); - const navigate = useNavigate(); - - useEffect(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - }, []); - - const gotoShopHandler = () => navigate('/shop', { replace: true }); - - const increaseItemHandler = (id) => { - const newItem = items.find((item) => item.id === id); - addItem({ - ...newItem, - amount: 1, - }); - }; - const decreaseItemHandler = (id) => { - decreaseItem(id); - }; - - const removeCartHandler = (id) => { - deleteItem(id); - }; - - return ( - <> - {items.length < 1 ? ( -
    - - Cart Empty - - -
    - ) : ( -
      - {items.map((item) => { - return ( - - ); - })} -
    - )} - - ); -}; - -export default CartList; diff --git a/src/components/Cart/CartList.tsx b/src/components/Cart/CartList.tsx new file mode 100644 index 0000000..3253b7a --- /dev/null +++ b/src/components/Cart/CartList.tsx @@ -0,0 +1,78 @@ +import clsx from 'clsx'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { addToCart, decreaseFromCart, removeFromCart } from '@store/cartSlice'; + +import { useAppDispatch } from '@hooks/useReduxT'; + +import CartItem from './CartItem'; + +import { INewProductToCart } from 'types/types'; + +interface ICartListProps { + cartItems: INewProductToCart[]; +} + +export default function CartList({ cartItems }: ICartListProps) { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }, []); + + const gotoShopHandler = () => navigate('/shop', { replace: true }); + + const increaseItemHandler = (id: number) => { + const newItem = cartItems.find((item) => item.product_id === id); + dispatch(addToCart({ ...newItem, quantity: 1 } as INewProductToCart)); + }; + const decreaseItemHandler = (id: number) => { + dispatch(decreaseFromCart(id)); + }; + + const removeCartHandler = (id: number) => { + dispatch(removeFromCart(id)); + }; + + return ( + <> + {cartItems?.length < 1 ? ( +
    + + Cart Empty + + +
    + ) : ( +
      + {cartItems.map((item) => { + return ( + + ); + })} +
    + )} + + ); +} diff --git a/src/components/Cart/CartSummary.jsx b/src/components/Cart/CartSummary.jsx deleted file mode 100644 index 9265af3..0000000 --- a/src/components/Cart/CartSummary.jsx +++ /dev/null @@ -1,195 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Spinner } from 'flowbite-react'; -import countryList from 'react-select-country-list'; - -import { useCart, useAuth } from 'hooks/useStoreContext'; -import useFormState from 'hooks/useFormState'; -import useAxios from 'hooks/useAxios'; -import TotalPricesOrder from './TotalPricesOrder'; -import { SelectItems, Button, Input } from 'components/UI'; - -const inputClassName = - 'w-full rounded-sm border-2 border-dark-brown bg-white-bone p-2 text-sm font-medium outline-none placeholder:text-sm focus:border-dark-brown focus:ring-0 placeholder:uppercase'; - -const CartSummary = ({ totalCartItems }) => { - const [country, setCountry] = useState(''); - const { input, setInput, onChangeInputHandler } = useFormState({ - fullName: '', - email: '', - street: '', - phone: '', - zipCode: '', - city: '', - province: '', - }); - - const { requestHttp, loading, error } = useAxios(); - const { totalPriceAmount } = useCart(); - const { isAuth } = useAuth(); - const navigate = useNavigate(); - const countryOption = useMemo(() => countryList().getData(), []); - - useEffect(() => { - const userId = JSON.parse(localStorage.getItem('decode')); - if (userId) { - requestHttp( - { - method: 'GET', - url: `users/${userId.sub}`, - }, - (data) => { - const { - name: { firstname, lastname }, - email, - address: { city, number, street, zipcode }, - phone, - } = data; - - setInput((prevState) => ({ - ...prevState, - email, - phone, - city, - fullName: `${firstname} ${lastname}`, - street: `${street}, ${number}`, - zipCode: zipcode, - })); - } - ); - } - }, [requestHttp, setInput]); - - const { fullName, email, street, phone, zipCode, city, province } = input; - - const countryChangeHandler = (value) => { - setCountry(value); - }; - - const formOrderHandler = (event) => { - event.preventDefault(); - - if (isAuth) { - // Handling Order - } else { - navigate('/login'); - } - - setInput({ - fullName: '', - email: '', - street: '', - phone: '', - zipCode: '', - city: '', - province: '', - }); - setCountry(''); - }; - - return ( -
    -
    -

    - Shipping detail -

    - {loading.isLoading && } - {error.isError && ( - - {error.errorMessage} - - )} -
    -
    -
    - - - - -
    -
    - - - -
    - - - - -
    - ); -}; - -export default CartSummary; diff --git a/src/components/Cart/CartSummary.tsx b/src/components/Cart/CartSummary.tsx new file mode 100644 index 0000000..7cfec17 --- /dev/null +++ b/src/components/Cart/CartSummary.tsx @@ -0,0 +1,68 @@ +import clsx from 'clsx'; + +import Loading from '@components/UI/Loading'; + +import { useAppSelector } from '@hooks/useReduxT'; + +import TotalPricesOrder from './TotalPricesOrder'; + +interface ICartSummaryProps { + totalCartItems: number; + onPaymentHandler: () => void; +} + +export default function CartSummary({ + totalCartItems, + onPaymentHandler, +}: ICartSummaryProps) { + const { totalPrice, status, errorMessage } = useAppSelector( + (state) => state.cart + ); + + const { status: orderStatus } = useAppSelector((state) => state.order); + + return ( +
    +
    +
    +

    + Cart Totals +

    + {status === 'pending' && } + {status === 'rejected' && ( + + {errorMessage} + + )} +
    + +
    + +
    + ); +} diff --git a/src/components/Cart/TotalPricesOrder.jsx b/src/components/Cart/TotalPricesOrder.jsx deleted file mode 100644 index 67b80a9..0000000 --- a/src/components/Cart/TotalPricesOrder.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -import { formatCurrencyToFixed } from 'utils/formatCurrency'; - -const shippingCost = 20000; - -const TotalPricesOrder = ({ totalCartItems, totalPriceAmount }) => { - return ( - <> -
    -

    - Subtotal ({totalCartItems} - {totalCartItems === 1 ? ' Item' : ' Items'} ) : -

    - - Rp. {formatCurrencyToFixed(totalPriceAmount)} - -
    - {totalCartItems >= 1 && ( -
    -

    Shipping

    - - Rp. {formatCurrencyToFixed(shippingCost)} - -
    - )} - -
    -

    Total

    - - Rp. - {formatCurrencyToFixed( - totalPriceAmount + (totalCartItems >= 1 ? shippingCost : 0) - )} - -
    - - ); -}; - -export default TotalPricesOrder; diff --git a/src/components/Cart/TotalPricesOrder.tsx b/src/components/Cart/TotalPricesOrder.tsx new file mode 100644 index 0000000..649a64f --- /dev/null +++ b/src/components/Cart/TotalPricesOrder.tsx @@ -0,0 +1,52 @@ +import clsx from 'clsx'; + +import { formatCurrencyToFixed } from '@utils/formatted'; + +interface ITotalPricesOrderProps { + totalCartItems: number; + totalPriceAmount: number; +} + +export default function TotalPricesOrder({ + totalCartItems, + totalPriceAmount, +}: ITotalPricesOrderProps) { + return ( + <> +
    +

    + Subtotal ( + + {totalCartItems > 99 ? '99+' : totalCartItems} + + {totalCartItems === 1 ? ' Item' : ' Items'} ) : +

    + + {formatCurrencyToFixed(totalPriceAmount)} + +
    +
    +

    Total

    + + {formatCurrencyToFixed(totalPriceAmount)} + +
    + + ); +} diff --git a/src/components/Footer/BrandFooter.tsx b/src/components/Footer/BrandFooter.tsx new file mode 100644 index 0000000..8c90655 --- /dev/null +++ b/src/components/Footer/BrandFooter.tsx @@ -0,0 +1,25 @@ +import clsx from 'clsx'; + +import { Image } from '@components/UI'; + +import LogoImg from '@assets/image/logo/logo-no-background.svg'; + +export default function BrandFooter() { + return ( +
    + Urban Fashion +

    + You have a more input life if you wear impressive clothes. +

    +
    + ); +} diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx deleted file mode 100644 index a222869..0000000 --- a/src/components/Footer/Footer.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -import NewsLetter from './NewsLetter'; -import NavigationFooter from './NavigationFooter'; -import SocialsMedia from './SocialsMedia'; - -const Footer = () => { - return ( -
    -
    - - -
    - -
    - ); -}; - -export default Footer; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..77d7a9a --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,26 @@ +import clsx from 'clsx'; + +import BrandFooter from './BrandFooter'; +import NavigationFooter from './NavigationFooter'; +// import NewsLetter from './NewsLetter'; +import SocialsMedia from './SocialsMedia'; + +export default function Footer() { + return ( +
    +
    +
    + + {/* */} + +
    + +
    +
    + ); +} diff --git a/src/components/Footer/NavigationFooter.jsx b/src/components/Footer/NavigationFooter.jsx deleted file mode 100644 index b5e3e9c..0000000 --- a/src/components/Footer/NavigationFooter.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; - -import useAxios from 'hooks/useAxios'; -import { useAuth } from 'hooks/useStoreContext'; - -const NavigationFooter = () => { - const [categories, setCategories] = useState([]); - const { isAuth, unAuth } = useAuth(); - const { requestHttp } = useAxios(); - - useEffect(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - const categoriesStorage = JSON.parse(localStorage.getItem('categories')); - if (!categoriesStorage) { - requestHttp( - { - method: 'GET', - url: 'products/categories', - }, - (data) => { - setCategories(data); - localStorage.setItem('categories', JSON.stringify(data)); - } - ); - } else { - setCategories(categoriesStorage); - } - }, [requestHttp]); - - return ( -
    -
    -

    - Category -

    -
      - {categories.map((category) => { - return ( -
    • - - {category} - -
    • - ); - })} -
    -
    -
    -

    - Menu -

    -
      -
    • - - Home - -
    • -
    • - - Shop - -
    • - {isAuth && ( -
    • - - My Account - -
    • - )} - {isAuth ? ( -
    • - unAuth(true, 'Logout Successfully')} - > - Logout - -
    • - ) : ( -
    • - - Login - -
    • - )} -
    -
    -
    - ); -}; - -export default NavigationFooter; diff --git a/src/components/Footer/NavigationFooter.tsx b/src/components/Footer/NavigationFooter.tsx new file mode 100644 index 0000000..c24f33f --- /dev/null +++ b/src/components/Footer/NavigationFooter.tsx @@ -0,0 +1,107 @@ +import clsx from 'clsx'; +import { Link } from 'react-router-dom'; + +import { logoutHandler } from '@store/authSlice'; + +import { useAppDispatch, useAppSelector } from '@hooks/useReduxT'; + +export default function NavigationFooter() { + const { isAuthenticated } = useAppSelector((state) => state.auth); + const { categories } = useAppSelector((state) => state.products); + + const dispatch = useAppDispatch(); + + const logOutUserHandler = () => { + dispatch(logoutHandler()); + }; + + return ( +
    +
    +

    + Category +

    +
      + {categories?.data.map((category) => { + return ( +
    • + + {category?.attributes.name} + +
    • + ); + })} +
    +
    +
    +

    + Menu +

    +
      +
    • + + Home + +
    • +
    • + + Shop + +
    • + {isAuthenticated && ( +
    • + + My Account + +
    • + )} + {isAuthenticated ? ( +
    • + + Logout + +
    • + ) : ( +
    • + + Login + +
    • + )} +
    +
    +
    + ); +} diff --git a/src/components/Footer/NewsLetter.jsx b/src/components/Footer/NewsLetter.jsx deleted file mode 100644 index 5207b3a..0000000 --- a/src/components/Footer/NewsLetter.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useRef } from 'react'; - -import { Input, Button } from 'components/UI'; - -const NewsLetter = () => { - const inputRef = useRef(); - - const emailSubmitHandler = (event) => { - event.preventDefault(); - const emailInput = inputRef.current.value; - alert(emailInput); - }; - - return ( -
    -

    - NewsLetter -

    -

    Join our newsletter

    -
    - - -
    -
    - ); -}; - -export default NewsLetter; diff --git a/src/components/Footer/NewsLetter.tsx b/src/components/Footer/NewsLetter.tsx new file mode 100644 index 0000000..a0117e5 --- /dev/null +++ b/src/components/Footer/NewsLetter.tsx @@ -0,0 +1,62 @@ +import clsx from 'clsx'; +import { useRef } from 'react'; + +export default function NewsLetter() { + const inputRef = useRef(null); + + const emailSubmitHandler = (event: React.FormEvent) => { + event.preventDefault(); + const emailInput = inputRef.current?.value; + alert(emailInput); + }; + + return ( +
    +

    + NewsLetter +

    +

    + Join our newsletter +

    +
    + + +
    +
    + ); +} diff --git a/src/components/Footer/SocialsMedia.jsx b/src/components/Footer/SocialsMedia.jsx deleted file mode 100644 index 6057640..0000000 --- a/src/components/Footer/SocialsMedia.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; - -import { BsInstagram, BsFacebook } from 'react-icons/bs'; - -const SocialsMedia = () => { - return ( -
    - - -
    - Privacy Policy - Term & Conditions -
    -
    - urban fashion © {new Date().getFullYear()} -
    -
    - ); -}; - -export default SocialsMedia; diff --git a/src/components/Footer/SocialsMedia.tsx b/src/components/Footer/SocialsMedia.tsx new file mode 100644 index 0000000..355ba2a --- /dev/null +++ b/src/components/Footer/SocialsMedia.tsx @@ -0,0 +1,65 @@ +import clsx from 'clsx'; +import { BsFacebook, BsInstagram } from 'react-icons/bs'; + +export default function SocialsMedia() { + return ( +
    + + +
    + Privacy Policy + Term & Conditions +
    +
    + urban fashion © {new Date().getFullYear()} +
    +
    + ); +} diff --git a/src/components/Home/BestSellers.jsx b/src/components/Home/BestSellers.jsx deleted file mode 100644 index 9bd7aac..0000000 --- a/src/components/Home/BestSellers.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import useAxios from 'hooks/useAxios'; -import ProductItem from 'components/Shop/ProductItem'; - -const BestSellers = () => { - const [bestSellers, setBestSellers] = useState([]); - - const { requestHttp, loading, error } = useAxios(); - - useEffect(() => { - requestHttp( - { - method: 'GET', - url: 'products?limit=4', - }, - (data) => setBestSellers(data) - ); - }, [requestHttp]); - - const bestSellersContent = ( -
      - {bestSellers.map((product) => { - return ( - - ); - })} -
    - ); - return ( -
    -

    - BestSellers -

    - {loading.isLoading && ( -

    - {loading.loadingMessage} -

    - )} - {error.isError && ( -

    - {error.errorMessage} -

    - )} - {!loading.isLoading && !error.isError && bestSellersContent} -
    - ); -}; - -export default BestSellers; diff --git a/src/components/Home/BestSellers.tsx b/src/components/Home/BestSellers.tsx new file mode 100644 index 0000000..cf32db1 --- /dev/null +++ b/src/components/Home/BestSellers.tsx @@ -0,0 +1,58 @@ +import clsx from 'clsx'; + +import ProductItem from '@components/Shop/ProductItem'; +import Loading from '@components/UI/Loading'; + +import { useAppSelector } from '@hooks/useReduxT'; + +export default function BestSellers() { + const { products, status, errorMessage } = useAppSelector( + (state) => state.products + ); + + const bestSellersContent = ( +
      + {products?.data.slice(0, 4).map((product) => { + return ( + + ); + })} +
    + ); + return ( +
    +

    + BestSellers +

    + {status === 'pending' && } + {status === 'rejected' && ( +

    + {errorMessage} +

    + )} + {status === 'fulfilled' && bestSellersContent} +
    + ); +} diff --git a/src/components/Home/FashionProducts.jsx b/src/components/Home/FashionProducts.jsx deleted file mode 100644 index b457055..0000000 --- a/src/components/Home/FashionProducts.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; - -import { Button } from 'components/UI'; - -import imageHome from '../../assets/image/home-image.webp'; - -const FashionProducts = () => { - const navigate = useNavigate(); - - const goToShopHandler = () => navigate('/shop', { replace: true }); - - return ( -
    - -
    -
    -

    - Fashion
    Products -

    -

    - Fashion is part of the daily air and it changes all the time, with - all the events. You can even see the approaching of a revolution in - clothes. You can see and feel everything in clothes. -

    -
    - -
    -
    - ); -}; - -export default FashionProducts; diff --git a/src/components/Home/FashionProducts.tsx b/src/components/Home/FashionProducts.tsx new file mode 100644 index 0000000..ee68b69 --- /dev/null +++ b/src/components/Home/FashionProducts.tsx @@ -0,0 +1,70 @@ +import clsx from 'clsx'; +import { useNavigate } from 'react-router-dom'; + +import { Image } from '@components/UI'; + +import imageHome from '@assets/image/home-image.webp'; + +export default function FashionProducts() { + const navigate = useNavigate(); + + const goToShopHandler = () => navigate('/shop', { replace: true }); + + return ( +
    + Fashion Products +
    +
    +

    + Fashion
    Products +

    +

    + Fashion is part of the daily air and it changes all the time, with + all the events. You can even see the approaching of a revolution in + clothes. You can see and feel everything in clothes. +

    +
    + +
    +
    + ); +} diff --git a/src/components/Home/Hero.jsx b/src/components/Home/Hero.tsx similarity index 53% rename from src/components/Home/Hero.jsx rename to src/components/Home/Hero.tsx index 7217bf4..d2157a9 100644 --- a/src/components/Home/Hero.jsx +++ b/src/components/Home/Hero.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import clsx from 'clsx'; import { Carousel } from 'flowbite-react'; -import { Image } from 'components/UI'; +import { Image } from '@components/UI'; -import heroImage1 from 'assets/image/hero-image-1.webp'; -import heroImage2 from 'assets/image/hero-image-2.webp'; -import heroImage3 from 'assets/image/hero-image-3.webp'; +import heroImage1 from '@assets/image/hero-image-1.webp'; +import heroImage2 from '@assets/image/hero-image-2.webp'; +import heroImage3 from '@assets/image/hero-image-3.webp'; const heroImage = [ { @@ -22,9 +22,9 @@ const heroImage = [ }, ]; -const Hero = () => { +export default function Hero() { return ( -
    +
    {heroImage.map((image, index) => ( {`hero-image-${index}`} @@ -32,6 +32,4 @@ const Hero = () => {
    ); -}; - -export default Hero; +} diff --git a/src/components/Home/OurPhilosophy.jsx b/src/components/Home/OurPhilosophy.jsx deleted file mode 100644 index 9e5efc4..0000000 --- a/src/components/Home/OurPhilosophy.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; - -import { Image } from 'components/UI'; - -import philosophyImage from 'assets/image/philosophy-image.webp'; - -const OurPhilosophy = () => { - return ( -
    -
    -

    - Our Philosophy -

    -

    - You have a more interesting life if you wear impressive clothes. -

    -
    - You have a more interesting life if you wear impressive clothes. -
    - ); -}; - -export default OurPhilosophy; diff --git a/src/components/Home/OurPhilosophy.tsx b/src/components/Home/OurPhilosophy.tsx new file mode 100644 index 0000000..c765aee --- /dev/null +++ b/src/components/Home/OurPhilosophy.tsx @@ -0,0 +1,40 @@ +import clsx from 'clsx'; + +import { Image } from '@components/UI'; + +import philosophyImage from '@assets/image/philosophy-image.webp'; + +export default function OurPhilosophy() { + return ( +
    +
    +

    + Our Philosophy +

    +

    + You have a more interesting life if you wear impressive clothes. +

    +
    + You have a more interesting life if you wear impressive clothes. +
    + ); +} diff --git a/src/components/Layout/Layout.jsx b/src/components/Layout/Layout.jsx deleted file mode 100644 index eef0e9c..0000000 --- a/src/components/Layout/Layout.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; - -import Navigation from 'components/Navigation/Navigation'; -import Footer from 'components/Footer/Footer'; -import { ScrollTop } from 'components/UI'; - -const Layout = () => { - return ( - <> -
    - -
    -
    - -
    -