├── .env.sample ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components ├── Badge.jsx ├── Chart.tsx ├── Container.tsx ├── ContinuePayBtn.tsx ├── Dropdown.tsx ├── Link.tsx ├── Modal.tsx ├── Navbar.tsx ├── ProductItem.tsx ├── Rating.tsx ├── RequirementField.tsx ├── UnderDevelopment.tsx ├── UserBadge.tsx ├── Wrapper.tsx ├── admin │ ├── AdminContainer.tsx │ ├── ModalHeader.tsx │ ├── ModalHeaderButton.tsx │ └── products │ │ ├── AddProductsModal.tsx │ │ ├── DeleteProductModal.tsx │ │ ├── EditProductModal.tsx │ │ ├── Product.tsx │ │ └── ProductsTable.tsx ├── home │ └── RatingsModal.tsx ├── mui │ └── Tabs.ts ├── profile │ ├── OrderHistory.tsx │ └── TopUpInformation.tsx └── topup │ ├── 1000-baris │ ├── Diamond.jsx │ ├── MLSection.jsx │ └── Starlight.jsx │ ├── TopupInfo.tsx │ ├── TopupItem.tsx │ ├── TopupItems.tsx │ └── TopupRequirements.tsx ├── hooks ├── useClickOutside.js ├── useGetRequest.ts ├── useIsMounted.ts └── usePayHandler.ts ├── lib ├── admin.ts ├── apiHandler.ts ├── cartHandler.ts ├── data.ts ├── prisma.ts ├── request.ts └── utils.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.jsx ├── _document.jsx ├── admin │ ├── index.tsx │ ├── orders.tsx │ ├── products.tsx │ └── users.tsx ├── api │ ├── auth │ │ └── [...nextauth].js │ ├── authToken.js │ ├── carts │ │ └── me.ts │ ├── categories │ │ ├── [categoryId].ts │ │ └── index.ts │ ├── discountCodes │ │ └── [code].ts │ ├── orders │ │ ├── [orderId].ts │ │ ├── index.ts │ │ └── me.ts │ ├── products │ │ ├── [productId].ts │ │ ├── index.ts │ │ └── setDiscount.ts │ ├── ratings │ │ └── index.ts │ ├── requirements │ │ ├── [fieldName].ts │ │ └── index.ts │ └── users │ │ ├── displayName.ts │ │ └── fakeAdmin.ts ├── blog │ ├── about-rausky.tsx │ ├── discount.tsx │ └── rausky-payments.tsx ├── cart.tsx ├── index.jsx ├── order.tsx ├── products │ └── [category].tsx ├── profile.tsx ├── signin.tsx └── topup │ └── [category].tsx ├── postcss.config.js ├── prisma └── schema.prisma ├── public └── images │ ├── auth │ ├── discord.png │ ├── github.png │ └── google.webp │ ├── icon │ ├── bag.svg │ ├── instagram.png │ ├── line.png │ ├── person.svg │ └── union.svg │ ├── illustration │ ├── empty-cart.svg │ ├── empty-order.svg │ ├── empty-rating.svg │ └── under-development.svg │ ├── logo │ ├── favicon.png │ ├── overview.jpg │ └── rausky-logo.png │ └── product │ ├── ff-banner.jpg │ ├── ff-logo.jpg │ ├── mobile-legend-banner.jpg │ ├── mobile-legend-logo.png │ ├── netflix-banner.jpg │ ├── netflix-logo.jpg │ ├── ps-banner.jpg │ ├── ps-logo.png │ ├── pubg-mobile-banner.jpg │ ├── pubg-mobile-logo.png │ ├── spotify-banner.png │ ├── spotify-logo.png │ ├── steam-banner.webp │ ├── steam-logo.png │ ├── valorant-banner.jpg │ └── valorant-logo.png ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── types ├── globals.d.ts ├── next-auth.d.ts └── state.d.ts /.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mongodb+srv://:@cluster0.caudh.mongodb.net/rausky-store?retryWrites=true&w=majority 2 | 3 | GOOGLE_ID= 4 | GOOGLE_SECRET= 5 | 6 | GITHUB_ID= 7 | GITHUB_SECRET= 8 | 9 | DISCORD_ID= 10 | DISCORD_SECRET= 11 | 12 | NEXTAUTH_URL=http://localhost:3000 13 | NEXTAUTH_SECRET= 14 | 15 | MIDTRANS_SERVER_KEY= 16 | NEXT_PUBLIC_MIDTRANS_CLIENT_KEY= 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rausky Store 2 | 3 | ## Setup 4 | 5 | ``` 6 | npm i 7 | npx prisma generate 8 | npm run dev 9 | ``` 10 | 11 | ## Todo 12 | 13 | 1. [x] Bikin function buat reorder 14 | 1. [x] Bikin halaman topup information buat ganti topup requirements 15 | 1. [x] Bikin function buat edit display name 16 | 1. [x] Bikin display 'no rating' di rating modal 17 | 1. [x] Bikin animasi pergantian page pake framer motion 18 | 1. [x] Bikin search buat mencari topup category atau produk tiap category 19 | 1. [x] Bikin Dark Mode 20 | 1. [x] Bikin halaman about rausky dan learn more 21 | 1. [] Bikin halaman other product (gaming gear, merchandise) 22 | -------------------------------------------------------------------------------- /components/Badge.jsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import { useEffect, useState } from 'react' 3 | 4 | const Badge = ({ children }) => { 5 | const [membesar, setMembesar] = useState(false) 6 | useEffect(() => { 7 | setMembesar(true) 8 | }, []) 9 | 10 | return ( 11 |
17 | {children} 18 |
19 | ) 20 | } 21 | 22 | export default Badge 23 | -------------------------------------------------------------------------------- /components/Chart.tsx: -------------------------------------------------------------------------------- 1 | import { Chart as ReactChart, ChartProps } from 'react-chartjs-2' 2 | import 'chart.js/auto' 3 | 4 | interface Props extends ChartProps {} 5 | 6 | const Chart = ({ ...props }: Props) => { 7 | return ( 8 | 22 | ) 23 | } 24 | 25 | export default Chart 26 | -------------------------------------------------------------------------------- /components/Container.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { FC } from 'react' 3 | import Navbar from './Navbar' 4 | 5 | interface Props { 6 | title?: string 7 | noNavbar?: boolean 8 | noTopMargin?: boolean 9 | } 10 | 11 | const head = { 12 | title: 'Rausky Gamestore', 13 | } 14 | 15 | const Container: FC = ({ title, children, noNavbar, noTopMargin }) => { 16 | return ( 17 |
18 | 19 | {title ? `${title} - ${head.title}` : head.title} 20 | 21 | 25 | 29 | 30 | 31 | 32 | 33 | {/* Open Graph / Facebook */} 34 | 35 | 36 | 37 | 41 | 42 | 43 | {/* Twitter */} 44 | 45 | 46 | 47 | 51 | 52 | 53 | {/* Dicoding */} 54 | 55 | 56 | 57 | {!noNavbar && } 58 | {!noTopMargin &&
} 59 | {children} 60 |
61 | {/* TODO: bikin footer */} 62 |
63 | ) 64 | } 65 | 66 | export default Container 67 | -------------------------------------------------------------------------------- /components/ContinuePayBtn.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from 'framer-motion' 2 | import { useStateMachine } from 'little-state-machine' 3 | import Link from './Link' 4 | import Wrapper from './Wrapper' 5 | 6 | const ContinuePayBtn = () => { 7 | const { state } = useStateMachine() 8 | const { cart, order } = state 9 | 10 | const totalItemsInCart = cart.length 11 | 12 | return ( 13 | 14 | {totalItemsInCart > 0 && ( 15 | <> 16 |
17 | 32 | 33 | 37 | 38 | Continue to payment (Rp {order.total.toLocaleString()}) 39 | 40 | {order.promoCode && ( 41 | 42 | '{order.promoCode}' Applied ({order.discount}% 43 | OFF) 44 | 45 | )} 46 | 47 | 48 | 49 | 50 | )} 51 |
52 | ) 53 | } 54 | 55 | export default ContinuePayBtn 56 | -------------------------------------------------------------------------------- /components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChevronLeftIcon, 3 | ChevronRightIcon, 4 | LockClosedIcon, 5 | } from '@heroicons/react/outline' 6 | import cn from 'classnames' 7 | import { useEffect, useRef, useState } from 'react' 8 | 9 | export interface DropdownItem { 10 | icon?: any 11 | className?: string 12 | label: string 13 | more?: DropdownItem[] 14 | onClick?: () => void 15 | disabled?: boolean 16 | } 17 | 18 | interface Props { 19 | items: DropdownItem[] 20 | children: React.ReactNode 21 | className?: string 22 | minWidth?: number 23 | translateY?: number 24 | deps?: any[] 25 | } 26 | 27 | const Dropdown = ({ 28 | children, 29 | className, 30 | minWidth = 250, 31 | items, 32 | translateY = 0, 33 | deps = [], 34 | }: Props) => { 35 | const [show, setShow] = useState(false) 36 | const containerRef = useRef(null) 37 | const [containerWidth, setContainerWidth] = useState(0) 38 | const [dropdownItems, setDropdownItems] = useState(items) 39 | const [inMore, setInMore] = useState(null) 40 | 41 | useEffect(() => { 42 | const changeContainerWidth = () => { 43 | setContainerWidth(containerRef.current.clientWidth) 44 | } 45 | changeContainerWidth() 46 | window.addEventListener('resize', changeContainerWidth) 47 | return () => { 48 | window.removeEventListener('resize', changeContainerWidth) 49 | } 50 | }, deps) 51 | 52 | useEffect(() => { 53 | setDropdownItems(items) 54 | }, [items]) 55 | 56 | const resetDropdown = () => { 57 | setInMore(null) 58 | setDropdownItems(items) 59 | } 60 | 61 | const dropdownProps = 62 | 'ontouchstart' in window // if in touch device 63 | ? { 64 | onClick: () => { 65 | setShow(true) 66 | }, 67 | } 68 | : { 69 | onMouseEnter: () => { 70 | setShow(true) 71 | }, 72 | onMouseLeave: () => { 73 | setShow(false) 74 | resetDropdown() 75 | }, 76 | } 77 | 78 | return ( 79 | <> 80 | {show && ( 81 |
{ 84 | setShow(false) 85 | resetDropdown() 86 | }} 87 | >
88 | )} 89 |
95 | {children} 96 | {show && ( 97 |
106 | {inMore && ( 107 |
108 |
109 | 115 |

{inMore.label}

116 |
117 | {inMore.customMore || null} 118 |
119 | )} 120 | 121 | {dropdownItems?.map((item, idx) => { 122 | return ( 123 | 161 | ) 162 | })} 163 |
164 | )} 165 |
166 | 167 | ) 168 | } 169 | 170 | export default Dropdown 171 | -------------------------------------------------------------------------------- /components/Link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink, { LinkProps } from 'next/link' 2 | import { FC } from 'react' 3 | 4 | interface Props extends React.PropsWithChildren { 5 | className?: string 6 | } 7 | 8 | const Link: FC = ({ className, children, ...linkProps }) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | 16 | export default Link 17 | -------------------------------------------------------------------------------- /components/Modal.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import { useEffect, forwardRef, ForwardedRef } from 'react' 3 | 4 | interface Props { 5 | children: React.ReactNode 6 | open: boolean 7 | onClose: () => void 8 | } 9 | 10 | const Modal = forwardRef( 11 | ({ children, open, onClose }: Props, ref: ForwardedRef) => { 12 | useEffect(() => { 13 | if (open) { 14 | document.body.classList.add('overflow-hidden') 15 | } else { 16 | document.body.classList.remove('overflow-hidden') 17 | } 18 | }, [open]) 19 | 20 | return ( 21 | open && ( 22 |
23 | {/* OVERLAY */} 24 |
28 | {/* MODAL BODY */} 29 |
33 | {children} 34 |
35 |
36 | ) 37 | ) 38 | } 39 | ) 40 | 41 | export default Modal 42 | -------------------------------------------------------------------------------- /components/ProductItem.tsx: -------------------------------------------------------------------------------- 1 | import { TrashIcon } from '@heroicons/react/outline' 2 | import cn from 'classnames' 3 | 4 | interface Props { 5 | item: any 6 | actions?: any 7 | size?: 'normal' | 'small' 8 | } 9 | 10 | const ProductItem = ({ item, actions, size = 'normal' }: Props) => { 11 | return ( 12 |
13 | {item.title} 21 |
27 |

33 | {item.category.name} 34 |

35 |

41 | {item.title}{' '} 42 | {item.amount && item.amount > 1 && ( 43 | x{item.amount} 44 | )} 45 |

46 |

52 | Rp {item.price.toLocaleString()} 53 |

54 |
55 | 56 | {actions ? ( 57 |
58 | {/* SET QUANTITY */} 59 |
60 | 67 |
{item.amount}
68 | 75 |
76 | 77 | {/* DELETE */} 78 | 84 |
85 | ) : null} 86 |
87 | ) 88 | } 89 | 90 | export default ProductItem 91 | -------------------------------------------------------------------------------- /components/Rating.tsx: -------------------------------------------------------------------------------- 1 | import { StarIcon } from '@heroicons/react/outline' 2 | import { StarIcon as StarIconSolid } from '@heroicons/react/solid' 3 | import ReactStars from 'react-rating-stars-component' 4 | import { defaultAvatar } from '../lib/data' 5 | import UserBadge from './UserBadge' 6 | 7 | const Rating = ({ rating }) => { 8 | return ( 9 |
10 |
11 | } 14 | filledIcon={} 15 | value={rating.star} 16 | /> 17 | 18 | {new Date(rating.createdAt).toLocaleDateString()} 19 | 20 |
21 |
22 |
23 | 24 | {rating.order.user?.name 29 | 30 | {rating.order.user?.displayName || 31 | rating.order.user?.name || 32 | 'Guest'} 33 | {' '} 34 | {rating.order.user && } 35 | 36 |
37 | 38 |
39 |

40 | {rating.order.products[0].product.title} 41 |

42 | {rating.order.products.length > 1 && ( 43 |

44 | + {rating.order.products.length - 1} other 45 |

46 | )} 47 |
48 | 49 | {/* TODO: cari tau cara bikin truncate */} 50 |

51 | {rating.comment} 52 |

53 |
54 |
55 | ) 56 | } 57 | 58 | export default Rating 59 | -------------------------------------------------------------------------------- /components/RequirementField.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import { useStateMachine } from 'little-state-machine' 3 | import { useEffect, useRef } from 'react' 4 | import { editRequirement } from '../lib/cartHandler' 5 | import request from '../lib/request' 6 | 7 | interface Props { 8 | field: CustomObject 9 | categorySlug: string 10 | user?: CustomObject 11 | error?: string 12 | } 13 | const RequirementField = ({ field, categorySlug, user, error }: Props) => { 14 | const { state, actions } = useStateMachine({ 15 | editRequirement, 16 | setUpdatingDB: (state, value) => { 17 | return { ...state, updatingDB: value } 18 | }, 19 | setUpdatedDB: (state, value) => { 20 | return { ...state, updatedDB: value } 21 | }, 22 | }) 23 | const { order } = state 24 | const requirement = order.requirements[categorySlug] 25 | const fieldValue = requirement?.[field.value] 26 | 27 | const handleRequirement = (e, field) => { 28 | const { value: fieldName } = field 29 | actions.editRequirement({ 30 | fieldName, 31 | fieldValue: e.target.value, 32 | categorySlug, 33 | }) 34 | updateUserRequirement(e.target.value) 35 | } 36 | 37 | const debounce = useRef() 38 | const updateUserRequirement = async (fieldValue) => { 39 | if (!user) return 40 | actions.setUpdatingDB(true) 41 | actions.setUpdatedDB(false) 42 | clearTimeout(debounce.current) 43 | debounce.current = setTimeout(async () => { 44 | try { 45 | await request.put(`/requirements/${field.value}`, { fieldValue }) 46 | } catch (err) { 47 | console.log(err) 48 | } finally { 49 | actions.setUpdatingDB(false) 50 | actions.setUpdatedDB(true) 51 | } 52 | }, 500) 53 | } 54 | 55 | useEffect(() => { 56 | actions.setUpdatedDB(false) 57 | actions.setUpdatingDB(false) 58 | }, []) 59 | 60 | return ( 61 |
62 | (e.target as HTMLInputElement).blur()} 73 | onChange={(e) => handleRequirement(e, field)} 74 | /> 75 | {error && ( 76 |

{error}

77 | )} 78 |
79 | ) 80 | } 81 | 82 | export default RequirementField 83 | -------------------------------------------------------------------------------- /components/UnderDevelopment.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import Container from './Container' 3 | import Wrapper from './Wrapper' 4 | 5 | const UnderDevelopment = () => { 6 | const router = useRouter() 7 | return ( 8 | 9 | 10 |
11 | under development 16 |
17 |

{router.asPath}

18 |

19 | This page is under development 🚧 20 |

21 |

22 | Sorry for the inconvenience. We will provide the best experience 23 | for you ✨ 24 |

25 | 31 |
32 |
33 |
34 |
35 | ) 36 | } 37 | 38 | export default UnderDevelopment 39 | -------------------------------------------------------------------------------- /components/UserBadge.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | 3 | interface Props { 4 | className?: string 5 | role: string 6 | } 7 | 8 | const UserBadge = ({ className, role }: Props) => { 9 | return ( 10 | role != 'USER' && ( 11 | 18 | {role.replace('_', ' ')} 19 | 20 | ) 21 | ) 22 | } 23 | 24 | export default UserBadge 25 | -------------------------------------------------------------------------------- /components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import { FC } from 'react' 3 | 4 | interface Props 5 | extends React.DetailedHTMLProps< 6 | React.HTMLAttributes, 7 | HTMLDivElement 8 | > { 9 | className?: string 10 | } 11 | 12 | const Wrapper: FC = ({ children, className, ...props }) => { 13 | return ( 14 |
18 | {children} 19 |
20 | ) 21 | } 22 | 23 | export default Wrapper 24 | -------------------------------------------------------------------------------- /components/admin/AdminContainer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChartPieIcon, 3 | ShoppingBagIcon, 4 | ShoppingCartIcon, 5 | UserIcon, 6 | } from '@heroicons/react/outline' 7 | import cn from 'classnames' 8 | import Head from 'next/head' 9 | import { useRouter } from 'next/router' 10 | import Link from '../../components/Link' 11 | import UserBadge from '../UserBadge' 12 | import { useEffect, useMemo } from 'react' 13 | import { createTheme } from '@mui/material' 14 | import { useStateMachine } from 'little-state-machine' 15 | import { ThemeProvider } from '@emotion/react' 16 | 17 | const sidebarLinks = [ 18 | { 19 | Icon: ChartPieIcon, 20 | path: '/', 21 | label: 'Overview', 22 | }, 23 | { 24 | Icon: ShoppingCartIcon, 25 | path: '/orders', 26 | label: 'Orders', 27 | }, 28 | { 29 | Icon: ShoppingBagIcon, 30 | path: '/products', 31 | label: 'Products', 32 | }, 33 | { 34 | Icon: UserIcon, 35 | path: '/users', 36 | label: 'Users', 37 | }, 38 | ] 39 | 40 | const AdminContainer = ({ children, user }) => { 41 | const router = useRouter() 42 | const { state } = useStateMachine() 43 | const { globalTheme } = state 44 | 45 | const theme = useMemo(() => { 46 | return createTheme({ 47 | palette: { mode: globalTheme }, 48 | }) 49 | }, [globalTheme]) 50 | 51 | useEffect(() => {}, []) 52 | 53 | const currentPage = 54 | sidebarLinks.find( 55 | (link) => 56 | link.label.toLowerCase() == router.route.replace(/\/admin\/?/, '') 57 | ) || sidebarLinks[0] 58 | 59 | return ( 60 | 61 |
62 | 63 | {currentPage.label} - Rausky Admin 64 | 65 | 96 |
97 |
98 |

99 | {currentPage.label} 100 |

101 |
102 | 112 |
113 |
114 |
{children}
115 |
116 |
117 |
118 | ) 119 | } 120 | 121 | export default AdminContainer 122 | -------------------------------------------------------------------------------- /components/admin/ModalHeader.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | logo?: string 3 | title: string 4 | rightMenu?: React.ReactNode 5 | } 6 | 7 | const ModalHeader = ({ logo, title, rightMenu }: Props) => { 8 | return ( 9 |
10 |
11 | {logo && } 12 |

{title}

13 |
14 | {rightMenu && ( 15 |
{rightMenu}
16 | )} 17 |
18 | ) 19 | } 20 | 21 | export default ModalHeader 22 | -------------------------------------------------------------------------------- /components/admin/ModalHeaderButton.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from 'react' 3 | 4 | interface Props 5 | extends DetailedHTMLProps< 6 | ButtonHTMLAttributes, 7 | HTMLButtonElement 8 | > { 9 | children?: ReactNode 10 | Icon?: any 11 | fill?: 'solid' | 'outlined' 12 | variant?: 'green' | 'red' 13 | } 14 | 15 | const Button = ({ 16 | children, 17 | Icon, 18 | fill = 'solid', 19 | variant = 'green', 20 | ...props 21 | }: Props) => { 22 | const { className } = props 23 | 24 | return ( 25 | 47 | ) 48 | } 49 | 50 | export default Button 51 | -------------------------------------------------------------------------------- /components/admin/products/DeleteProductModal.tsx: -------------------------------------------------------------------------------- 1 | import { adminRequestHandler } from '../../../lib/admin' 2 | import request from '../../../lib/request' 3 | import Modal from '../../Modal' 4 | import ModalHeader from '../ModalHeader' 5 | 6 | const DeleteProductModal = ({ 7 | open, 8 | onClose, 9 | setCategories, 10 | product, 11 | products, 12 | category, 13 | }) => { 14 | const updateCategoryProducts = (products) => { 15 | setCategories((categories) => { 16 | const index = categories.findIndex((c) => c.id == category.id) 17 | return [ 18 | ...categories.slice(0, index), 19 | { ...category, products }, 20 | ...categories.slice(index + 1), 21 | ] 22 | }) 23 | } 24 | 25 | const onDeleteHandler = async () => { 26 | adminRequestHandler({ 27 | loading: 'Deleting...', 28 | handler: async () => { 29 | if (products.length > 1) { 30 | const responses = await Promise.all( 31 | products.map((product) => { 32 | return request.delete(`/products/${product.id}`) 33 | }) 34 | ) 35 | const deletedIds = responses.map((res) => res.data.id) 36 | setCategories((categories) => { 37 | const index = categories.findIndex((c) => c.id == category.id) 38 | return [ 39 | ...categories.slice(0, index), 40 | { 41 | ...category, 42 | products: category.products.filter( 43 | (p) => !deletedIds.includes(p.id) 44 | ), 45 | }, 46 | ...categories.slice(index + 1), 47 | ] 48 | }) 49 | } else { 50 | await request.delete(`/products/${product?.id || products[0].id}`) 51 | const index = category.products.findIndex( 52 | (p) => p.id == (product?.id || products[0].id) 53 | ) 54 | updateCategoryProducts([ 55 | ...category.products.slice(0, index), 56 | ...category.products.slice(index + 1), 57 | ]) 58 | } 59 | onClose() 60 | }, 61 | success: 'Delete Success', 62 | }) 63 | } 64 | return ( 65 | 66 | 70 |
71 |

72 | Are you sure want to delete{' '} 73 | {products.length > 1 ? ( 74 | <> 75 | {products.length} products ? 76 |

    77 | {products.map((product) => ( 78 |
  • 79 | {product?.title} 84 | {product?.title} 85 |
  • 86 | ))} 87 |
88 | 89 | ) : ( 90 | 91 | {product?.title 96 | 97 | {product?.title || products[0]?.title} ? 98 | 99 | 100 | )} 101 |

102 |
103 | 109 | 115 |
116 |
117 |
118 | ) 119 | } 120 | 121 | export default DeleteProductModal 122 | -------------------------------------------------------------------------------- /components/admin/products/EditProductModal.tsx: -------------------------------------------------------------------------------- 1 | import { RefreshIcon, UploadIcon } from '@heroicons/react/outline' 2 | import { useState } from 'react' 3 | import { adminRequestHandler } from '../../../lib/admin' 4 | import request from '../../../lib/request' 5 | import Modal from '../../Modal' 6 | import ModalHeader from '../ModalHeader' 7 | import ModalHeaderButton from '../ModalHeaderButton' 8 | import Product from './Product' 9 | 10 | const initProduct = (product, category) => { 11 | let currentProduct: CustomObject = { 12 | title: product.title, 13 | price: product.price, 14 | stock: product.stock, 15 | } 16 | 17 | if (category.subCategories.length > 0) { 18 | currentProduct.subCategory = category.subCategories[0].slug 19 | } 20 | 21 | return currentProduct 22 | } 23 | 24 | const EditProductModal = ({ 25 | open, 26 | onClose, 27 | setCategories, 28 | product, 29 | category, 30 | }) => { 31 | const [editedProduct, setEditedProduct] = useState( 32 | initProduct(product, category) 33 | ) 34 | 35 | const onFieldChange = (field, value) => { 36 | setEditedProduct({ 37 | ...editedProduct, 38 | [field]: value, 39 | }) 40 | } 41 | 42 | const resetHandler = () => { 43 | let obj: CustomObject = { 44 | title: product.title, 45 | price: product.price, 46 | stock: product.stock, 47 | } 48 | if (product.subCategory?.slug) { 49 | obj.subCategory = product.subCategory.slug 50 | } 51 | setEditedProduct(obj) 52 | } 53 | 54 | const updateCategoryProducts = (products) => { 55 | setCategories((categories) => { 56 | const index = categories.findIndex((c) => c.id == category.id) 57 | return [ 58 | ...categories.slice(0, index), 59 | { ...category, products }, 60 | ...categories.slice(index + 1), 61 | ] 62 | }) 63 | } 64 | 65 | const saveHandler = () => { 66 | adminRequestHandler({ 67 | loading: 'Saving...', 68 | handler: async () => { 69 | let body = { ...editedProduct } 70 | if (editedProduct.subCategory) { 71 | body.subCategory = { 72 | connect: { slug: editedProduct.subCategory }, 73 | } 74 | } 75 | const { data } = await request.put(`/products/${product.id}`, body) 76 | const index = category.products.findIndex((p) => p.id == product.id) 77 | updateCategoryProducts([ 78 | ...category.products.slice(0, index), 79 | data.product, 80 | ...category.products.slice(index + 1), 81 | ]) 82 | onClose() 83 | }, 84 | success: 'Product saved', 85 | }) 86 | } 87 | 88 | return ( 89 | 90 | 95 | 101 | Reset 102 | 103 | 109 | Save 110 | 111 | 112 | } 113 | /> 114 |
115 | onFieldChange('title', e.target.value)} 119 | onPriceChange={(e) => onFieldChange('price', e.target.value)} 120 | onStockChange={(e) => onFieldChange('stock', e.target.value)} 121 | onSubCategoryChange={(e) => 122 | onFieldChange('subCategory', e.target.value) 123 | } 124 | /> 125 |
126 |
127 | ) 128 | } 129 | 130 | export default EditProductModal 131 | -------------------------------------------------------------------------------- /components/admin/products/Product.tsx: -------------------------------------------------------------------------------- 1 | import { TrashIcon } from '@heroicons/react/outline' 2 | import TopupItem from '../../topup/TopupItem' 3 | 4 | interface Props { 5 | product: any 6 | category: any 7 | index?: number 8 | errors?: any 9 | dropdown?: React.ReactNode 10 | onTitleChange: React.ChangeEventHandler 11 | onPriceChange: React.ChangeEventHandler 12 | onStockChange: React.ChangeEventHandler 13 | onSubCategoryChange?: React.ChangeEventHandler 14 | } 15 | 16 | const Product = ({ 17 | category, 18 | product, 19 | index, 20 | dropdown, 21 | onTitleChange, 22 | onPriceChange, 23 | onStockChange, 24 | errors, 25 | onSubCategoryChange, 26 | }: Props) => { 27 | return ( 28 |
29 |
30 | {index !== undefined && ( 31 |

32 | 33 | {index + 1} 34 | 35 | {product.title || 'Untitled Product'} 36 |

37 | )} 38 | {dropdown} 39 |
40 | 41 |
42 | {/* FORM */} 43 |
44 | {/* TITLE */} 45 | 60 | {/* PRICE */} 61 | 77 |
78 | {/* STOCK */} 79 | 89 | 90 | {/* SUBCATEGORY */} 91 | {category.subCategories.length ? ( 92 | 113 | ) : null} 114 |
115 |
116 | 117 | {/* PREVIEW */} 118 |
119 | Preview 120 | 121 | {/* TODO: refactor, bikin komponen topup sendiri */} 122 | 123 | {/*
124 |
125 | {category.logoImg && ( 126 | 131 | c.slug == product.subCategory || 132 | c.slug == product.subCategory.slug 133 | ) 134 | : category 135 | )?.logoImg 136 | } 137 | className="w-10 h-10 object-cover rounded-lg mb-1.5" 138 | /> 139 | )} 140 | 141 | 144 |
145 |
146 |

147 | {product.title || 'Untitled Product'} 148 |

149 |

150 | Rp {Number(product.price).toLocaleString()} 151 |

152 | 153 |
154 |
155 | 159 |
{1}
160 | 164 |
165 | 168 |
169 |
170 |
*/} 171 |
172 |
173 | 174 | {/* DESCRIPTION */} 175 | {product.description !== undefined && ( 176 | 180 | )} 181 |
182 | ) 183 | } 184 | 185 | export default Product 186 | -------------------------------------------------------------------------------- /components/admin/products/ProductsTable.tsx: -------------------------------------------------------------------------------- 1 | import { PencilIcon, TrashIcon } from '@heroicons/react/outline' 2 | import { DataGrid, GridColDef } from '@mui/x-data-grid' 3 | import { useMemo, useState } from 'react' 4 | import DeleteProductModal from './DeleteProductModal' 5 | import EditProductModal from './EditProductModal' 6 | 7 | const ProductsTable = ({ setCategories, category }) => { 8 | const { products } = category 9 | const [showEditProduct, setShowEditProduct] = useState(false) 10 | const [showDeleteProduct, setShowDeleteProduct] = useState(false) 11 | 12 | const [selectedProductId, setSelectedProductId] = useState('') 13 | 14 | const [selectedProductIds, setSelectedProductsIds] = useState([]) 15 | 16 | const editHandler = (productId) => { 17 | setSelectedProductId(productId) 18 | setShowEditProduct(true) 19 | } 20 | 21 | const deleteHandler = async (productId) => { 22 | setSelectedProductId(productId) 23 | setShowDeleteProduct(true) 24 | } 25 | 26 | const columns: GridColDef[] = [ 27 | { 28 | field: 'img', 29 | renderCell: (params) => { 30 | return 31 | }, 32 | }, 33 | { 34 | field: 'title', 35 | width: 300, 36 | }, 37 | { 38 | field: 'price', 39 | width: 200, 40 | renderCell: (params) => `Rp ${params.row.price.toLocaleString()}`, 41 | }, 42 | { 43 | field: 'actions', 44 | renderCell: (params) => { 45 | return ( 46 |
47 | 53 | 59 |
60 | ) 61 | }, 62 | }, 63 | ] 64 | 65 | const rows = products 66 | .sort((p1, p2) => p1.price - p2.price) 67 | .map((product) => { 68 | let data = { 69 | id: product.id, 70 | title: product.title, 71 | img: product.img, 72 | price: product.price, 73 | } 74 | return data 75 | }) 76 | 77 | const selectedProduct = useMemo( 78 | () => products.find((p) => p.id == selectedProductId), 79 | [selectedProductId, products] 80 | ) 81 | 82 | const selectedProducts = useMemo( 83 | () => selectedProductIds.map((id) => products.find((p) => p.id == id)), 84 | [selectedProductIds, products] 85 | ) 86 | 87 | return ( 88 |
89 | {selectedProductIds.length > 0 && ( 90 |
91 | 98 |
99 | )} 100 | { 107 | setSelectedProductsIds(productIds) 108 | }} 109 | /> 110 | {selectedProduct && ( 111 | { 117 | setShowEditProduct(false) 118 | setSelectedProductId('') 119 | }} 120 | /> 121 | )} 122 | {(selectedProduct || selectedProducts.length > 0) && ( 123 | { 130 | setShowDeleteProduct(false) 131 | setSelectedProductId('') 132 | setSelectedProductsIds([]) 133 | }} 134 | /> 135 | )} 136 |
137 | ) 138 | } 139 | 140 | export default ProductsTable 141 | -------------------------------------------------------------------------------- /components/home/RatingsModal.tsx: -------------------------------------------------------------------------------- 1 | import Modal from '../Modal' 2 | import { StarIcon as StarIconSolid } from '@heroicons/react/solid' 3 | import Rating from '../Rating' 4 | import { useEffect, useState } from 'react' 5 | import cn from 'classnames' 6 | import { XIcon } from '@heroicons/react/outline' 7 | 8 | const buttonClassname = 9 | 'flex-grow p-2 rounded-md border text-sm font-semibold flex items-center justify-center' 10 | 11 | const RatingsModal = ({ open, onClose, ratings }) => { 12 | const [filter, setFilter] = useState(5) 13 | const [filteredRatings, setFilteredRatings] = useState(ratings.ratings) 14 | 15 | useEffect(() => { 16 | let newRatings = [] 17 | const ratingsCopy = [...ratings.ratings] 18 | // star filter 19 | if (typeof filter == 'number') { 20 | newRatings = ratingsCopy.filter((rating) => rating.star == filter) 21 | } 22 | // time created filter 23 | else if (typeof filter == 'string') { 24 | newRatings = ratingsCopy.sort((a, b) => { 25 | const dateA = new Date(a.createdAt).getTime() 26 | const dateB = new Date(b.createdAt).getTime() 27 | if (filter == 'newest') return dateB - dateA 28 | if (filter == 'oldest') return dateA - dateB 29 | }) 30 | } 31 | setFilteredRatings(newRatings) 32 | }, [filter]) 33 | 34 | return ( 35 | 36 |
37 |

38 |
39 | Ratings{' '} 40 | 41 | ({ratings.ratings.length}) 42 | 43 |
44 | 50 |

51 |
52 | {[5, 4, 3, 2, 1].map((star) => ( 53 | 67 | ))} 68 | {['newest', 'oldest'].map((sortTo) => ( 69 | 82 | ))} 83 |
84 |
85 |
86 | {filteredRatings.length > 0 ? ( 87 | filteredRatings.map((rating) => ( 88 | 89 | )) 90 | ) : ( 91 |
92 | empty-rating 97 |

No ratings.

98 |
99 | )} 100 |
101 |
102 | ) 103 | } 104 | 105 | export default RatingsModal 106 | -------------------------------------------------------------------------------- /components/mui/Tabs.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | import Tab from '@mui/material/Tab' 3 | import Tabs from '@mui/material/Tabs' 4 | 5 | export const CustomTabs = styled(Tabs)({ 6 | '& .MuiTabs-indicator': { 7 | backgroundColor: 'rgb(34 197 94)', 8 | }, 9 | '.MuiTabs-scrollButtons.Mui-disabled': { 10 | opacity: 0.3, 11 | }, 12 | }) 13 | 14 | export const CustomTab = styled(Tab)({ 15 | fontWeight: 500, 16 | fontFamily: 'inherit', 17 | '&.Mui-focusVisible': { 18 | backgroundColor: 'rgb(188, 255, 212)', 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /components/profile/OrderHistory.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CheckCircleIcon, 3 | ClockIcon, 4 | TicketIcon, 5 | } from '@heroicons/react/outline' 6 | import Skeleton from 'react-loading-skeleton' 7 | import Link from '../Link' 8 | import ProductItem from '../ProductItem' 9 | import { StarIcon } from '@heroicons/react/solid' 10 | import useGetRequest from '../../hooks/useGetRequest' 11 | import usePayHandler from '../../hooks/usePayHandler' 12 | 13 | const OrderHistory = () => { 14 | const { data, loading } = useGetRequest('/orders/me') 15 | const [reorderHandler, launching] = usePayHandler() 16 | 17 | if (loading) 18 | return ( 19 | 25 | ) 26 | 27 | const { orders } = data 28 | 29 | console.log(orders) 30 | 31 | return ( 32 |
33 | {orders.length > 0 ? ( 34 | orders.map((order) => { 35 | return ( 36 |
40 | {/* HEADER */} 41 |
42 |

43 | 44 | {new Date(order.paidAt).toDateString()} at{' '} 45 | {new Date(order.paidAt).toLocaleTimeString()} 46 |

47 | 48 | 49 | {order.status} 50 | 51 |
52 | {/* BODY */} 53 | 54 | {/* PRODUCT */} 55 |
56 | 57 | {order.products.length > 1 && ( 58 |

59 | + {order.products.length - 1} other products 60 |

61 | )} 62 |
63 | {/* TOTAL */} 64 |
65 |
66 |

Total

67 |

68 | Rp {order.total.toLocaleString()} 69 |

70 | {order.promoCode && ( 71 | 72 | 73 | {order.promoCode} 74 | 75 | )} 76 |
77 | {order.rating && ( 78 |
79 | {' '} 80 | {order.rating.star} 81 |
82 | )} 83 |
84 | 85 | 86 | {/* REORDER BTN */} 87 | 96 |
97 | ) 98 | }) 99 | ) : ( 100 |
101 | empty order 106 |

You have no orders yet.

107 |
108 | )} 109 |
110 | ) 111 | } 112 | 113 | export default OrderHistory 114 | -------------------------------------------------------------------------------- /components/profile/TopUpInformation.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon, CloudUploadIcon } from '@heroicons/react/outline' 2 | import { useStateMachine } from 'little-state-machine' 3 | import { useEffect, useState } from 'react' 4 | import Skeleton from 'react-loading-skeleton' 5 | import useGetRequest from '../../hooks/useGetRequest' 6 | import RequirementField from '../RequirementField' 7 | import TopupRequirements from '../topup/TopupRequirements' 8 | 9 | const TopUpInformation = ({ user }) => { 10 | const { data: data1, loading: loading1 } = useGetRequest( 11 | '/categories?select=name,slug&topupOnly=true&hasRequirementOnly=true' 12 | ) 13 | 14 | const [selectedCategory, setselectedCategory] = useState('') 15 | const { data: data2, loading: loading2 } = useGetRequest( 16 | `/categories/${selectedCategory}` 17 | ) 18 | 19 | const { state } = useStateMachine() 20 | const { updatedDB, updatingDB } = state 21 | 22 | useEffect(() => { 23 | if (data1) { 24 | const { categories } = data1 25 | setselectedCategory(categories[0].slug) 26 | } 27 | }, [data1]) 28 | 29 | if (loading1 || loading2) { 30 | return ( 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 | ) 44 | } 45 | 46 | const { categories } = data1 47 | const { category } = data2 48 | 49 | console.log(category) 50 | 51 | return ( 52 |
53 | {/* HEADER */} 54 |
55 |
56 | {category?.slug} 61 | 72 |
73 |
74 | {updatingDB && ( 75 |
76 | saving 77 |
78 | )} 79 | {updatedDB && ( 80 |
81 | saved 82 |
83 | )} 84 |
85 |
86 | 87 | {/* FORM */} 88 |
89 |
90 | {category?.requirement.fields.map((field) => ( 91 | 97 | ))} 98 | 99 | 100 |
101 |
102 | ) 103 | } 104 | 105 | export default TopUpInformation 106 | -------------------------------------------------------------------------------- /components/topup/1000-baris/Starlight.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import MLSection from './MLSection' 3 | 4 | const Starlight = ({ 5 | Starlight1, 6 | Starlight2, 7 | Starlight3, 8 | Starlight4, 9 | setStarlight4, 10 | }) => { 11 | const [angka, setAngka] = useState(0) 12 | 13 | const tambah = () => { 14 | setAngka(angka + 1) 15 | } 16 | 17 | const kurang = () => { 18 | if (angka <= 0) { 19 | return 20 | } 21 | 22 | setAngka(angka - 1) 23 | } 24 | 25 | const delete4 = () => { 26 | setStarlight4(false) 27 | } 28 | 29 | return ( 30 | <> 31 | {Starlight1 && ( 32 |
33 | 34 |
35 | 36 |

Starlight Member

37 |
38 |
39 | 45 | {angka} 46 | 52 |
53 |
54 | )} 55 | 56 | {Starlight2 && ( 57 |
58 | 59 |
60 | 61 |

Starlight + 390 Diamonds

62 |
63 |
64 | 70 | {angka} 71 | 77 |
78 |
79 | )} 80 | 81 | {Starlight3 && ( 82 |
83 | 84 |
85 | 90 |

Twilight Pass

91 |
92 |
93 | 99 | {angka} 100 | 106 |
107 |
108 | )} 109 | 110 | {Starlight4 && ( 111 |
112 | 113 |
114 | 115 |

Starlight Member Plus

116 |
117 |
118 | 124 | {angka} 125 | 131 |
132 | 133 |
134 | )} 135 | 136 | ) 137 | } 138 | 139 | export default Starlight 140 | -------------------------------------------------------------------------------- /components/topup/TopupInfo.tsx: -------------------------------------------------------------------------------- 1 | const TopupInfo = ({ category }) => { 2 | return ( 3 |
4 | {category.slug} 9 |
10 | ML 11 |
12 |

{category.name}

13 | {/*

Developer

*/} 14 |
15 |
16 |
17 | Description 18 |

19 | {category.description} 20 |

21 |
22 |

23 | {category.description} 24 |

25 |
26 | ) 27 | } 28 | 29 | export default TopupInfo 30 | -------------------------------------------------------------------------------- /components/topup/TopupItem.tsx: -------------------------------------------------------------------------------- 1 | import { TrashIcon } from '@heroicons/react/outline' 2 | import cn from 'classnames' 3 | import { getProductInCart } from '../../lib/cartHandler' 4 | 5 | interface Props { 6 | product: any 7 | category: any 8 | cart?: any 9 | onAddToCart?: any 10 | onRemoveFromCart?: any 11 | onDecrementAmount?: any 12 | preview?: boolean 13 | } 14 | 15 | const TopupItem = ({ 16 | product, 17 | cart, 18 | category, 19 | onAddToCart, 20 | onRemoveFromCart, 21 | onDecrementAmount, 22 | preview = false, 23 | }: Props) => { 24 | const { isProductInCart, productInCart } = getProductInCart(product, cart) 25 | 26 | return ( 27 |
{ 30 | if (isProductInCart) return 31 | onAddToCart && onAddToCart({ product, category }) 32 | }} 33 | id={product.title} 34 | className={cn( 35 | 'px-4 py-3 border rounded-xl dark:bg-gray-700', 36 | isProductInCart 37 | ? 'border-green-400 dark:border-green-400' 38 | : 'hover:border-green-400 dark:hover:border-green-400 dark:border-gray-600' 39 | )} 40 | > 41 |
42 | {((preview && category.logoImg) || product.img) && ( 43 | 49 | c.slug == product.subCategory || 50 | c.slug == product.subCategory.slug 51 | ) 52 | : category 53 | )?.logoImg 54 | : product.img 55 | } 56 | className="w-10 h-10 object-cover rounded-lg mb-1.5" 57 | /> 58 | )} 59 | {isProductInCart && ( 60 | 66 | )} 67 |
68 |
69 |

70 | {product.title || (preview && 'Untitled Product')} 71 |

72 |

73 | Rp {product.price.toLocaleString()} 74 |

75 | {(isProductInCart || preview) && ( 76 |
77 |
78 | 87 |
{preview ? 1 : productInCart.amount}
88 | 97 |
98 | 104 |
105 | )} 106 |
107 |
108 | ) 109 | } 110 | 111 | export default TopupItem 112 | -------------------------------------------------------------------------------- /components/topup/TopupItems.tsx: -------------------------------------------------------------------------------- 1 | import { useStateMachine } from 'little-state-machine' 2 | import { useEffect, useMemo, useState } from 'react' 3 | import cn from 'classnames' 4 | import { 5 | CheckIcon, 6 | CloudUploadIcon, 7 | InformationCircleIcon, 8 | TrashIcon, 9 | } from '@heroicons/react/outline' 10 | import { 11 | addToCart, 12 | decrementAmount, 13 | getProductInCart, 14 | removeFromCart, 15 | editRequirement, 16 | } from '../../lib/cartHandler' 17 | import { signIn } from 'next-auth/react' 18 | import RequirementField from '../RequirementField' 19 | import TopupRequirements from './TopupRequirements' 20 | import { useRouter } from 'next/router' 21 | import { useUpdateEffect } from 'usehooks-ts' 22 | import { isObjectEmpty } from '../../lib/utils' 23 | import TopupItem from './TopupItem' 24 | 25 | const filterProductsBySubCategory = (subCategory, products) => { 26 | return subCategory 27 | ? products.filter((product) => product.subCategory?.slug == subCategory) 28 | : products 29 | } 30 | 31 | const TopupItems = ({ category, user }) => { 32 | const { state, actions } = useStateMachine({ 33 | addToCart, 34 | decrementAmount, 35 | removeFromCart, 36 | editRequirement, 37 | }) 38 | const { cart, updatedDB, updatingDB } = state 39 | 40 | const [currentSubCategory, setCurrentSubCategory] = useState( 41 | category.subCategories[0]?.slug 42 | ) 43 | const router = useRouter() 44 | 45 | // reset state on dynamic route changes 46 | useEffect(() => { 47 | setCurrentSubCategory(category.subCategories[0]?.slug) 48 | }, [router.asPath]) 49 | 50 | const products = useMemo(() => { 51 | return filterProductsBySubCategory(currentSubCategory, category.products) 52 | }, [currentSubCategory, category]) 53 | 54 | useEffect(() => { 55 | const { subCategory, title } = router.query 56 | if (subCategory) { 57 | setCurrentSubCategory(subCategory) 58 | } 59 | if (title) { 60 | document.getElementById(title as string)?.scrollIntoView() 61 | } 62 | }, [router.query]) 63 | 64 | useUpdateEffect(() => { 65 | const { title } = router.query 66 | if (title) { 67 | document.getElementById(title as string)?.scrollIntoView() 68 | } 69 | }, [currentSubCategory, router.query]) 70 | 71 | return ( 72 |
73 | {/* REQUIREMENT */} 74 | {category.requirement && ( 75 |
76 |
77 | 1 78 |
79 |

80 | {category.requirement.title}{' '} 81 | {updatingDB && ( 82 |
83 | saving 84 |
85 | )} 86 | {updatedDB && ( 87 |
88 | saved 89 |
90 | )} 91 |

92 | {!user && ( 93 |

94 | 95 | 96 | {' '} 102 | biar kesimpen di akunmu 103 | 104 |

105 | )} 106 | 107 |
108 | {category.requirement.fields.map((field) => ( 109 | 115 | ))} 116 | 117 | 118 |
119 | )} 120 | 121 | {/* CHOOSE */} 122 |
123 |
124 | {category.requirement ? 2 : 1} 125 |
126 |

Pilih Nominal Topup

127 | 128 | {/* SUB CATEGORY */} 129 | {category.subCategories.length > 0 && ( 130 |
131 |

Pilih Kategori

132 |
133 | {category.subCategories.map((subCategory) => ( 134 | 159 | ))} 160 |
161 |
162 | )} 163 | 164 | {/* ITEMS */} 165 |
166 |

Pilih Item

167 |
168 | {products.map((product) => ( 169 | 178 | ))} 179 |
180 |
181 |
182 |
183 | ) 184 | } 185 | 186 | export default TopupItems 187 | -------------------------------------------------------------------------------- /components/topup/TopupRequirements.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import cn from 'classnames' 3 | 4 | const TopupRequirements = ({ 5 | category, 6 | isOpen = false, 7 | }: { 8 | category: any 9 | isOpen?: boolean 10 | }) => { 11 | const [isZoom, setIsZoom] = useState(false) 12 | 13 | const zoomHandler = () => { 14 | setIsZoom(!isZoom) 15 | } 16 | 17 | return category?.requirement.img || category?.requirement.description ? ( 18 |
19 | Details 20 |
21 | {category.requirement.img && ( 22 |
26 | {category.requirement.title} 34 |
35 | )} 36 | 37 | {category?.requirement.description && ( 38 |

{category.requirement.description}

39 | )} 40 |
41 |
42 | ) : null 43 | } 44 | 45 | export default TopupRequirements 46 | -------------------------------------------------------------------------------- /hooks/useClickOutside.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const useClickOutside = (callback, open) => { 4 | const ref = React.useRef(null) 5 | 6 | React.useEffect(() => { 7 | const { current: el } = ref 8 | const handleClick = (e) => { 9 | // run callback if open is true and el is not null 10 | if (open && el && !el.contains(e.target)) { 11 | console.log('run callback') 12 | callback(e) 13 | } 14 | } 15 | 16 | document.addEventListener('click', handleClick) 17 | return () => { 18 | document.removeEventListener('click', handleClick) 19 | } 20 | }, [open]) 21 | 22 | return ref 23 | } 24 | 25 | export default useClickOutside 26 | -------------------------------------------------------------------------------- /hooks/useGetRequest.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | import request from '../lib/request' 3 | 4 | const fetcher = (url: string) => request.get(url) 5 | 6 | const useGetRequest = (url: string) => { 7 | const { data, error } = useSWR(url, fetcher) 8 | const loading = !data && !error 9 | return { data: data?.data, loading, error } 10 | } 11 | 12 | export default useGetRequest 13 | -------------------------------------------------------------------------------- /hooks/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const useIsMounted = () => { 4 | const [isMounted, setIsMounted] = useState(false) 5 | 6 | useEffect(() => { 7 | setIsMounted(true) 8 | }, []) 9 | 10 | return isMounted 11 | } 12 | 13 | export default useIsMounted 14 | -------------------------------------------------------------------------------- /hooks/usePayHandler.ts: -------------------------------------------------------------------------------- 1 | import { useStateMachine } from 'little-state-machine' 2 | import { useSession } from 'next-auth/react' 3 | import { useRouter } from 'next/router' 4 | import { useEffect, useState } from 'react' 5 | import toast from 'react-hot-toast' 6 | import request from '../lib/request' 7 | 8 | const usePayHandler = () => { 9 | const router = useRouter() 10 | const { data: session } = useSession() 11 | const { state, actions } = useStateMachine({ 12 | clearOrder: (state) => { 13 | request.delete('/carts/me') 14 | return { 15 | ...state, 16 | cart: [], 17 | order: { 18 | ...state.order, 19 | categoryRequirements: [], 20 | missingRequirements: {}, 21 | subtotal: 0, 22 | tax: 0, 23 | discount: 0, 24 | total: 0, 25 | promoCode: '', 26 | }, 27 | } 28 | }, 29 | }) 30 | const { order } = state 31 | const [launching, setLaunching] = useState(false) 32 | 33 | useEffect(() => { 34 | if (document.getElementById('snap-midtrans-js') != undefined) return 35 | 36 | const midtransScriptUrl = 'https://app.sandbox.midtrans.com/snap/snap.js' 37 | const myMidtransClientKey = process.env.NEXT_PUBLIC_MIDTRANS_CLIENT_KEY 38 | 39 | let scriptTag = document.createElement('script') 40 | scriptTag.src = midtransScriptUrl 41 | scriptTag.id = 'snap-midtrans-js' 42 | scriptTag.setAttribute('data-client-key', myMidtransClientKey) 43 | 44 | document.body.appendChild(scriptTag) 45 | }, []) 46 | 47 | const payHandler = async ({ products, user }) => { 48 | let toastId: string 49 | try { 50 | toastId = toast.loading('Launching Payment Gateway...') 51 | setLaunching(true) 52 | // create new order 53 | const { data } = await request.post('/orders', { 54 | products: products.map((product) => ({ 55 | id: product.id, 56 | amount: product.amount, 57 | })), 58 | requirements: order.requirements, 59 | promoCode: order.promoCode, 60 | user: !user ? order.user : null, 61 | }) 62 | 63 | const newOrder = data.order 64 | const { paymentToken } = newOrder 65 | 66 | // pay with midtrans snap api 67 | // @ts-ignore 68 | window.snap.pay(paymentToken, { 69 | onPending: async (result) => { 70 | toast.success('Success. Redirecting to order summary page...', { 71 | id: toastId, 72 | }) 73 | const paidAt = new Date(result.transaction_time).toISOString() 74 | await request.put(`/orders/${result.order_id}`, { 75 | paymentMethod: result.payment_type, 76 | status: 'PAID', 77 | paidAt, 78 | discount: order.discount, 79 | promoCode: order.promoCode, 80 | }) 81 | if (session && order.discount > 0) { 82 | // change discountCodes isUsed to true 83 | request.put(`/discountCodes/${order.promoCode}`) 84 | } 85 | actions.clearOrder() 86 | 87 | router.push({ 88 | pathname: '/order', 89 | query: { 90 | orderId: result.order_id, 91 | }, 92 | }) 93 | }, 94 | onError: (error) => { 95 | console.log(error) 96 | request.delete(`/orders/${newOrder.id}`) 97 | }, 98 | onClose: () => { 99 | request.delete(`/orders/${newOrder.id}`) 100 | toast.remove(toastId) 101 | }, 102 | }) 103 | } catch (err) { 104 | let errMsg = '' 105 | if (err?.data?.message) { 106 | errMsg = err.data.message 107 | } else { 108 | errMsg = 'Please retry.' 109 | } 110 | toast.error(`Failed. ${errMsg}`, { id: toastId }) 111 | console.log(err) 112 | } finally { 113 | setLaunching(false) 114 | } 115 | } 116 | 117 | return [payHandler, launching] as [ 118 | ({ products, user }: { products: any; user: any }) => Promise, 119 | boolean 120 | ] 121 | } 122 | 123 | export default usePayHandler 124 | -------------------------------------------------------------------------------- /lib/admin.ts: -------------------------------------------------------------------------------- 1 | import { getSession } from 'next-auth/react' 2 | import toast from 'react-hot-toast' 3 | import { User } from '../types/next-auth' 4 | 5 | export const getUserAdmin = async ( 6 | ctx 7 | ): Promise< 8 | [ 9 | User | null, 10 | { 11 | redirect: { 12 | destination: string 13 | permanent: boolean 14 | } 15 | } | null 16 | ] 17 | > => { 18 | const session = await getSession(ctx) 19 | 20 | if (!session) { 21 | return [ 22 | null, 23 | { 24 | redirect: { 25 | destination: '/signin', 26 | permanent: false, 27 | }, 28 | }, 29 | ] 30 | } 31 | 32 | const { user } = session 33 | if (!['ADMIN', 'FAKE_ADMIN'].includes(user.role)) { 34 | return [ 35 | null, 36 | { 37 | redirect: { 38 | destination: '/', 39 | permanent: false, 40 | }, 41 | }, 42 | ] 43 | } 44 | 45 | return [user, null] 46 | } 47 | 48 | export const adminRequestHandler = async ({ 49 | loading, 50 | success, 51 | handler, 52 | onError, 53 | }: { 54 | loading: string 55 | success: string 56 | handler: () => void 57 | onError?: () => void 58 | }) => { 59 | let toastId: string 60 | try { 61 | toastId = toast.loading(loading) 62 | await handler() 63 | toast.success(success, { id: toastId }) 64 | } catch (err) { 65 | console.log(err) 66 | let errorMessage = '' 67 | if (err.status == 403) { 68 | errorMessage = 'Opps... fake admin is not allowed to do this operation' 69 | } else { 70 | errorMessage = 'Failed. Check console for details' 71 | } 72 | toast.error(errorMessage, { id: toastId }) 73 | onError && onError() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/apiHandler.ts: -------------------------------------------------------------------------------- 1 | import nc, { Middleware } from 'next-connect' 2 | import { NextApiRequest, NextApiResponse } from 'next' 3 | import { getSession } from 'next-auth/react' 4 | import { User } from 'next-auth' 5 | import { Role } from '@prisma/client' 6 | 7 | const apiHandler = () => { 8 | return nc({ 9 | onError: (err, req, res, next) => { 10 | console.error(err) 11 | res 12 | .status(err.status || 500) 13 | .json({ message: err.message || 'Something broke!', ...err }) 14 | }, 15 | onNoMatch: (req, res) => { 16 | res.status(404).json({ message: `${req.method} ${req.url} not found` }) 17 | }, 18 | }) 19 | } 20 | 21 | export default apiHandler 22 | 23 | interface ExtendedRequest extends NextApiRequest { 24 | user: User 25 | } 26 | 27 | export const checkAuth = 28 | (role: Role = 'USER'): Middleware => 29 | async (req, res, next) => { 30 | const session = await getSession({ req }) 31 | if (!session?.user) { 32 | throw { status: 401, message: 'you are not logged in' } 33 | } 34 | // check if role = ADMIN 35 | if (role == 'ADMIN' && session.user.role !== 'ADMIN') { 36 | throw { status: 403, message: 'forbidden' } 37 | } 38 | // @ts-ignore 39 | req.user = session.user 40 | next() 41 | } 42 | -------------------------------------------------------------------------------- /lib/cartHandler.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState } from 'little-state-machine' 2 | import request from './request' 3 | 4 | export const getProductInCart = (product, cart) => { 5 | const idx = cart?.findIndex((item) => item.id == product.id) 6 | const isProductInCart = idx >= 0 7 | const productInCart = cart?.[idx] 8 | return { isProductInCart, idx, productInCart } 9 | } 10 | 11 | const isRequirementInOrder = (categoryRequirements, newRequirement) => { 12 | const requirement = categoryRequirements.find( 13 | (requirement) => requirement.id == newRequirement.id 14 | ) 15 | return requirement 16 | } 17 | 18 | export const countTotal = (cart, order) => { 19 | const subtotal = cart.reduce((total, item) => { 20 | total += item.amount * item.price 21 | return total 22 | }, 0) 23 | 24 | const plusTax = subtotal + order.tax 25 | 26 | const total = plusTax - (order.discount / 100) * plusTax 27 | 28 | return { total, subtotal } 29 | } 30 | 31 | const removeCategoryRequirement = ({ 32 | productInCart, 33 | newCart, 34 | categoryRequirements, 35 | }) => { 36 | const isProductHasRequirement = categoryRequirements.find( 37 | (req) => req.categorySlug == productInCart.category.slug 38 | ) 39 | 40 | // if product has requirement, update categoryRequirements 41 | if (isProductHasRequirement) { 42 | // get different category slugs 43 | const diffCategorySlugs = [] 44 | for (const product of newCart) { 45 | const categorySlug = product.category.slug 46 | if (!diffCategorySlugs.includes(categorySlug)) { 47 | diffCategorySlugs.push(categorySlug) 48 | } 49 | } 50 | 51 | return categoryRequirements.filter((req) => { 52 | return diffCategorySlugs.includes(req.categorySlug) 53 | }) 54 | } 55 | 56 | return categoryRequirements 57 | } 58 | 59 | const checkUserRequirement = ({ categorySlug, order }) => { 60 | const myRequirements = order.requirements[categorySlug] 61 | 62 | const requiredFields = order.categoryRequirements 63 | .find((req) => req.categorySlug == categorySlug) 64 | ?.fields.map((reqField) => ({ slug: reqField.value, label: reqField.name })) 65 | 66 | if (!requiredFields) { 67 | delete order.missingRequirements[categorySlug] 68 | return order.missingRequirements 69 | } 70 | 71 | order.missingRequirements[categorySlug] = {} 72 | 73 | requiredFields.forEach((reqField) => { 74 | if ( 75 | !myRequirements || 76 | !(reqField.slug in myRequirements) || 77 | !myRequirements[reqField.slug] 78 | ) { 79 | order.missingRequirements[categorySlug][ 80 | reqField.slug 81 | ] = `${reqField.label} required` 82 | } else { 83 | delete order.missingRequirements[categorySlug][reqField.slug] 84 | } 85 | }) 86 | 87 | return order.missingRequirements 88 | } 89 | 90 | const updateUserCart = (products) => { 91 | return request.put('/carts/me', { products }) 92 | } 93 | 94 | export const addToCart = ( 95 | state: GlobalState, 96 | payload: { product; category?: any } 97 | ): GlobalState => { 98 | const product = { 99 | ...payload.product, 100 | amount: 1, 101 | } 102 | 103 | const newCart = [...state.cart] 104 | 105 | const { isProductInCart, idx } = getProductInCart(product, state.cart) 106 | if (isProductInCart) { 107 | newCart.splice(idx, 1, { 108 | ...product, 109 | amount: state.cart[idx].amount + 1, 110 | }) 111 | } else { 112 | newCart.push(product) 113 | } 114 | 115 | const { subtotal, total } = countTotal(newCart, state.order) 116 | 117 | const newState = { 118 | ...state, 119 | cart: newCart, 120 | order: { 121 | ...state.order, 122 | subtotal, 123 | total, 124 | }, 125 | } 126 | 127 | // store category requirement 128 | if (payload.category?.requirement) { 129 | const newRequirement = { 130 | categorySlug: payload.category.slug, 131 | categoryName: payload.category.name, 132 | categoryLogo: payload.category.logoImg, 133 | ...payload.category.requirement, 134 | } 135 | if ( 136 | !isRequirementInOrder(newState.order.categoryRequirements, newRequirement) 137 | ) { 138 | newState.order.categoryRequirements.push(newRequirement) 139 | } 140 | } 141 | 142 | newState.order.missingRequirements = checkUserRequirement({ 143 | categorySlug: payload.category?.slug, 144 | order: state.order, 145 | }) 146 | 147 | updateUserCart(newCart) 148 | return newState 149 | } 150 | 151 | export const decrementAmount = ( 152 | state: GlobalState, 153 | payload: { product } 154 | ): GlobalState => { 155 | let newCart = [...state.cart] 156 | let newOrder = { ...state.order } 157 | 158 | const { idx, productInCart } = getProductInCart(payload.product, state.cart) 159 | const newAmount = productInCart.amount - 1 160 | if (newAmount <= 0) { 161 | newCart.splice(idx, 1) 162 | newOrder.categoryRequirements = removeCategoryRequirement({ 163 | productInCart, 164 | newCart, 165 | categoryRequirements: newOrder.categoryRequirements, 166 | }) 167 | newOrder.missingRequirements = checkUserRequirement({ 168 | categorySlug: payload.product.category.slug, 169 | order: newOrder, 170 | }) 171 | } else { 172 | newCart.splice(idx, 1, { 173 | ...productInCart, 174 | amount: newAmount, 175 | }) 176 | } 177 | 178 | const { subtotal, total } = countTotal(newCart, state.order) 179 | newOrder = { ...newOrder, subtotal, total } 180 | 181 | updateUserCart(newCart) 182 | 183 | return { 184 | ...state, 185 | cart: newCart, 186 | order: newOrder, 187 | } 188 | } 189 | 190 | export const removeFromCart = (state: GlobalState, payload): GlobalState => { 191 | let newCart = [...state.cart] 192 | let newOrder = { ...state.order } 193 | const { idx, productInCart } = getProductInCart(payload, state.cart) 194 | newCart.splice(idx, 1) 195 | 196 | newOrder.categoryRequirements = removeCategoryRequirement({ 197 | productInCart, 198 | newCart, 199 | categoryRequirements: state.order.categoryRequirements, 200 | }) 201 | 202 | newOrder.missingRequirements = checkUserRequirement({ 203 | categorySlug: payload.category.slug, 204 | order: newOrder, 205 | }) 206 | 207 | const { subtotal, total } = countTotal(newCart, state.order) 208 | newOrder = { ...newOrder, subtotal, total } 209 | updateUserCart(newCart) 210 | return { 211 | ...state, 212 | cart: newCart, 213 | order: newOrder, 214 | } 215 | } 216 | 217 | export const editRequirement = ( 218 | state: GlobalState, 219 | payload: { 220 | categorySlug: string 221 | fieldName: string 222 | fieldValue: string 223 | } 224 | ) => { 225 | let newOrder = { ...state.order } 226 | 227 | newOrder.requirements[payload.categorySlug] = { 228 | ...newOrder.requirements[payload.categorySlug], 229 | [payload.fieldName]: payload.fieldValue, 230 | } 231 | 232 | newOrder.missingRequirements = checkUserRequirement({ 233 | categorySlug: payload.categorySlug, 234 | order: newOrder, 235 | }) 236 | 237 | return { 238 | ...state, 239 | order: newOrder, 240 | } 241 | } 242 | 243 | export const setRequirements = (state, payload) => { 244 | return { 245 | ...state, 246 | order: { 247 | ...state.order, 248 | requirements: payload, 249 | }, 250 | } 251 | } 252 | 253 | export const setCart = (state: GlobalState, payload): GlobalState => { 254 | const newOrder = { ...state.order } 255 | const { total, subtotal } = countTotal(payload, state.order) 256 | newOrder.total = total 257 | newOrder.subtotal = subtotal 258 | 259 | // TODO: set category requirements from db 260 | 261 | // TODO: TEMP! verify each product requirement 262 | payload.forEach((product) => { 263 | // store category requirement 264 | if (product.category?.requirement) { 265 | const newRequirement = { 266 | categorySlug: product.category.slug, 267 | categoryName: product.category.name, 268 | categoryLogo: product.category.logoImg, 269 | ...product.category.requirement, 270 | } 271 | if ( 272 | !isRequirementInOrder(newOrder.categoryRequirements, newRequirement) 273 | ) { 274 | newOrder.categoryRequirements.push(newRequirement) 275 | } 276 | } 277 | 278 | newOrder.missingRequirements = checkUserRequirement({ 279 | categorySlug: product.category?.slug, 280 | order: newOrder, 281 | }) 282 | }) 283 | 284 | return { 285 | ...state, 286 | cart: payload, 287 | order: { 288 | ...state.order, 289 | total, 290 | subtotal, 291 | }, 292 | } 293 | } 294 | 295 | export const setOrderPromoCode = (state, payload) => { 296 | const newOrder = { 297 | ...state.order, 298 | promoCode: payload.code, 299 | discount: payload.discountPercent, 300 | } 301 | const { total } = countTotal(state.cart, newOrder) 302 | newOrder.total = total 303 | return { 304 | ...state, 305 | order: newOrder, 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /lib/data.ts: -------------------------------------------------------------------------------- 1 | // TODO: update hrefnya 2 | export const socialMedia = [ 3 | { 4 | id: 'ig', 5 | name: 'Instagram', 6 | img: '/images/icon/instagram.png', 7 | href: 'https://www.instagram.com/rausky.gamestore/', 8 | }, 9 | { 10 | id: 'line', 11 | name: 'LINE', 12 | img: '/images/icon/line.png', 13 | href: 'https://line.me/R/ti/p/%40983nqcnr', 14 | }, 15 | ] 16 | 17 | export const defaultAvatar = 18 | 'https://www.kindpng.com/picc/m/207-2074624_white-gray-circle-avatar-png-transparent-png.png' 19 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | declare global { 4 | // allow global `var` declarations 5 | // eslint-disable-next-line no-var 6 | var prisma: PrismaClient | undefined 7 | } 8 | 9 | const prisma = global.prisma || new PrismaClient() 10 | 11 | if (process.env.NODE_ENV !== 'production') global.prisma = prisma 12 | 13 | export default prisma 14 | -------------------------------------------------------------------------------- /lib/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const request = axios.create() 4 | 5 | // set baseURL to current url origin before sending request 6 | request.interceptors.request.use((config) => { 7 | config.baseURL = window.location.origin + '/api' 8 | return config 9 | }) 10 | 11 | // before sending response to client 12 | request.interceptors.response.use( 13 | function (response) { 14 | // Any status code that lie within the range of 2xx cause this function to trigger 15 | // Do something with response data 16 | return response 17 | }, 18 | function (error) { 19 | // redirect to /signin page if user is not logged in 20 | // if (error.response.status == 401) { 21 | // Router.push('/signin') 22 | // } 23 | return Promise.reject(error.response ?? error) 24 | } 25 | ) 26 | 27 | export default request 28 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const parseData = (obj) => JSON.parse(JSON.stringify(obj)) 2 | export const isObjectEmpty = (obj) => Object.keys(obj).length == 0 3 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rausky-store", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.9.3", 13 | "@emotion/styled": "^11.9.3", 14 | "@heroicons/react": "^1.0.6", 15 | "@mui/material": "^5.8.4", 16 | "@mui/x-data-grid": "^5.12.2", 17 | "@next-auth/prisma-adapter": "^1.0.3", 18 | "@prisma/client": "^3.14.0", 19 | "axios": "^0.27.2", 20 | "chart.js": "^3.8.0", 21 | "classnames": "^2.3.1", 22 | "framer-motion": "^6.3.3", 23 | "little-state-machine": "^4.3.2", 24 | "midtrans-client": "^1.3.1", 25 | "next": "^12.1.6", 26 | "next-auth": "^4.3.4", 27 | "next-connect": "^0.12.2", 28 | "nextjs-progressbar": "^0.0.14", 29 | "nookies": "^2.5.2", 30 | "react": "17.0.2", 31 | "react-chartjs-2": "^4.2.0", 32 | "react-confetti": "^6.1.0", 33 | "react-dom": "17.0.2", 34 | "react-hot-toast": "^2.2.0", 35 | "react-hotkeys-hook": "^3.4.6", 36 | "react-loading-skeleton": "^3.1.0", 37 | "react-rating-stars-component": "^2.2.0", 38 | "swr": "^1.3.0", 39 | "usehooks-ts": "^2.5.3" 40 | }, 41 | "devDependencies": { 42 | "@tailwindcss/forms": "^0.5.2", 43 | "@types/node": "^17.0.35", 44 | "@types/react": "^17.0.2", 45 | "autoprefixer": "^10.4.7", 46 | "depcheck": "^1.4.3", 47 | "eslint": "^7.32.0", 48 | "eslint-config-next": "^12.1.6", 49 | "postcss": "^8.4.13", 50 | "prisma": "^3.14.0", 51 | "tailwindcss": "^3.0.24", 52 | "typescript": "^4.6.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { SessionProvider, useSession } from 'next-auth/react' 3 | import NextNProgress from 'nextjs-progressbar' 4 | import '../styles/globals.css' 5 | import { 6 | createStore, 7 | StateMachineProvider, 8 | useStateMachine, 9 | } from 'little-state-machine' 10 | import ContinuePayBtn from '../components/ContinuePayBtn' 11 | import 'react-loading-skeleton/dist/skeleton.css' 12 | import { useRouter } from 'next/router' 13 | import request from '../lib/request' 14 | import { setRequirements, setCart, setOrderPromoCode } from '../lib/cartHandler' 15 | import { Toaster } from 'react-hot-toast' 16 | import { AnimatePresence, motion } from 'framer-motion' 17 | 18 | createStore( 19 | { 20 | globalTheme: 'device', 21 | cart: [], 22 | order: { 23 | user: {}, 24 | requirements: {}, 25 | categoryRequirements: [], 26 | missingRequirements: {}, 27 | subtotal: 0, 28 | tax: 0, 29 | discount: 0, 30 | total: 0, 31 | }, 32 | }, 33 | { 34 | name: 'state', 35 | } 36 | ) 37 | 38 | function MyApp({ Component, pageProps: { session, ...pageProps } }) { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | export default MyApp 49 | 50 | const MyComponent = ({ Component, pageProps }) => { 51 | const router = useRouter() 52 | const { data: session } = useSession() 53 | 54 | const { actions } = useStateMachine({ 55 | setRequirements, 56 | setCart, 57 | setOrderPromoCode, 58 | setGlobalTheme: (state, payload) => ({ ...state, globalTheme: payload }), 59 | }) 60 | 61 | useEffect(() => { 62 | const setMyCart = async () => { 63 | try { 64 | const res = await request.get('/carts/me') 65 | actions.setCart(res.data.cart.products) 66 | } catch (err) {} 67 | } 68 | const setMyRequirements = async () => { 69 | try { 70 | const res = await request.get('/requirements') 71 | actions.setRequirements(res.data.requirements) 72 | } catch (err) {} 73 | } 74 | 75 | const getUserData = async () => { 76 | await setMyRequirements() 77 | await setMyCart() 78 | } 79 | getUserData() 80 | 81 | const html = document.documentElement 82 | actions.setGlobalTheme(html.classList.contains('dark') ? 'dark' : 'light') 83 | }, []) 84 | 85 | useEffect(() => { 86 | if (!session) { 87 | actions.setOrderPromoCode({ 88 | code: '', 89 | discountPercent: 0, 90 | }) 91 | } 92 | }, [session, actions]) 93 | 94 | return ( 95 | <> 96 | 97 | {/* TODO: make animation smoother */} 98 | 99 |
100 | 110 | 111 | 112 |
113 |
114 | 115 | {!['cart', 'signin', 'order', 'admin'].includes( 116 | router.route.split('/')[1] 117 | ) && } 118 | 123 | 124 | ) 125 | } 126 | -------------------------------------------------------------------------------- /pages/_document.jsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | import Script from 'next/script' 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | 13 | 14 | 19 | 23 | 24 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /pages/admin/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import AdminContainer from '../../components/admin/AdminContainer' 3 | import { getUserAdmin } from '../../lib/admin' 4 | import { User } from '../../types/next-auth' 5 | 6 | interface Props { 7 | user: User 8 | } 9 | 10 | const Admin = ({ user }: Props) => { 11 | return admin content 12 | } 13 | 14 | export default Admin 15 | 16 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 17 | const [user, error] = await getUserAdmin(ctx) 18 | if (error) return error 19 | return { 20 | props: { 21 | user, 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pages/admin/orders.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import AdminContainer from '../../components/admin/AdminContainer' 3 | import { getUserAdmin } from '../../lib/admin' 4 | 5 | const Orders = ({ user }) => { 6 | return Orders 7 | } 8 | 9 | export default Orders 10 | 11 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 12 | const [user, error] = await getUserAdmin(ctx) 13 | if (error) return error 14 | return { 15 | props: { 16 | user, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pages/admin/products.tsx: -------------------------------------------------------------------------------- 1 | import { PencilIcon, PlusSmIcon } from '@heroicons/react/outline' 2 | import { GetServerSideProps } from 'next' 3 | import { useState } from 'react' 4 | import AdminContainer from '../../components/admin/AdminContainer' 5 | import { getUserAdmin } from '../../lib/admin' 6 | import { parseData } from '../../lib/utils' 7 | import { getAllCategories } from '../api/categories' 8 | import AddProductsModal from '../../components/admin/products/AddProductsModal' 9 | import Chart from '../../components/Chart' 10 | import ProductsTable from '../../components/admin/products/ProductsTable' 11 | 12 | const Products = ({ user, categories: categoriesFromServer }) => { 13 | const [categories, setCategories] = useState(categoriesFromServer) 14 | const [categoryIndex, setCategoryIndex] = useState(0) 15 | const [showAddProducts, setShowAddProducts] = useState(false) 16 | 17 | const category = categories[categoryIndex] 18 | console.log(category) 19 | 20 | const editLogoImg = () => { 21 | prompt(`Logo image path/url`) 22 | } 23 | 24 | const editBannerImg = () => { 25 | prompt(`Banner image path/url`) 26 | } 27 | 28 | return ( 29 | 30 | {/* HEADER */} 31 |
32 | {/* SELECT CATEGORY */} 33 |
34 |

Category

35 | 38 |
39 | 49 |
50 | 51 |
52 | {/* CATEGORY OVERVIEW */} 53 |
54 | {/* BANNER */} 55 | {category.bannerImg ? ( 56 | 69 | ) : ( 70 | 73 | )} 74 |
75 | {/* LOGO */} 76 | {category.logoImg ? ( 77 | 90 | ) : ( 91 | 94 | )} 95 | 96 |
97 |

{category.name}

98 | {/* TODO: bikin edit category detail -> munculin modal buat ngedit name, description, requirements, subcategories */} 99 | 102 |
103 |
104 |
105 | 106 | {/* TODO: ganti hardcoded category chart dengan dynamic data */} 107 | {/* CATEGORY CHART */} 108 |
109 |
110 |
111 |

Total Revenue

112 | 117 |
118 |

Rp 5,000,000

119 |
120 |
121 |
122 |

Total Sales

123 | 128 |
129 | 152 |
153 |
154 |
155 | 156 | {/* PRODUCTS */} 157 |
158 |
159 |

160 | Products{' '} 161 | 162 | ({category.products.length}) 163 | 164 |

165 | 171 | setShowAddProducts(false)} 174 | setCategories={setCategories} 175 | category={category} 176 | /> 177 |
178 | 179 | {/* TODO: tampilin products pake table mui */} 180 | 181 |
182 |
183 | ) 184 | } 185 | 186 | export default Products 187 | 188 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 189 | const [user, error] = await getUserAdmin(ctx) 190 | if (error) return error 191 | 192 | const categories = await getAllCategories({ 193 | include: 'subCategories,products', 194 | productInclude: 'subCategory', 195 | }) 196 | 197 | return parseData({ 198 | props: { 199 | user, 200 | categories, 201 | }, 202 | }) 203 | } 204 | -------------------------------------------------------------------------------- /pages/admin/users.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import AdminContainer from '../../components/admin/AdminContainer' 3 | import { getUserAdmin } from '../../lib/admin' 4 | 5 | const Users = ({ user }) => { 6 | return Users 7 | } 8 | 9 | export default Users 10 | 11 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 12 | const [user, error] = await getUserAdmin(ctx) 13 | if (error) return error 14 | return { 15 | props: { 16 | user, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth' 2 | import GoogleProvider from 'next-auth/providers/google' 3 | import GithubProvider from 'next-auth/providers/github' 4 | import DiscordProvider from 'next-auth/providers/discord' 5 | import { PrismaAdapter } from '@next-auth/prisma-adapter' 6 | import prisma from '../../../lib/prisma' 7 | 8 | export default NextAuth({ 9 | adapter: PrismaAdapter(prisma), 10 | providers: [ 11 | GoogleProvider({ 12 | clientId: process.env.GOOGLE_ID, 13 | clientSecret: process.env.GOOGLE_SECRET, 14 | }), 15 | DiscordProvider({ 16 | clientId: process.env.DISCORD_ID, 17 | clientSecret: process.env.DISCORD_SECRET, 18 | }), 19 | GithubProvider({ 20 | clientId: process.env.GITHUB_ID, 21 | clientSecret: process.env.GITHUB_SECRET, 22 | }), 23 | ], 24 | callbacks: { 25 | async jwt({ token, user, account, profile, isNewUser }) { 26 | user && (token.user = user) 27 | return token 28 | }, 29 | async session({ session, token, user }) { 30 | // set user information from db to session.user 31 | const userData = await prisma.user.findUnique({ where: { id: user.id } }) 32 | session.user = userData 33 | 34 | return session 35 | }, 36 | }, 37 | pages: { 38 | signIn: '/signin', 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /pages/api/authToken.js: -------------------------------------------------------------------------------- 1 | import apiHandler from '../../lib/apiHandler' 2 | import nookies from 'nookies' 3 | 4 | const app = apiHandler() 5 | 6 | // this api route is used for testing in postman 7 | export default app.post((req, res) => { 8 | const { sessionToken, csrfToken } = req.body 9 | if (!sessionToken || !csrfToken) { 10 | throw { status: 400, message: 'please provide sessionToken, csrfToken' } 11 | } 12 | nookies.set({ res }, 'next-auth.session-token', sessionToken, { 13 | httpOnly: true, 14 | path: '/', 15 | }) 16 | nookies.set({ res }, 'next-auth.csrf-token', csrfToken, { 17 | httpOnly: true, 18 | path: '/', 19 | }) 20 | res.send('success') 21 | }) 22 | -------------------------------------------------------------------------------- /pages/api/carts/me.ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | export default app 7 | // get my cart 8 | .get(checkAuth(), async (req, res) => { 9 | const myCart = await prisma.cart.findUnique({ 10 | where: { userId: req.user.id }, 11 | }) 12 | res.status(200).json({ cart: myCart ?? [] }) 13 | }) 14 | // create or update my cart 15 | .put(checkAuth(), async (req, res) => { 16 | const { products } = req.body 17 | if (!products) { 18 | throw { status: 400, message: 'Please provide products' } 19 | } 20 | const myCart = await prisma.cart.upsert({ 21 | where: { userId: req.user.id }, 22 | create: { products, userId: req.user.id }, 23 | update: { products }, 24 | }) 25 | res.status(200).json({ cart: myCart }) 26 | }) 27 | // delete my cart 28 | .delete(checkAuth(), async (req, res) => { 29 | await prisma.cart.delete({ 30 | where: { userId: req.user.id }, 31 | }) 32 | res 33 | .status(200) 34 | .json({ message: `success delete cart owned by user id ${req.user.id}` }) 35 | }) 36 | -------------------------------------------------------------------------------- /pages/api/categories/[categoryId].ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | export const getSpecificCategory = async ({ 5 | categorySlug, 6 | includeProducts, 7 | }: { 8 | categorySlug: string 9 | includeProducts?: boolean 10 | }) => { 11 | let query: CustomObject = { 12 | where: { 13 | slug: categorySlug, 14 | }, 15 | include: { 16 | subCategories: true, 17 | requirement: { 18 | include: { 19 | fields: { 20 | select: { 21 | placeholder: true, 22 | type: true, 23 | id: true, 24 | value: true, 25 | name: true, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | } 32 | 33 | if (includeProducts) { 34 | query.include.products = { 35 | orderBy: { price: 'asc' }, 36 | include: { 37 | subCategory: { select: { name: true, slug: true } }, 38 | category: { 39 | select: { 40 | name: true, 41 | slug: true, 42 | logoImg: true, 43 | requirement: { 44 | include: { 45 | fields: { 46 | select: { 47 | placeholder: true, 48 | type: true, 49 | id: true, 50 | value: true, 51 | name: true, 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | } 60 | } 61 | 62 | // @ts-ignore 63 | const category = await prisma.category.findUnique(query) 64 | 65 | return category 66 | } 67 | 68 | const app = apiHandler() 69 | 70 | export default app 71 | // get specific category 72 | .get(async (req, res) => { 73 | const { categoryId: categorySlug, includeProducts } = req.query as { 74 | [key: string]: string 75 | } 76 | const category = await getSpecificCategory({ 77 | categorySlug, 78 | includeProducts: includeProducts == 'true' ? true : false, 79 | }) 80 | res.status(200).json({ category }) 81 | }) 82 | // edit category 83 | .put(checkAuth('ADMIN'), async (req, res) => { 84 | const category = await prisma.category.update({ 85 | where: { id: req.query.categoryId as string }, 86 | data: req.body, 87 | }) 88 | res.status(200).json({ category }) 89 | }) 90 | // delete category 91 | .delete(checkAuth('ADMIN'), async (req, res) => { 92 | const categoryId = req.query.categoryId as string 93 | await prisma.product.delete({ 94 | where: { id: categoryId }, 95 | }) 96 | res.status(200).json({ message: `Success delete categoryId ${categoryId}` }) 97 | }) 98 | -------------------------------------------------------------------------------- /pages/api/categories/index.ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | interface GetAllCategoriesProps { 5 | select?: string 6 | include?: string 7 | productInclude?: string 8 | topupOnly?: boolean 9 | hasRequirementOnly?: boolean 10 | search?: string 11 | } 12 | 13 | export const getAllCategories = async ({ 14 | select, 15 | include, 16 | productInclude, 17 | topupOnly, 18 | hasRequirementOnly, 19 | search, 20 | }: GetAllCategoriesProps) => { 21 | let query: CustomObject = { 22 | where: {}, 23 | } 24 | 25 | if (select) { 26 | query.select = select.split(',').reduce((acc, field) => { 27 | acc[field] = true 28 | return acc 29 | }, {}) 30 | } 31 | 32 | if (include) { 33 | query.include = include.split(',').reduce((acc, field) => { 34 | acc[field] = true 35 | return acc 36 | }, {}) 37 | } 38 | 39 | if (productInclude && query.include.products) { 40 | query.include.products = { 41 | include: productInclude.split(',').reduce((acc, field) => { 42 | acc[field] = true 43 | return acc 44 | }, {}), 45 | } 46 | } 47 | 48 | if (topupOnly) { 49 | query.where.isTopup = true 50 | } 51 | 52 | if (hasRequirementOnly) { 53 | query.where.requirement = { isNot: null } 54 | } 55 | 56 | // @ts-ignore 57 | let categories = await prisma.category.findMany(query) 58 | 59 | if (search) { 60 | categories = categories.filter((category) => 61 | category.name.toLowerCase().includes(search.toLowerCase()) 62 | ) 63 | } 64 | 65 | return categories 66 | } 67 | 68 | const app = apiHandler() 69 | 70 | export default app 71 | // get all categories 72 | .get(async (req, res) => { 73 | const { select, topupOnly, hasRequirementOnly, search } = req.query as { 74 | [key: string]: string 75 | } 76 | 77 | const categories = await getAllCategories({ 78 | select, 79 | topupOnly: topupOnly == 'true' ? true : false, 80 | hasRequirementOnly: hasRequirementOnly == 'true' ? true : false, 81 | search, 82 | }) 83 | res.status(200).json({ categories, length: categories.length }) 84 | }) 85 | // create new category 86 | .post(checkAuth('ADMIN'), async (req, res) => { 87 | if (!req.body.name) { 88 | throw { status: 400, message: 'Please provide name' } 89 | } 90 | 91 | req.body.slug = req.body.name.toLowerCase().replace(/ /g, '-') 92 | 93 | const category = await prisma.category.create({ 94 | data: req.body, 95 | }) 96 | 97 | res.status(201).json({ category }) 98 | }) 99 | -------------------------------------------------------------------------------- /pages/api/discountCodes/[code].ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | app.get(checkAuth(), async (req, res) => { 7 | const { code } = req.query 8 | const discountCode = await prisma.discountCode.findUnique({ 9 | where: { code: code as string }, 10 | }) 11 | 12 | if (!discountCode) { 13 | throw { status: 404, message: `Promo Code '${code}' is not available` } 14 | } 15 | 16 | const promoData = await prisma.usersOnDiscountCodes.findFirst({ 17 | where: { 18 | discountCodeId: discountCode.id, 19 | userId: req.user.id, 20 | }, 21 | }) 22 | if (promoData) { 23 | if (promoData.isUsed) { 24 | throw { status: 400, message: `You already use '${code}'` } 25 | } 26 | res.status(200).json({ 27 | message: `Promo Code '${code}' applied`, 28 | promoCode: { 29 | code: discountCode.code, 30 | discountPercent: discountCode.discountPercent, 31 | }, 32 | }) 33 | return 34 | } 35 | 36 | if (discountCode.quota === 0) { 37 | throw { status: 404, message: `Promo Code '${code}' quota exhausted` } 38 | } 39 | 40 | // check if Promo code date still valid 41 | const now = new Date() 42 | if (discountCode.validUntil && now > discountCode.validUntil) { 43 | throw { status: 403, message: `Promo Code '${code}' is no longer valid` } 44 | } 45 | 46 | const updatedPromoCode = await prisma.discountCode.update({ 47 | where: { 48 | code: code as string, 49 | }, 50 | data: { 51 | quota: { decrement: 1 }, 52 | users: { 53 | create: { 54 | userId: req.user.id, 55 | }, 56 | }, 57 | }, 58 | select: { 59 | discountPercent: true, 60 | code: true, 61 | }, 62 | }) 63 | 64 | res.status(200).json({ 65 | message: `Promo Code '${code}' applied`, 66 | promoCode: updatedPromoCode, 67 | }) 68 | }) 69 | 70 | app.put(checkAuth(), async (req, res) => { 71 | const { code } = req.query 72 | 73 | const promoData = await prisma.usersOnDiscountCodes.findFirst({ 74 | where: { 75 | discountCode: { 76 | code: code as string, 77 | }, 78 | userId: req.user.id, 79 | }, 80 | select: { 81 | id: true, 82 | }, 83 | }) 84 | 85 | if (!promoData) { 86 | throw { status: 404, message: `Promo Code '${code}' is not available` } 87 | } 88 | 89 | await prisma.usersOnDiscountCodes.update({ 90 | where: { 91 | id: promoData.id, 92 | }, 93 | data: { 94 | isUsed: true, 95 | }, 96 | }) 97 | 98 | res.status(200).json({ message: 'Promo Code Updated' }) 99 | }) 100 | 101 | export default app 102 | -------------------------------------------------------------------------------- /pages/api/orders/[orderId].ts: -------------------------------------------------------------------------------- 1 | import apiHandler from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | export default app 7 | .get(async (req, res) => { 8 | const order = await prisma.order.findUnique({ 9 | where: { id: req.query.orderId as string }, 10 | include: { 11 | user: true, 12 | rating: true, 13 | products: { 14 | select: { 15 | product: { include: { category: true } }, 16 | amount: true, 17 | }, 18 | }, 19 | }, 20 | }) 21 | 22 | // @ts-ignore 23 | order.products = order.products.map(({ product, amount }) => ({ 24 | ...product, 25 | amount, 26 | })) 27 | res.status(200).json({ order }) 28 | }) 29 | .put(async (req, res) => { 30 | const { paymentMethod, status, paidAt, discount, promoCode } = req.body 31 | 32 | const where = { id: req.query.orderId as string } 33 | 34 | const include = { 35 | user: true, 36 | rating: true, 37 | products: { 38 | select: { 39 | product: { include: { category: true } }, 40 | amount: true, 41 | }, 42 | }, 43 | } 44 | 45 | let order = await prisma.order.findUnique({ 46 | where, 47 | include, 48 | }) 49 | 50 | if (order.status == 'WAITING_PAYMENT') { 51 | order = await prisma.order.update({ 52 | where, 53 | data: { 54 | paymentMethod, 55 | status, 56 | paidAt, 57 | discount, 58 | promoCode, 59 | }, 60 | include, 61 | }) 62 | } 63 | 64 | // @ts-ignore 65 | order.products = order.products.map(({ product, amount }) => ({ 66 | ...product, 67 | amount, 68 | })) 69 | res.status(200).json({ order }) 70 | }) 71 | .delete(async (req, res) => { 72 | await prisma.order.delete({ where: { id: req.query.orderId as string } }) 73 | res.status(200).send(`success delete order with id ${req.query.orderId}`) 74 | }) 75 | -------------------------------------------------------------------------------- /pages/api/orders/index.ts: -------------------------------------------------------------------------------- 1 | import apiHandler from '../../../lib/apiHandler' 2 | import midtransClient from 'midtrans-client' 3 | import { getSession } from 'next-auth/react' 4 | import prisma from '../../../lib/prisma' 5 | 6 | const app = apiHandler() 7 | 8 | let snap = new midtransClient.Snap({ 9 | // Set to true if you want Production Environment (accept real transaction). 10 | isProduction: false, 11 | serverKey: process.env.MIDTRANS_SERVER_KEY, 12 | }) 13 | 14 | export default app 15 | // create new order 16 | .post(async (req, res) => { 17 | const { products, requirements, user: reqUser, promoCode } = req.body 18 | if (!products || products.length == 0) { 19 | throw { 20 | status: 400, 21 | message: 'please provide valid products ([] of { id, amount })', 22 | } 23 | } 24 | 25 | const session = await getSession({ req }) 26 | const user = session?.user 27 | 28 | const order = await prisma.order.create({ 29 | data: { 30 | userId: user?.id, 31 | products: { 32 | create: products.map((product) => ({ 33 | productId: product.id, 34 | amount: product.amount, 35 | })), 36 | }, 37 | }, 38 | include: { 39 | products: { 40 | select: { 41 | amount: true, 42 | product: { 43 | select: { 44 | id: true, 45 | title: true, 46 | description: true, 47 | category: true, 48 | price: true, 49 | discount: true, 50 | }, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }) 56 | 57 | let distinctCategories = [] 58 | 59 | // calculate subtotal 60 | let subtotal = 0 61 | for (const item of order.products) { 62 | let discount = (item.product.discount / 100) * item.product.price 63 | subtotal += item.product.price * item.amount - discount 64 | const categorySlug = item.product.category.slug 65 | if (!distinctCategories.includes(categorySlug)) { 66 | distinctCategories.push(categorySlug) 67 | } 68 | } 69 | 70 | let discountPercent = 0 71 | // validate promoCode 72 | if (user && promoCode) { 73 | const promoData = await prisma.usersOnDiscountCodes.findFirst({ 74 | where: { 75 | userId: user.id, 76 | discountCode: { 77 | code: promoCode, 78 | }, 79 | }, 80 | select: { 81 | id: true, 82 | isUsed: true, 83 | discountCode: { 84 | select: { 85 | discountPercent: true, 86 | }, 87 | }, 88 | }, 89 | }) 90 | 91 | if (!promoData) { 92 | throw { status: 404, message: `You are not using '${promoCode}'` } 93 | } 94 | 95 | if (promoData.isUsed) { 96 | throw { status: 400, message: `You already use '${promoCode}'` } 97 | } 98 | 99 | discountPercent = promoData.discountCode.discountPercent 100 | } 101 | 102 | // calculate total 103 | const tax = 0 104 | const plusTax = subtotal + tax 105 | const total = plusTax - (discountPercent / 100) * plusTax 106 | 107 | // check requirements from req.body 108 | let invalidRequirements = [] 109 | for (const categorySlug of distinctCategories) { 110 | const category = await prisma.category.findUnique({ 111 | where: { slug: categorySlug }, 112 | select: { 113 | requirement: { 114 | select: { 115 | fields: { 116 | select: { value: true }, 117 | }, 118 | }, 119 | }, 120 | }, 121 | }) 122 | 123 | // skip if category doesn't has any requirement 124 | if (!category.requirement) continue 125 | 126 | if (!requirements?.[categorySlug]) { 127 | invalidRequirements.push(categorySlug) 128 | continue 129 | } 130 | 131 | const requiredFields = category.requirement.fields.map( 132 | (field) => field.value 133 | ) 134 | 135 | const isRequirementExist = requiredFields.every((requiredField) => { 136 | return ( 137 | requiredField in requirements[categorySlug] && 138 | requirements[categorySlug][requiredField] 139 | ) 140 | }) 141 | 142 | if (!isRequirementExist) { 143 | invalidRequirements.push(categorySlug) 144 | } 145 | } 146 | 147 | if (invalidRequirements.length > 0) { 148 | // delete unfinished order 149 | await prisma.order.delete({ where: { id: order.id } }) 150 | throw { 151 | status: 400, 152 | message: 'Please provide valid requirements', 153 | invalidRequirements, 154 | } 155 | } 156 | 157 | // create payment url and token 158 | const userName = user?.name ?? reqUser?.name 159 | const userEmail = user?.email ?? reqUser?.email 160 | 161 | let paymentData: CustomObject = { 162 | transaction_details: { 163 | order_id: order.id, 164 | gross_amount: total, 165 | }, 166 | credit_card: { 167 | secure: true, 168 | }, 169 | item_details: [ 170 | ...order.products.map((item) => ({ 171 | id: item.product.id, 172 | price: item.product.price, 173 | quantity: item.amount, 174 | name: item.product.title, 175 | })), 176 | ], 177 | } 178 | 179 | if (tax > 0) { 180 | paymentData.item_details = [ 181 | ...paymentData.item_details, 182 | { 183 | id: 'tax', 184 | price: tax, 185 | quantity: 1, 186 | name: 'Tax', 187 | }, 188 | ] 189 | } 190 | 191 | if (discountPercent > 0) { 192 | paymentData.item_details = [ 193 | ...paymentData.item_details, 194 | { 195 | id: 'discount', 196 | price: -((discountPercent / 100) * plusTax), 197 | quantity: 1, 198 | name: 'Discount', 199 | }, 200 | ] 201 | } 202 | 203 | if (userName || userEmail) { 204 | paymentData.customer_details = {} 205 | if (userName) paymentData.customer_details.first_name = userName 206 | if (userEmail) paymentData.customer_details.email = userEmail 207 | } 208 | 209 | try { 210 | const { token, redirect_url } = await snap.createTransaction(paymentData) 211 | 212 | let fullOrder = await prisma.order.update({ 213 | where: { id: order.id }, 214 | data: { 215 | total, 216 | tax, 217 | paymentToken: token, 218 | paymentUrl: redirect_url, 219 | requirements, 220 | }, 221 | include: { 222 | user: true, 223 | products: { 224 | select: { 225 | product: { include: { category: true } }, 226 | }, 227 | }, 228 | rating: true, 229 | }, 230 | }) 231 | 232 | // @ts-ignore 233 | fullOrder.products = fullOrder.products.map(({ product }) => product) 234 | 235 | res.status(201).json({ order: fullOrder }) 236 | } catch (err) { 237 | if (err.name == 'MidtransError') { 238 | throw { 239 | status: err.httpStatusCode, 240 | message: err.ApiResponse.error_messages, 241 | } 242 | } 243 | throw err 244 | } 245 | }) 246 | -------------------------------------------------------------------------------- /pages/api/orders/me.ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | export default app 7 | // get my orders 8 | .get(checkAuth(), async (req, res) => { 9 | let orders = await prisma.order.findMany({ 10 | where: { 11 | userId: req.user.id, 12 | }, 13 | include: { 14 | products: { 15 | select: { 16 | product: { include: { category: true } }, 17 | amount: true, 18 | }, 19 | }, 20 | rating: true, 21 | }, 22 | orderBy: { 23 | paidAt: 'desc', 24 | }, 25 | }) 26 | 27 | // @ts-ignore 28 | orders = orders.map((order) => { 29 | const products = order.products.map(({ product, amount }) => ({ 30 | ...product, 31 | amount, 32 | })) 33 | return { ...order, products } 34 | }) 35 | 36 | res.status(200).json({ orders, length: orders.length }) 37 | }) 38 | -------------------------------------------------------------------------------- /pages/api/products/[productId].ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | export default app 7 | // edit product 8 | .put(checkAuth('ADMIN'), async (req, res) => { 9 | const product = await prisma.product.update({ 10 | where: { id: req.query.productId as string }, 11 | data: req.body, 12 | include: { 13 | subCategory: true, 14 | }, 15 | }) 16 | res.status(200).json({ product }) 17 | }) 18 | // delete product 19 | .delete(checkAuth('ADMIN'), async (req, res) => { 20 | const productId = req.query.productId as string 21 | 22 | const product = await prisma.product.delete({ 23 | where: { id: productId }, 24 | }) 25 | 26 | res.status(200).json({ 27 | message: `Success delete productId ${productId}`, 28 | id: product.id, 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /pages/api/products/index.ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | const createNewProduct = async ({ user, product }) => { 7 | const { title, price, category, subCategory } = product 8 | 9 | if (title == undefined || price == undefined) { 10 | throw { status: 400, message: 'Please provide title, price' } 11 | } 12 | 13 | const categoryDb = await prisma.category.findUnique({ 14 | where: { slug: category }, 15 | select: { logoImg: true, subCategories: true, isTopup: true }, 16 | }) 17 | 18 | // logic for topup category only 19 | if (categoryDb.isTopup) { 20 | const hasSubCategories = categoryDb.subCategories.length > 0 21 | 22 | // but subCategory not in req.body, throw 400 23 | if (hasSubCategories && !subCategory) { 24 | throw { 25 | status: 400, 26 | message: 'Please provide subCategory for this product', 27 | } 28 | } 29 | 30 | product.img = ( 31 | hasSubCategories 32 | ? categoryDb.subCategories.find((sc) => sc.slug == subCategory) 33 | : categoryDb 34 | ).logoImg 35 | } 36 | 37 | if (subCategory) { 38 | product.subCategory = { connect: { slug: subCategory } } 39 | } 40 | 41 | return await prisma.product.create({ 42 | data: { 43 | ...product, 44 | category: { connect: { slug: category } }, 45 | user: { connect: { id: user.id } }, 46 | }, 47 | include: { 48 | subCategory: true, 49 | }, 50 | }) 51 | } 52 | 53 | export default app 54 | // get all products 55 | .get(async (req, res) => { 56 | const { category, from, to, discount, search, include } = req.query as { 57 | [key: string]: string 58 | } 59 | 60 | let includeQuery = {} 61 | if (include) { 62 | includeQuery = include.split(',').reduce((obj, prop) => { 63 | obj[prop] = true 64 | return obj 65 | }, {}) 66 | } 67 | 68 | let products = await prisma.product.findMany({ 69 | where: { 70 | price: { gte: from && +from, lte: to && +to }, 71 | discount: { gt: discount == 'true' ? 0 : undefined }, 72 | category: { slug: category }, 73 | }, 74 | include: includeQuery, 75 | }) 76 | 77 | if (search) { 78 | products = products.filter((product) => 79 | product.title.toLowerCase().includes(search.toLowerCase()) 80 | ) 81 | } 82 | 83 | res.status(200).json({ products, length: products.length }) 84 | }) 85 | 86 | // create new product 87 | .post(checkAuth('ADMIN'), async (req, res) => { 88 | if (Array.isArray(req.body)) { 89 | const products = await Promise.all( 90 | req.body.map((product) => createNewProduct({ user: req.user, product })) 91 | ) 92 | res.status(201).json({ products, count: products.length }) 93 | return 94 | } 95 | 96 | const product = await createNewProduct({ 97 | user: req.user, 98 | product: req.body, 99 | }) 100 | res.status(201).json({ product }) 101 | }) 102 | -------------------------------------------------------------------------------- /pages/api/products/setDiscount.ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | 3 | const app = apiHandler() 4 | 5 | export default app 6 | // set discount to all products in category 7 | .post(checkAuth('ADMIN'), async (req, res) => { 8 | const { discount, category } = req.body 9 | if (discount == undefined) { 10 | throw { status: 400, message: 'Please provide discount' } 11 | } 12 | 13 | let categoryId 14 | 15 | if (category) { 16 | categoryId = ( 17 | await prisma.category.findUnique({ 18 | where: { 19 | slug: category, 20 | }, 21 | }) 22 | )?.id 23 | } 24 | 25 | const products = await prisma.product.updateMany({ 26 | where: { categoryId }, 27 | data: { discount }, 28 | }) 29 | 30 | res.status(200).json({ updatedProducts: products.count }) 31 | }) 32 | -------------------------------------------------------------------------------- /pages/api/ratings/index.ts: -------------------------------------------------------------------------------- 1 | import apiHandler from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | export const getRatings = async () => { 7 | const { _avg } = await prisma.rating.aggregate({ 8 | _avg: { 9 | star: true, 10 | }, 11 | }) 12 | const ratings = await prisma.rating.findMany({ 13 | orderBy: { updatedAt: 'desc' }, 14 | include: { 15 | order: { 16 | select: { 17 | user: true, 18 | products: { 19 | select: { 20 | amount: true, 21 | product: { 22 | select: { 23 | title: true, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }) 32 | return { 33 | avg: Number(_avg.star.toFixed(1)), 34 | ratings, 35 | count: ratings.length, 36 | } 37 | } 38 | 39 | export default app 40 | // create rating 41 | .post(async (req, res) => { 42 | const { star, comment, orderId } = req.body 43 | const rating = await prisma.rating.create({ 44 | data: { 45 | star, 46 | comment, 47 | orderId, 48 | }, 49 | }) 50 | res.status(201).json({ rating }) 51 | }) 52 | // aggregate rating 53 | .get(async (req, res) => { 54 | const ratings = await getRatings() 55 | res.status(200).json(ratings) 56 | }) 57 | -------------------------------------------------------------------------------- /pages/api/requirements/[fieldName].ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | export default app.put(checkAuth(), async (req, res) => { 7 | const reqField = await prisma.categoryRequirementField.findFirst({ 8 | where: { value: req.query.fieldName as string }, 9 | }) 10 | 11 | const reqMe = await prisma.usersFields.findFirst({ 12 | where: { 13 | userId: req.user.id, 14 | fieldId: reqField.id, 15 | }, 16 | }) 17 | 18 | if (reqMe) { 19 | await prisma.categoryRequirementField.update({ 20 | where: { id: reqField.id }, 21 | data: { 22 | users: { 23 | update: { 24 | where: { id: reqMe.id }, 25 | data: { 26 | value: req.body.fieldValue, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }) 32 | } else { 33 | await prisma.categoryRequirementField.update({ 34 | where: { id: reqField.id }, 35 | data: { 36 | users: { 37 | create: [ 38 | { 39 | value: req.body.fieldValue, 40 | userId: req.user.id, 41 | }, 42 | ], 43 | }, 44 | }, 45 | }) 46 | } 47 | 48 | res.status(200).json({ message: 'success' }) 49 | }) 50 | -------------------------------------------------------------------------------- /pages/api/requirements/index.ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | export default app 7 | // get all users fields 8 | .get(checkAuth(), async (req, res) => { 9 | const usersFields = await prisma.usersFields.findMany({ 10 | where: { userId: req.user.id }, 11 | select: { 12 | id: true, 13 | value: true, 14 | field: { 15 | select: { 16 | categoryRequirement: { 17 | select: { category: { select: { slug: true } } }, 18 | }, 19 | value: true, 20 | }, 21 | }, 22 | }, 23 | }) 24 | 25 | const requirements = {} 26 | usersFields.forEach((userField) => { 27 | const { field, value } = userField 28 | requirements[field.categoryRequirement.category.slug] = { 29 | ...requirements[field.categoryRequirement.category.slug], 30 | [field.value]: value, 31 | } 32 | }) 33 | 34 | res.status(200).json({ requirements }) 35 | }) 36 | -------------------------------------------------------------------------------- /pages/api/users/displayName.ts: -------------------------------------------------------------------------------- 1 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 2 | import prisma from '../../../lib/prisma' 3 | 4 | const app = apiHandler() 5 | 6 | export default app 7 | // edit user's display name 8 | .put(checkAuth(), async (req, res) => { 9 | await prisma.user.update({ 10 | where: { id: req.user.id }, 11 | data: { 12 | displayName: req.body.displayName, 13 | }, 14 | }) 15 | res.status(200).json({ 16 | message: `Success update user's display name with id ${req.user.id}`, 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /pages/api/users/fakeAdmin.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@prisma/client' 2 | import apiHandler, { checkAuth } from '../../../lib/apiHandler' 3 | import prisma from '../../../lib/prisma' 4 | 5 | const app = apiHandler() 6 | 7 | export default app 8 | // make user fake admin 9 | .put(checkAuth(), async (req, res) => { 10 | const user = await prisma.user.findUnique({ 11 | where: { id: req.user.id }, 12 | }) 13 | 14 | let role: Role = user.role != 'FAKE_ADMIN' ? 'FAKE_ADMIN' : 'USER' 15 | 16 | const updatedUser = await prisma.user.update({ 17 | where: { 18 | id: req.user.id, 19 | }, 20 | data: { 21 | role, 22 | }, 23 | }) 24 | res.status(200).json({ user: updatedUser }) 25 | }) 26 | -------------------------------------------------------------------------------- /pages/blog/about-rausky.tsx: -------------------------------------------------------------------------------- 1 | import Container from '../../components/Container' 2 | import Wrapper from '../../components/Wrapper' 3 | 4 | // TODO: jelasin about rausky 5 | const RauskyPayments = () => { 6 | return ( 7 | 8 | 9 |

About Rausky

10 |
11 |

12 | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Eligendi 13 | molestiae alias voluptatum nemo, numquam pariatur molestias sapiente 14 | ad cupiditate tempore aliquam maxime illo magni iusto! Sapiente 15 | veniam modi saepe sed! 16 |

17 |

18 | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Laudantium 19 | fuga alias maiores eum iste nisi sint facilis nesciunt ducimus. 20 | Voluptates a eaque praesentium qui? Quibusdam excepturi vel minima 21 | quasi dignissimos! 22 |

23 |
24 |
25 |
26 | ) 27 | } 28 | 29 | export default RauskyPayments 30 | -------------------------------------------------------------------------------- /pages/blog/discount.tsx: -------------------------------------------------------------------------------- 1 | import Container from '../../components/Container' 2 | import Wrapper from '../../components/Wrapper' 3 | 4 | const RauskyPayments = () => { 5 | return ( 6 | 7 | 8 |

Discount Up To 50 %

9 |
10 |

Use code : WPURAUSKY to get 50 % discount for all payments

11 |
12 |
13 |
14 | ) 15 | } 16 | 17 | export default RauskyPayments 18 | -------------------------------------------------------------------------------- /pages/blog/rausky-payments.tsx: -------------------------------------------------------------------------------- 1 | import Container from '../../components/Container' 2 | import Wrapper from '../../components/Wrapper' 3 | 4 | // TODO: jelasin kenapa transaksinya belom beneran 5 | const RauskyPayments = () => { 6 | return ( 7 | 8 | 9 |

Temporary Rausky payments

10 |
11 |

12 | Web Aplication Rausky Gamestore masih dalam tahap pengembangan. 13 | Untuk itu semua pembayaran masih dalam bentuk ilustrasi sehingga 14 | tidak dikenakan biaya apapun. 15 |

16 |

17 | The Rausky Gamestore web application is still under development. For 18 | this reason, all payments are still in the form of illustrations so 19 | that there is no charge whatsoever. 20 |

21 |
22 |
23 |
24 | ) 25 | } 26 | 27 | export default RauskyPayments 28 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import { StarIcon as StarIconSolid } from '@heroicons/react/solid' 2 | import { useState } from 'react' 3 | import { useEffect } from 'react' 4 | import Skeleton from 'react-loading-skeleton' 5 | import Container from '../components/Container' 6 | import Link from '../components/Link' 7 | import Wrapper from '../components/Wrapper' 8 | import { parseData } from '../lib/utils' 9 | import { getAllCategories } from './api/categories' 10 | import request from '../lib/request' 11 | import Rating from '../components/Rating' 12 | import RatingsModal from '../components/home/RatingsModal' 13 | 14 | const getRatingCount = (ratings, star) => { 15 | return ratings.filter((rating) => rating.star == star).length 16 | } 17 | 18 | const Home = ({ categories }) => { 19 | const topupCategories = categories.filter((category) => category.isTopup) 20 | const otherCategories = categories.filter((category) => !category.isTopup) 21 | const [ratings, setRatings] = useState(null) 22 | const [showAllRatings, setShowAllRatings] = useState(false) 23 | 24 | console.log({ ratings }) 25 | 26 | useEffect(() => { 27 | const fetchAppRatings = async () => { 28 | const { data } = await request.get('/ratings') 29 | setRatings(data) 30 | } 31 | fetchAppRatings() 32 | }, []) 33 | 34 | return ( 35 | 36 | 37 | {/* TODO: biar keren kasih parallax effect */} 38 | 39 | {/* TOP UP */} 40 |
41 |

Top Up

42 |
43 | {topupCategories.map((category) => ( 44 | 49 | {category.slug} 54 | 55 | {category.name} 56 | 57 | 58 | ))} 59 |
60 |
61 | 62 |
63 | {/* DISCOUNTS */} 64 |
65 | 69 | discount 74 |
75 |
76 |

77 | GET UP TO 50 % 78 |

79 | 82 |
83 |
84 | 85 |
86 | 87 | {/* OTHERS */} 88 |
89 |

Yakali ga sekalian

90 |
91 | {otherCategories.map((category) => ( 92 | 97 | {category.slug} 98 |
99 | 100 | {category.name} → 101 | 102 |
103 | 104 | ))} 105 |
106 |
107 |
108 | 109 |
110 | {/* RATINGS */} 111 |
112 |

113 | Ratings{' '} 114 | {ratings ? ( 115 | 116 | ({ratings.count.toLocaleString()}) 117 | 118 | ) : ( 119 | 120 | )} 121 |

122 | {/* AVG RATING */} 123 |
124 | 125 | {ratings ? ( 126 | 127 | {ratings.avg} 128 | 129 | ) : ( 130 | 131 | )} 132 |
133 | {/* RATING STATISTICS */} 134 | {ratings ? ( 135 |
136 | {[5, 4, 3, 2, 1].map((star) => { 137 | const ratingCount = getRatingCount(ratings.ratings, star) 138 | const ratingCountPercent = 139 | (ratingCount / ratings.ratings.length) * 100 140 | return ( 141 |
142 | {star} 143 |
148 |
154 |
155 |
156 | ) 157 | })} 158 |
159 | ) : ( 160 |
161 | 162 |
163 | )} 164 | {/* RATING LIST */} 165 |
166 | {ratings ? ( 167 | ratings.ratings 168 | .slice(0, 2) 169 | .map((rating) => ) 170 | ) : ( 171 | 172 | )} 173 |
174 | {ratings && ( 175 | <> 176 | 182 | 183 | )} 184 |
185 |
186 | 187 | {ratings && ( 188 | setShowAllRatings(false)} 191 | ratings={ratings} 192 | /> 193 | )} 194 |
195 |
196 | ) 197 | } 198 | 199 | export default Home 200 | 201 | export const getStaticProps = async () => { 202 | const categories = await getAllCategories({ 203 | select: 'isTopup,id,slug,name,bannerImg,logoImg', 204 | hasProducts: true, 205 | }) 206 | return { 207 | props: parseData({ 208 | categories, 209 | }), 210 | revalidate: 10, 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /pages/products/[category].tsx: -------------------------------------------------------------------------------- 1 | import UnderDevelopment from '../../components/UnderDevelopment' 2 | 3 | const OtherProducts = () => { 4 | return 5 | } 6 | 7 | export default OtherProducts 8 | -------------------------------------------------------------------------------- /pages/profile.tsx: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from '@mui/material' 2 | import { GetServerSideProps } from 'next' 3 | import { getSession } from 'next-auth/react' 4 | import { useRouter } from 'next/router' 5 | import { useState } from 'react' 6 | import toast from 'react-hot-toast' 7 | import Container from '../components/Container' 8 | import Modal from '../components/Modal' 9 | import { CustomTab, CustomTabs } from '../components/mui/Tabs' 10 | import OrderHistory from '../components/profile/OrderHistory' 11 | import TopUpInformation from '../components/profile/TopUpInformation' 12 | import UserBadge from '../components/UserBadge' 13 | import Wrapper from '../components/Wrapper' 14 | import request from '../lib/request' 15 | import { User } from '../types/next-auth' 16 | 17 | interface Props { 18 | user: User 19 | } 20 | 21 | const TabPanel = ({ children, value, index, ...other }) => { 22 | return ( 23 | 26 | ) 27 | } 28 | 29 | const Profile = ({ user }: Props) => { 30 | const [tabIndex, setTabIndex] = useState(0) 31 | const [showFakeAdminModal, setShowFakeAdminModal] = useState(false) 32 | const changeTab = (event: React.SyntheticEvent, newIndex: number) => { 33 | setTabIndex(newIndex) 34 | } 35 | const onSmallScreen = useMediaQuery('(max-width: 420px)') 36 | 37 | const [displayName, setDisplayName] = useState(user.displayName ?? '') 38 | const router = useRouter() 39 | 40 | const beFakeAdminHandler = async () => { 41 | let toastId: string 42 | try { 43 | toastId = toast.loading('Processing...') 44 | await request.put(`/users/fakeAdmin`) 45 | toast.success("Congrats. Now you're a fake admin 🎉", { id: toastId }) 46 | router.push(router.asPath, undefined, { shallow: false }) 47 | setShowFakeAdminModal(false) 48 | } catch (err) { 49 | console.log(err) 50 | toast.error('Failed. Check console for details', { id: toastId }) 51 | } 52 | } 53 | 54 | const removeFakeAdminHandler = async () => { 55 | let toastId: string 56 | try { 57 | toastId = toast.loading('Processing...') 58 | await request.put(`/users/fakeAdmin`) 59 | toast.success("Success, Now you're a normal user", { id: toastId }) 60 | router.push(router.asPath, undefined, { shallow: false }) 61 | } catch (err) { 62 | console.log(err) 63 | toast.error('Failed. Check console for details', { id: toastId }) 64 | } 65 | } 66 | 67 | const saveUserDisplayName = async () => { 68 | let toastId: string 69 | try { 70 | toastId = toast.loading('Saving...') 71 | await request.put('/users/displayName', { displayName }) 72 | toast.success('Saved', { id: toastId }) 73 | router.push(router.asPath, undefined, { shallow: false }) 74 | } catch (err) { 75 | toast.error('Failed. Check console for details', { id: toastId }) 76 | console.log(err) 77 | } 78 | } 79 | 80 | return ( 81 | 82 | 83 |
84 | {/* USER PROFILE */} 85 |
86 | {/* USER IMAGE */} 87 | {user.name} 92 | {/* USER ROLE BADGE (MOBILE) */} 93 | 97 | 98 |
99 | {/* USER NAME */} 100 |

101 | {user.displayName || user.name} 102 | {/* USER ROLE BADGE (DESKTOP) */} 103 | 107 |

108 | 109 |
110 | {user.displayName && ( 111 |
{user.name}
112 | )} 113 | {/* USER EMAIL */} 114 |
{user.email}
115 |
116 | 117 | {user.role == 'USER' && ( 118 | 124 | )} 125 | 126 | {user.role == 'FAKE_ADMIN' && ( 127 | 133 | )} 134 |
135 |
136 |
137 | {user.role != 'USER' && ( 138 | 143 | Go to Admin Dashboard → 144 | 145 | )} 146 | 147 |
148 |

Edit Profile

149 |
150 | 162 |
163 | 169 |
170 |
171 |
172 | 173 |
174 | 179 | 180 | 181 | 182 |
183 | 184 | 185 | 186 | 187 | 188 | 189 |
190 |
191 |
192 | setShowFakeAdminModal(false)} 195 | > 196 |

Fake Admin ?

197 |
198 |

199 | By being a fake admin, you can access admin dashboard page
{' '} 200 | BUT you can't do all admin operations 201 |

202 |
203 | 209 | 215 |
216 |
217 |
218 |
219 | ) 220 | } 221 | 222 | export default Profile 223 | 224 | export const getServerSideProps: GetServerSideProps = async ({ 225 | req, 226 | query, 227 | }) => { 228 | const session = await getSession({ req }) 229 | if (!session) { 230 | return { 231 | redirect: { 232 | destination: '/signin', 233 | permanent: false, 234 | }, 235 | } 236 | } 237 | 238 | const { order_id } = query 239 | if (order_id) { 240 | return { 241 | redirect: { 242 | destination: `/order?orderId=${order_id}`, 243 | permanent: false, 244 | }, 245 | } 246 | } 247 | 248 | return { 249 | props: { 250 | user: session.user, 251 | }, 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /pages/signin.tsx: -------------------------------------------------------------------------------- 1 | import Container from '../components/Container' 2 | import Wrapper from '../components/Wrapper' 3 | import { 4 | ClientSafeProvider, 5 | getProviders, 6 | getSession, 7 | LiteralUnion, 8 | signIn, 9 | } from 'next-auth/react' 10 | import { GetServerSideProps } from 'next' 11 | import { BuiltInProviderType } from 'next-auth/providers' 12 | import Link from '../components/Link' 13 | 14 | interface Props { 15 | providers: Record< 16 | LiteralUnion, 17 | ClientSafeProvider 18 | > 19 | } 20 | 21 | const providerLogo = { 22 | google: 'google.webp', 23 | discord: 'discord.png', 24 | github: 'github.png', 25 | } 26 | 27 | const SignIn = ({ providers }: Props) => { 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 |

Sign In to Rausky Gamestore

35 |
36 | {Object.values(providers).map((provider) => { 37 | return ( 38 | 50 | ) 51 | })} 52 |
53 |
54 |
55 | ) 56 | } 57 | 58 | export default SignIn 59 | 60 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 61 | const session = await getSession(ctx) 62 | if (session) { 63 | return { 64 | redirect: { 65 | destination: '/', 66 | permanent: false, 67 | }, 68 | } 69 | } 70 | return { 71 | props: { 72 | providers: await getProviders(), 73 | }, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pages/topup/[category].tsx: -------------------------------------------------------------------------------- 1 | import Container from '../../components/Container' 2 | import Wrapper from '../../components/Wrapper' 3 | import { parseData } from '../../lib/utils' 4 | import { getSpecificCategory } from '../api/categories/[categoryId]' 5 | import TopupInfo from '../../components/topup/TopupInfo' 6 | import TopupItems from '../../components/topup/TopupItems' 7 | import { GetServerSideProps } from 'next' 8 | import { useSession } from 'next-auth/react' 9 | 10 | const Topup = ({ category }) => { 11 | const { data, status } = useSession() 12 | const user = data?.user 13 | 14 | return ( 15 | 16 |
17 | {/* BANNER IMG */} 18 | {category.slug} 23 | 24 | 25 | {status !== 'loading' && } 26 | 27 |
28 | ) 29 | } 30 | 31 | export default Topup 32 | 33 | export const getServerSideProps: GetServerSideProps = async ({ params }) => { 34 | const category = await getSpecificCategory({ 35 | categorySlug: params.category as string, 36 | includeProducts: true, 37 | }) 38 | 39 | if (!category) { 40 | return { 41 | notFound: true, 42 | } 43 | } 44 | 45 | return { 46 | props: parseData({ category }), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mongodb" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Account { 14 | id String @id @default(auto()) @map("_id") @db.ObjectId 15 | userId String 16 | type String 17 | provider String 18 | providerAccountId String 19 | refresh_token String? @db.String 20 | access_token String? @db.String 21 | expires_at Int? 22 | token_type String? 23 | scope String? 24 | id_token String? @db.String 25 | session_state String? 26 | 27 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 28 | 29 | @@unique([provider, providerAccountId]) 30 | } 31 | 32 | model Session { 33 | id String @id @default(auto()) @map("_id") @db.ObjectId 34 | sessionToken String @unique 35 | userId String 36 | expires DateTime 37 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 | } 39 | 40 | model VerificationToken { 41 | id String @id @default(auto()) @map("_id") @db.ObjectId 42 | identifier String 43 | token String @unique 44 | expires DateTime 45 | 46 | @@unique([identifier, token]) 47 | } 48 | 49 | enum Role { 50 | USER 51 | FAKE_ADMIN 52 | ADMIN 53 | } 54 | 55 | model User { 56 | id String @id @default(auto()) @map("_id") @db.ObjectId 57 | name String? 58 | displayName String? 59 | email String? @unique 60 | emailVerified DateTime? 61 | image String? 62 | role Role? @default(USER) 63 | accounts Account[] 64 | sessions Session[] 65 | orders Order[] 66 | products Product[] 67 | requirementFields UsersFields[] 68 | cart Cart? 69 | discountCodesUsed UsersOnDiscountCodes[] 70 | } 71 | 72 | model Category { 73 | id String @id @default(auto()) @map("_id") @db.ObjectId 74 | name String @unique 75 | slug String @unique 76 | description String? 77 | moreInfo Json? 78 | bannerImg String? 79 | logoImg String? 80 | isTopup Boolean? @default(false) 81 | requirement CategoryRequirement? 82 | products Product[] 83 | subCategories SubCategory[] 84 | createdAt DateTime @default(now()) 85 | updatedAt DateTime @updatedAt 86 | } 87 | 88 | model CategoryRequirement { 89 | id String @id @default(auto()) @map("_id") @db.ObjectId 90 | title String 91 | description String? 92 | fields CategoryRequirementField[] 93 | img String? 94 | category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade) 95 | categoryId String? @unique @db.ObjectId 96 | } 97 | 98 | model CategoryRequirementField { 99 | id String @id @default(auto()) @map("_id") @db.ObjectId 100 | name String 101 | value String? 102 | placeholder String? 103 | type String? @default("text") 104 | categoryRequirement CategoryRequirement? @relation(fields: [categoryRequirementId], references: [id], onDelete: Cascade) 105 | categoryRequirementId String? @db.ObjectId 106 | users UsersFields[] 107 | } 108 | 109 | model UsersFields { 110 | id String @id @default(auto()) @map("_id") @db.ObjectId 111 | userId String @db.ObjectId 112 | fieldId String @db.ObjectId 113 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 114 | field CategoryRequirementField @relation(fields: [fieldId], references: [id], onDelete: Cascade) 115 | value String? 116 | } 117 | 118 | model SubCategory { 119 | id String @id @default(auto()) @map("_id") @db.ObjectId 120 | name String @unique 121 | slug String @unique 122 | description String? 123 | moreInfo Json? 124 | logoImg String? 125 | categoryId String? @db.ObjectId 126 | category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade) 127 | products Product[] 128 | } 129 | 130 | model Product { 131 | id String @id @default(auto()) @map("_id") @db.ObjectId 132 | title String 133 | description String? 134 | img String? 135 | price Int 136 | discount Int? @default(0) 137 | stock Int? @default(0) 138 | categoryId String? @db.ObjectId 139 | moreInfo Json? 140 | userId String? @db.ObjectId 141 | subCategoryId String? @db.ObjectId 142 | orders ProductsOnOrders[] 143 | category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) 144 | user User? @relation(fields: [userId], references: [id], onDelete: Cascade) 145 | subCategory SubCategory? @relation(fields: [subCategoryId], references: [id], onDelete: Cascade) 146 | createdAt DateTime @default(now()) 147 | updatedAt DateTime @updatedAt 148 | } 149 | 150 | model ProductsOnOrders { 151 | id String @id @default(auto()) @map("_id") @db.ObjectId 152 | productId String @db.ObjectId 153 | amount Int 154 | orderId String @db.ObjectId 155 | createdAt DateTime @default(now()) 156 | updatedAt DateTime @updatedAt 157 | product Product @relation(fields: [productId], references: [id], onDelete: Cascade) 158 | order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) 159 | } 160 | 161 | model Cart { 162 | id String @id @default(auto()) @map("_id") @db.ObjectId 163 | products Json[] 164 | userId String @unique @db.ObjectId 165 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 166 | } 167 | 168 | enum PaymentStatus { 169 | WAITING_PAYMENT 170 | CANCELED 171 | PAID 172 | VERIFIED 173 | } 174 | 175 | model Order { 176 | id String @id @default(auto()) @map("_id") @db.ObjectId 177 | total Int? @default(0) 178 | tax Int? @default(0) 179 | discount Int? @default(0) 180 | paymentUrl String? 181 | paymentToken String? 182 | requirements Json? 183 | products ProductsOnOrders[] 184 | paymentMethod String? 185 | status PaymentStatus? @default(WAITING_PAYMENT) 186 | paidAt DateTime? 187 | userId String? @db.ObjectId 188 | user User? @relation(fields: [userId], references: [id], onDelete: Cascade) 189 | rating Rating? 190 | // TODO: change this to reference DiscountCode model 191 | promoCode String? 192 | createdAt DateTime @default(now()) 193 | updatedAt DateTime @updatedAt 194 | } 195 | 196 | model Rating { 197 | id String @id @default(auto()) @map("_id") @db.ObjectId 198 | star Int 199 | comment String? 200 | orderId String @unique @db.ObjectId 201 | order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) 202 | createdAt DateTime @default(now()) 203 | updatedAt DateTime @updatedAt 204 | } 205 | 206 | model DiscountCode { 207 | id String @id @default(auto()) @map("_id") @db.ObjectId 208 | code String @unique 209 | discountPercent Float? 210 | quota Int? @default(0) 211 | users UsersOnDiscountCodes[] 212 | validUntil DateTime? 213 | createdAt DateTime @default(now()) 214 | updatedAt DateTime @updatedAt 215 | } 216 | 217 | model UsersOnDiscountCodes { 218 | id String @id @default(auto()) @map("_id") @db.ObjectId 219 | createdAt DateTime @default(now()) 220 | updatedAt DateTime @updatedAt 221 | user User? @relation(fields: [userId], references: [id]) 222 | userId String? @db.ObjectId 223 | discountCode DiscountCode? @relation(fields: [discountCodeId], references: [id]) 224 | discountCodeId String? @db.ObjectId 225 | isUsed Boolean? @default(false) 226 | } 227 | -------------------------------------------------------------------------------- /public/images/auth/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/auth/discord.png -------------------------------------------------------------------------------- /public/images/auth/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/auth/github.png -------------------------------------------------------------------------------- /public/images/auth/google.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/auth/google.webp -------------------------------------------------------------------------------- /public/images/icon/bag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icon/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/icon/instagram.png -------------------------------------------------------------------------------- /public/images/icon/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/icon/line.png -------------------------------------------------------------------------------- /public/images/icon/person.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/icon/union.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/illustration/empty-cart.svg: -------------------------------------------------------------------------------- 1 | empty_cart -------------------------------------------------------------------------------- /public/images/illustration/empty-order.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/illustration/empty-rating.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/logo/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/logo/favicon.png -------------------------------------------------------------------------------- /public/images/logo/overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/logo/overview.jpg -------------------------------------------------------------------------------- /public/images/logo/rausky-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/logo/rausky-logo.png -------------------------------------------------------------------------------- /public/images/product/ff-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/ff-banner.jpg -------------------------------------------------------------------------------- /public/images/product/ff-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/ff-logo.jpg -------------------------------------------------------------------------------- /public/images/product/mobile-legend-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/mobile-legend-banner.jpg -------------------------------------------------------------------------------- /public/images/product/mobile-legend-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/mobile-legend-logo.png -------------------------------------------------------------------------------- /public/images/product/netflix-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/netflix-banner.jpg -------------------------------------------------------------------------------- /public/images/product/netflix-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/netflix-logo.jpg -------------------------------------------------------------------------------- /public/images/product/ps-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/ps-banner.jpg -------------------------------------------------------------------------------- /public/images/product/ps-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/ps-logo.png -------------------------------------------------------------------------------- /public/images/product/pubg-mobile-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/pubg-mobile-banner.jpg -------------------------------------------------------------------------------- /public/images/product/pubg-mobile-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/pubg-mobile-logo.png -------------------------------------------------------------------------------- /public/images/product/spotify-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/spotify-banner.png -------------------------------------------------------------------------------- /public/images/product/spotify-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/spotify-logo.png -------------------------------------------------------------------------------- /public/images/product/steam-banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/steam-banner.webp -------------------------------------------------------------------------------- /public/images/product/steam-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/steam-logo.png -------------------------------------------------------------------------------- /public/images/product/valorant-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/valorant-banner.jpg -------------------------------------------------------------------------------- /public/images/product/valorant-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AditNovadianto/Rausky-Store/ea60e647cfaf1be0a9677c5644ca66845507e60b/public/images/product/valorant-logo.png -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | *:disabled { 6 | @apply pointer-events-none opacity-40; 7 | } 8 | 9 | ::-webkit-scrollbar { 10 | width: 0.3em; 11 | height: 0.3em; 12 | } 13 | 14 | ::-webkit-scrollbar-thumb { 15 | @apply bg-gray-400 hover:bg-green-500 dark:bg-gray-700 dark:hover:bg-green-500; 16 | border-radius: 100vw; 17 | } 18 | 19 | .text-gray-500 { 20 | @apply dark:text-gray-400; 21 | } 22 | 23 | .MuiTab-root { 24 | @apply !text-gray-500 dark:!text-gray-400; 25 | } 26 | 27 | .MuiTab-root.Mui-selected { 28 | @apply !text-green-500; 29 | } 30 | 31 | .react-loading-skeleton { 32 | --base-color: rgb(229 231 235); 33 | --highlight-color: rgb(243 244 246); 34 | } 35 | 36 | html.dark .react-loading-skeleton { 37 | --base-color: rgb(31 41 55); 38 | --highlight-color: rgb(55 65 81); 39 | } 40 | 41 | @layer base { 42 | html { 43 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 44 | @apply scroll-smooth; 45 | } 46 | 47 | body { 48 | @apply antialiased bg-white dark:bg-gray-900 dark:text-gray-200; 49 | } 50 | } 51 | 52 | @layer components { 53 | .input { 54 | @apply block w-full px-5 py-3 rounded-xl border border-gray-300 focus:outline-none focus:border-green-400 bg-inherit dark:bg-gray-700 dark:border-gray-600 dark:focus:border-green-400; 55 | } 56 | 57 | .select { 58 | @apply border focus:outline-none focus:border-green-400 focus:ring-2 focus:ring-green-200 focus:ring-opacity-70 px-2 py-1 rounded-md cursor-pointer text-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:border-gray-700 dark:focus:ring-green-500; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkMode: 'class', 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx}', 5 | './components/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | sans: ["'Inter', sans-serif"], 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "./types/**/.d.ts"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /types/globals.d.ts: -------------------------------------------------------------------------------- 1 | type CustomObject = { [key: string]: any } 2 | 3 | declare global { 4 | interface Window { 5 | snap: any 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@prisma/client' 2 | import NextAuth, { DefaultSession } from 'next-auth' 3 | 4 | declare module 'next-auth' { 5 | /** 6 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 7 | */ 8 | interface Session { 9 | user: { 10 | id?: string 11 | role?: Role 12 | displayName?: string 13 | } & DefaultSession['user'] 14 | } 15 | } 16 | 17 | export type User = { 18 | id?: string 19 | role?: Role 20 | displayName?: string 21 | } & { 22 | name?: string 23 | email?: string 24 | image?: string 25 | } 26 | -------------------------------------------------------------------------------- /types/state.d.ts: -------------------------------------------------------------------------------- 1 | import 'little-state-machine' 2 | import { Category, Order, Product, Rating, User } from '@prisma/client' 3 | 4 | declare module 'little-state-machine' { 5 | interface GlobalState { 6 | globalTheme: 'dark' | 'light' 7 | cart: (Product & { 8 | amount: number 9 | category: Category 10 | })[] 11 | order: { 12 | user: { name: string; email: string } | {} 13 | requirements: CustomObject 14 | categoryRequirements: CustomObject[] 15 | missingRequirements: CustomObject 16 | subtotal: number 17 | tax: number 18 | discount: number 19 | total: number 20 | promoCode: string 21 | } 22 | updatingDB: boolean 23 | updatedDB: boolean 24 | } 25 | } 26 | --------------------------------------------------------------------------------