├── .npmrc ├── public ├── assets │ ├── .gitignore │ ├── payment-methods.png │ ├── banner │ │ ├── 74d32a7f-6a2d-49a3-b325-114de4b055c5.jpg │ │ └── d8a90cd0-bfa4-47e2-b288-3afd54e0cbea.jpg │ └── logo.svg ├── robots.txt └── favicon.ico ├── dev.log ├── types ├── contextType.d.ts ├── next-auth.d.ts └── global.d.ts ├── prisma ├── dev.db ├── utils.ts ├── seed-region.ts ├── seed.ts ├── gen-crud-router.ts └── generateCrudRouter.ts ├── store ├── enums │ └── index.ts └── index.ts ├── lib ├── zod │ ├── cart.ts │ ├── cartitem.ts │ ├── storeteam.ts │ ├── role.ts │ ├── permission.ts │ ├── store.ts │ ├── datacountry.ts │ ├── producttags.ts │ ├── databank.ts │ ├── membership.ts │ ├── storefront.ts │ ├── datacity.ts │ ├── datadistrict.ts │ ├── dataprovince.ts │ ├── datavillage.ts │ ├── productcategories.ts │ ├── product.ts │ ├── storelocation.ts │ ├── userlocation.ts │ ├── productcomments.ts │ ├── index.ts │ ├── accounts.ts │ └── user.ts ├── screen-size.js ├── hooks │ ├── useDarkMode.ts │ ├── useHeadlessuiDialog.tsx │ └── useToast.tsx ├── trpc.ts └── utils.ts ├── postcss.config.js ├── next-env.d.ts ├── server ├── trpc.ts ├── appRouter.ts ├── prisma.ts ├── context.ts ├── routers │ ├── adminRouter.ts │ ├── _app.ts │ ├── cartRouter.ts │ ├── roleRouter.ts │ ├── userRouter.ts │ ├── accountsRouter.ts │ ├── dataBankRouter.ts │ ├── dataCityRouter.ts │ ├── storeTeamRouter.ts │ ├── membershipRouter.ts │ ├── permissionRouter.ts │ ├── storeFrontRouter.ts │ ├── dataCountryRouter.ts │ ├── dataVillageRouter.ts │ ├── productTagsRouter.ts │ ├── dataDistrictRouter.ts │ ├── dataProvinceRouter.ts │ ├── userLocationRouter.ts │ ├── storeLocationRouter.ts │ ├── productCommentsRouter.ts │ ├── productCategoriesRouter.ts │ ├── storeRouter.ts │ └── productRouter.ts ├── utils.ts └── Authorization.ts ├── components ├── Layouts │ ├── AdminPage │ │ └── NavbarMenu │ │ │ ├── HelpMenu.tsx │ │ │ ├── Notifications.tsx │ │ │ └── index.tsx │ ├── CopyrightFooter.tsx │ ├── FrontPage │ │ ├── NavbarMenu │ │ │ ├── Auth.tsx │ │ │ ├── index.tsx │ │ │ ├── Message.tsx │ │ │ ├── Cart.tsx │ │ │ └── User.tsx │ │ ├── FooterMenu │ │ │ └── index.tsx │ │ ├── CategoriesMenu.tsx │ │ └── SearchForm.tsx │ ├── Overlays.tsx │ └── Auth │ │ └── index.tsx ├── Utils │ └── Inlined.tsx ├── Skeleton │ ├── ListSkeleton.tsx │ └── Skeleton.tsx ├── Icon │ └── SVGRaw.tsx ├── Dialog │ └── DialogConfirm.tsx ├── Menu │ ├── NavbarMenu.tsx │ └── NestedListMenu.tsx ├── Form │ └── Input.tsx ├── Banner │ └── HomepageCarousel.tsx ├── User │ └── Layouts │ │ ├── index.tsx │ │ └── SidebarUserMenu.tsx └── Admin │ └── Dashboard │ └── index.tsx ├── .env.example ├── next.config.js ├── pages ├── admin │ ├── profile.tsx │ ├── users │ │ ├── add.tsx │ │ └── index.tsx │ └── index.tsx ├── api │ ├── hello.ts │ ├── trpc │ │ └── [trpc].ts │ └── auth │ │ └── [...nextauth].ts ├── product │ └── [id].tsx ├── index.tsx ├── seller │ ├── index.tsx │ └── products │ │ ├── add.tsx │ │ └── edit │ │ └── [id].tsx ├── _app.tsx ├── cart │ └── index.tsx ├── accounts │ └── index.tsx ├── store │ └── index.tsx └── _document.tsx ├── .gitignore ├── styles ├── global.css ├── plugins │ └── scrollbar.js └── homepage-carousel.css ├── .vscode └── settings.json ├── tsconfig.json ├── tailwind.config.js ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /public/assets/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /dev.log: -------------------------------------------------------------------------------- 1 | 2 | > next-toko@1.0.0 dev /app 3 | > next dev 4 | -------------------------------------------------------------------------------- /types/contextType.d.ts: -------------------------------------------------------------------------------- 1 | export type Context = ServerControllerProps; -------------------------------------------------------------------------------- /prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arisris/next-toko/HEAD/prisma/dev.db -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arisris/next-toko/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /store/enums/index.ts: -------------------------------------------------------------------------------- 1 | export const enum Role { 2 | ADMIN = "admin", 3 | USER = "user" 4 | } 5 | -------------------------------------------------------------------------------- /public/assets/payment-methods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arisris/next-toko/HEAD/public/assets/payment-methods.png -------------------------------------------------------------------------------- /lib/zod/cart.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const CartModel = z.object({ 4 | id: z.number().int(), 5 | userId: z.number().int(), 6 | }) 7 | -------------------------------------------------------------------------------- /public/assets/banner/74d32a7f-6a2d-49a3-b325-114de4b055c5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arisris/next-toko/HEAD/public/assets/banner/74d32a7f-6a2d-49a3-b325-114de4b055c5.jpg -------------------------------------------------------------------------------- /public/assets/banner/d8a90cd0-bfa4-47e2-b288-3afd54e0cbea.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arisris/next-toko/HEAD/public/assets/banner/d8a90cd0-bfa4-47e2-b288-3afd54e0cbea.jpg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStoreon, StoreonModule } from "storeon" 2 | 3 | const app: StoreonModule<{}, {}> = (store) => { 4 | 5 | } 6 | 7 | export default createStoreon([app]) -------------------------------------------------------------------------------- /lib/zod/cartitem.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const CartItemModel = z.object({ 4 | id: z.number().int(), 5 | cartId: z.number().int(), 6 | productId: z.number().int(), 7 | quantity: z.number().int(), 8 | }) 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/zod/storeteam.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const StoreTeamModel = z.object({ 4 | id: z.number().int(), 5 | storeId: z.number().int(), 6 | createdAt: z.date().nullish(), 7 | updatedAt: z.date().nullish(), 8 | }) 9 | -------------------------------------------------------------------------------- /server/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from "@trpc/server"; 2 | import SuperJSON from "superjson"; 3 | import { Context } from "./context"; 4 | 5 | export const t = initTRPC.context().create({ 6 | transformer: SuperJSON, 7 | }); 8 | -------------------------------------------------------------------------------- /components/Layouts/AdminPage/NavbarMenu/HelpMenu.tsx: -------------------------------------------------------------------------------- 1 | import { List, ListItem } from "konsta/react"; 2 | 3 | export default function AdminPageHelpMenu() { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /lib/zod/role.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const RoleModel = z.object({ 4 | id: z.number().int(), 5 | name: z.string(), 6 | displayName: z.string().nullish(), 7 | createdAt: z.date().nullish(), 8 | updatedAt: z.date().nullish(), 9 | }) 10 | -------------------------------------------------------------------------------- /components/Layouts/AdminPage/NavbarMenu/Notifications.tsx: -------------------------------------------------------------------------------- 1 | import { List, ListItem } from "konsta/react"; 2 | 3 | export default function AdminPageNotificationsMenu() { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /lib/zod/permission.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const PermissionModel = z.object({ 4 | id: z.number().int(), 5 | name: z.string(), 6 | displayName: z.string(), 7 | createdAt: z.date().nullish(), 8 | updatedAt: z.date().nullish(), 9 | }) 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # openssl rand -base64 32 2 | APP_SECRET_KEY="lM3yTtmnq/4x/oBRA17ETX5J1Vi1fwB7Mt4s+fKXqxQ=" 3 | GITHUB_CLIENT_ID="" 4 | GITHUB_CLIENT_SECRET="" 5 | NEXTAUTH_URL="http://localhost:3000/api/auth" 6 | DATABASE_URL="mysql://user:password@localhost/next-toko" 7 | SUPER_ADMIN_EMAIL="admin@example.net" -------------------------------------------------------------------------------- /lib/zod/store.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const StoreModel = z.object({ 4 | id: z.number().int(), 5 | ownerId: z.number().int(), 6 | name: z.string(), 7 | createdAt: z.date().nullish(), 8 | updatedAt: z.date().nullish(), 9 | description: z.string().nullish(), 10 | }) 11 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("next").NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | pageExtensions: ["js", "jsx", "ts", "tsx"], 5 | images: { 6 | domains: [ 7 | "0.gravatar.com", 8 | "avatars.githubusercontent.com" // github avatar 9 | ] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/screen-size.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /** @type {import("types/global").ResponsiveScreenSize} */ 3 | module.exports = { 4 | xxxxs: 260, 5 | xxxs: 319, 6 | xxs: 359, 7 | xs: 410, 8 | sm: 640, 9 | md: 768, 10 | lg: 1024, 11 | xlg: 1150, 12 | xl: 1280, 13 | "2xl": 1536, 14 | max: 1920 15 | }; 16 | -------------------------------------------------------------------------------- /pages/admin/profile.tsx: -------------------------------------------------------------------------------- 1 | import AdminProfile from "@/components/Admin/Profile"; 2 | import AdminPageLayout from "@/components/Layouts/AdminPage"; 3 | 4 | export default function Profile() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } -------------------------------------------------------------------------------- /server/appRouter.ts: -------------------------------------------------------------------------------- 1 | import { t } from "./trpc"; 2 | import { type AppRouter, appRouter as _appRouter } from "./routers/_app"; 3 | 4 | export const appRouter = t.mergeRouters( 5 | t.router({ 6 | hello: t.procedure.query(() => "Hello World"), 7 | }), 8 | _appRouter, 9 | ); 10 | 11 | export { AppRouter }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .next 3 | node_modules/ 4 | out/ 5 | .bak/ 6 | prisma/migrations 7 | server/generated 8 | 9 | #files 10 | .env 11 | .env.local 12 | package-lock.json 13 | public/sw.js 14 | public/workbox-*.js 15 | public/worker-*.js 16 | test.js 17 | test.mjs 18 | yarn-error.log 19 | total.util.js 20 | yarn.lock -------------------------------------------------------------------------------- /pages/admin/users/add.tsx: -------------------------------------------------------------------------------- 1 | 2 | import AdminPageLayout from "@/components/Layouts/AdminPage"; 3 | import { MdAdd } from "react-icons/md"; 4 | 5 | export default function Page() { 6 | return ( 7 | }> 8 | Add new User 9 | 10 | ); 11 | } -------------------------------------------------------------------------------- /lib/zod/datacountry.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const DataCountryModel = z.object({ 4 | id: z.number().int(), 5 | name: z.string(), 6 | lng: z.string().nullish(), 7 | lat: z.string().nullish(), 8 | icon: z.string().nullish(), 9 | createdAt: z.date().nullish(), 10 | updatedAt: z.date().nullish(), 11 | }) 12 | -------------------------------------------------------------------------------- /lib/zod/producttags.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const ProductTagsModel = z.object({ 4 | id: z.number().int(), 5 | name: z.string(), 6 | description: z.string(), 7 | image: z.string().nullish(), 8 | icon: z.string().nullish(), 9 | createdAt: z.date().nullish(), 10 | updatedAt: z.date().nullish(), 11 | }) 12 | -------------------------------------------------------------------------------- /lib/zod/databank.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const DataBankModel = z.object({ 4 | id: z.number().int(), 5 | name: z.string(), 6 | type: z.string(), 7 | code: z.string(), 8 | address: z.string().nullish(), 9 | phone: z.string().nullish(), 10 | fax: z.string().nullish(), 11 | website: z.string().nullish(), 12 | }) 13 | -------------------------------------------------------------------------------- /lib/zod/membership.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const MembershipModel = z.object({ 4 | id: z.number().int(), 5 | name: z.string(), 6 | description: z.string().nullish(), 7 | pricing: z.string().nullish(), 8 | isActive: z.boolean().nullish(), 9 | createdAt: z.date().nullish(), 10 | updatedAt: z.date().nullish(), 11 | }) 12 | -------------------------------------------------------------------------------- /server/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | let prisma : PrismaClient; 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | prisma = new PrismaClient(); 7 | } else { 8 | if (!global.prisma) { 9 | global.prisma = new PrismaClient(); 10 | } 11 | prisma = global.prisma; 12 | } 13 | 14 | export default prisma; 15 | -------------------------------------------------------------------------------- /styles/global.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | @import "./homepage-carousel.css"; 5 | 6 | /* * { 7 | @apply dark:border-list-divider-dark; 8 | } */ 9 | 10 | html, 11 | body { 12 | @apply font-rubik; 13 | } 14 | 15 | /* svg rect { 16 | fill: currentColor; 17 | } */ 18 | -------------------------------------------------------------------------------- /components/Utils/Inlined.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { PropsWithChildren } from "react"; 3 | 4 | export default function Inlined( 5 | props: PropsWithChildren<{ className?: string }> 6 | ) { 7 | return ( 8 |
9 | {props.children} 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /lib/zod/storefront.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const StoreFrontModel = z.object({ 4 | id: z.number().int(), 5 | storeId: z.number().int(), 6 | name: z.string(), 7 | description: z.string(), 8 | image: z.string().nullish(), 9 | icon: z.string().nullish(), 10 | createdAt: z.date().nullish(), 11 | updatedAt: z.date().nullish(), 12 | }) 13 | -------------------------------------------------------------------------------- /prisma/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | export async function getJsonData(filename: string) { 5 | try { 6 | const json = await fs.readFile( 7 | path.join(process.cwd(), `/lib/data/${filename}.json`), 8 | "utf-8" 9 | ); 10 | return JSON.parse(json); 11 | } catch (e) { 12 | throw e; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/zod/datacity.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const DataCityModel = z.object({ 4 | id: z.number().int(), 5 | provinceId: z.number().int().nullish(), 6 | name: z.string(), 7 | lng: z.string().nullish(), 8 | lat: z.string().nullish(), 9 | icon: z.string().nullish(), 10 | createdAt: z.date().nullish(), 11 | updatedAt: z.date().nullish(), 12 | }) 13 | -------------------------------------------------------------------------------- /lib/zod/datadistrict.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const DataDistrictModel = z.object({ 4 | id: z.number().int(), 5 | cityId: z.number().int().nullish(), 6 | name: z.string(), 7 | lng: z.string().nullish(), 8 | lat: z.string().nullish(), 9 | icon: z.string().nullish(), 10 | createdAt: z.date().nullish(), 11 | updatedAt: z.date().nullish(), 12 | }) 13 | -------------------------------------------------------------------------------- /lib/zod/dataprovince.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const DataProvinceModel = z.object({ 4 | id: z.number().int(), 5 | countryId: z.number().int().nullish(), 6 | name: z.string(), 7 | lng: z.string().nullish(), 8 | lat: z.string().nullish(), 9 | icon: z.string().nullish(), 10 | createdAt: z.date().nullish(), 11 | updatedAt: z.date().nullish(), 12 | }) 13 | -------------------------------------------------------------------------------- /lib/zod/datavillage.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const DataVillageModel = z.object({ 4 | id: z.number().int(), 5 | districtId: z.number().int().nullish(), 6 | name: z.string(), 7 | lng: z.string().nullish(), 8 | lat: z.string().nullish(), 9 | icon: z.string().nullish(), 10 | createdAt: z.date().nullish(), 11 | updatedAt: z.date().nullish(), 12 | }) 13 | -------------------------------------------------------------------------------- /pages/admin/index.tsx: -------------------------------------------------------------------------------- 1 | import { AdminPageDashboardIndex } from "@/components/Admin/Dashboard"; 2 | import AdminPageLayout from "@/components/Layouts/AdminPage"; 3 | export default function AdminPageIndex() { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | 11 | AdminPageIndex.protected = true; 12 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { getSession } from "next-auth/react" 3 | 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | const session = await getSession({ req }); 7 | const msg = `Hello! Wellcome Back ${(session?.user?.name || "Guest")}`; 8 | res.json({ 9 | msg 10 | }) 11 | } -------------------------------------------------------------------------------- /lib/zod/productcategories.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const ProductCategoriesModel = z.object({ 4 | id: z.number().int(), 5 | productCategoriesId: z.number().int().nullish(), 6 | name: z.string(), 7 | description: z.string(), 8 | image: z.string().nullish(), 9 | icon: z.string().nullish(), 10 | createdAt: z.date().nullish(), 11 | updatedAt: z.date().nullish(), 12 | }) 13 | -------------------------------------------------------------------------------- /lib/zod/product.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const ProductModel = z.object({ 4 | id: z.number().int(), 5 | storeId: z.number().int(), 6 | authorId: z.number().int(), 7 | storeFrontId: z.number().int(), 8 | name: z.string(), 9 | description: z.string().nullish(), 10 | price: z.number(), 11 | stock: z.number().int(), 12 | createdAt: z.date().nullish(), 13 | updatedAt: z.date().nullish(), 14 | }) 15 | -------------------------------------------------------------------------------- /lib/zod/storelocation.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const StoreLocationModel = z.object({ 4 | id: z.number().int(), 5 | storeId: z.number().int(), 6 | countryId: z.number().int().nullish(), 7 | provinceId: z.number().int().nullish(), 8 | cityId: z.number().int().nullish(), 9 | districtId: z.number().int().nullish(), 10 | villageId: z.number().int().nullish(), 11 | createdAt: z.date().nullish(), 12 | updatedAt: z.date().nullish(), 13 | }) 14 | -------------------------------------------------------------------------------- /lib/zod/userlocation.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const UserLocationModel = z.object({ 4 | id: z.number().int(), 5 | userId: z.number().int(), 6 | countryId: z.number().int().nullish(), 7 | provinceId: z.number().int().nullish(), 8 | cityId: z.number().int().nullish(), 9 | districtId: z.number().int().nullish(), 10 | villageId: z.number().int().nullish(), 11 | createdAt: z.date().nullish(), 12 | updatedAt: z.date().nullish(), 13 | }) 14 | -------------------------------------------------------------------------------- /components/Skeleton/ListSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import Skeleton from "./Skeleton"; 2 | import { fakeArray } from "@/lib/utils"; 3 | import clsx from "clsx"; 4 | 5 | export default function ListSkeleton(props: { 6 | size?: number; 7 | className?: string; 8 | }) { 9 | return ( 10 | <> 11 | {fakeArray(props.size).map((i, k) => ( 12 | 16 | ))} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[typescriptreact]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[javascript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[javascriptreact]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[css]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | } 17 | } -------------------------------------------------------------------------------- /lib/zod/productcomments.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const ProductCommentsModel = z.object({ 4 | id: z.number().int(), 5 | productId: z.number().int(), 6 | authorId: z.number().int(), 7 | productCommentsId: z.number().int().nullish(), 8 | type: z.string().nullish(), 9 | status: z.string().nullish(), 10 | rating: z.number().int().nullish(), 11 | description: z.string().nullish(), 12 | createdAt: z.date().nullish(), 13 | updatedAt: z.date().nullish(), 14 | }) 15 | -------------------------------------------------------------------------------- /pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import * as trpcNext from "@trpc/server/adapters/next"; 2 | import { appRouter as router } from "@/server/appRouter"; 3 | import { createContext } from "@/server/context"; 4 | 5 | export default trpcNext.createNextApiHandler({ 6 | router, 7 | createContext, 8 | onError({ error }) { 9 | if (error.code === "INTERNAL_SERVER_ERROR") { 10 | // send to bug reporting 11 | // console.error("Something went wrong", error); 12 | } 13 | }, 14 | batching: { 15 | enabled: true, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /components/Layouts/CopyrightFooter.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import Link from "next/link"; 3 | 4 | export default function CopyrightFooter({ className = "" }) { 5 | return ( 6 |
12 | Copyright © 13 | 17 | Super Toko 18 | 19 | 2022, All rights reserved. 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/Icon/SVGRaw.tsx: -------------------------------------------------------------------------------- 1 | export default function SVGRaw({ 2 | d, 3 | strokeWith, 4 | className 5 | }: { 6 | d: string, 7 | strokeWith?: number | undefined, 8 | className?: string 9 | }) { 10 | return ( 11 | 18 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/Layouts/FrontPage/NavbarMenu/Auth.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { signIn } from "next-auth/react"; 3 | import { Button } from "konsta/react"; 4 | import { useRouter } from "next/router"; 5 | 6 | export default function FrontPageNavbarMenuAuth() { 7 | const router = useRouter(); 8 | return ( 9 |
10 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { EnumRole } from "@prisma/client"; 2 | import { DefaultUser, Session, UserData } from "next-auth"; 3 | import { JWT } from "next-auth/jwt"; 4 | 5 | declare module "next-auth" { 6 | interface UserData extends DefaultUser { 7 | id?: number; 8 | role?: EnumRole; 9 | } 10 | interface Session { 11 | user?: UserData; 12 | } 13 | } 14 | declare module "next-auth/jwt" { 15 | interface JWT { 16 | userId?: number; 17 | } 18 | } 19 | declare module "next" { 20 | interface NextApiRequest extends NextApiRequest { 21 | session?: Session; 22 | user?: UserData; 23 | } 24 | } -------------------------------------------------------------------------------- /server/context.ts: -------------------------------------------------------------------------------- 1 | import prisma from "./prisma"; 2 | import * as trpcNext from "@trpc/server/adapters/next"; 3 | import { getSession } from "next-auth/react"; 4 | import { inferAsyncReturnType } from "@trpc/server"; 5 | import { Authorization } from "./Authorization"; 6 | 7 | export const createContext = async ({ 8 | req, 9 | res 10 | }: trpcNext.CreateNextContextOptions) => { 11 | let auth = new Authorization(prisma); 12 | await auth.init(await getSession({ req })); 13 | return { 14 | req, 15 | res, 16 | prisma, 17 | auth 18 | }; 19 | }; 20 | 21 | export type Context = inferAsyncReturnType; 22 | -------------------------------------------------------------------------------- /lib/hooks/useDarkMode.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorageState } from "ahooks"; 2 | import { useEffect } from "react"; 3 | 4 | export function useDarkMode() { 5 | const [dark, setDark] = useLocalStorageState("__dark_mode__", {defaultValue: false}); 6 | useEffect(() => { 7 | const ec: DOMTokenList = document.documentElement.classList ?? null; 8 | const key = "dark"; 9 | if ( 10 | dark || 11 | window.matchMedia(`(prefers-color-scheme: ${key})`).matches 12 | ) { 13 | if (!ec?.contains(key)) ec?.add(key); 14 | } else if (ec?.contains(key)) { 15 | ec?.remove(key); 16 | } 17 | }, [dark]); 18 | const toggle = () => setDark(!dark); 19 | return { toggle, dark }; 20 | } 21 | -------------------------------------------------------------------------------- /lib/zod/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user" 2 | export * from "./accounts" 3 | export * from "./membership" 4 | export * from "./role" 5 | export * from "./permission" 6 | export * from "./store" 7 | export * from "./storeteam" 8 | export * from "./product" 9 | export * from "./storefront" 10 | export * from "./productcategories" 11 | export * from "./producttags" 12 | export * from "./productcomments" 13 | export * from "./datacountry" 14 | export * from "./dataprovince" 15 | export * from "./datacity" 16 | export * from "./datadistrict" 17 | export * from "./datavillage" 18 | export * from "./storelocation" 19 | export * from "./userlocation" 20 | export * from "./databank" 21 | export * from "./cart" 22 | export * from "./cartitem" 23 | -------------------------------------------------------------------------------- /lib/zod/accounts.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const AccountsModel = z.object({ 4 | id: z.number().int(), 5 | userId: z.number().int(), 6 | type: z.string(), 7 | provider: z.string(), 8 | providerAccountId: z.string(), 9 | refresh_token: z.string().nullish(), 10 | refresh_token_expires_in: z.number().int().nullish(), 11 | access_token: z.string().nullish(), 12 | expires_at: z.number().int().nullish(), 13 | token_type: z.string().nullish(), 14 | scope: z.string().nullish(), 15 | id_token: z.string().nullish(), 16 | session_state: z.string().nullish(), 17 | oauth_token_secret: z.string().nullish(), 18 | oauth_token: z.string().nullish(), 19 | createdAt: z.date().nullish(), 20 | updatedAt: z.date().nullish(), 21 | }) 22 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | import { NextComponentType, NextPageContext } from "next"; 2 | import { Session } from "next-auth"; 3 | import { Router } from "next/router"; 4 | 5 | declare module "next/app" { 6 | type NextComponentTypeWithProps

> = 7 | NextComponentType & { 8 | protected?: Function | boolean; 9 | }; 10 | type AppProps = { 11 | Component: NextComponentTypeWithProps; 12 | router: Router; 13 | __N_SSG?: boolean; 14 | __N_SSP?: boolean; 15 | pageProps: P & { 16 | session?: Session; 17 | }; 18 | }; 19 | } 20 | 21 | type ResponsiveScreenSize = { 22 | xxxxs: number; 23 | xxxs: number; 24 | xxs: number; 25 | xs: number; 26 | sm: number; 27 | md: number; 28 | lg: number; 29 | xlg: number; 30 | xl: number; 31 | "2xl": number; 32 | max: number; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/zod/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const UserModel = z.object({ 4 | id: z.number().int(), 5 | name: z.string().nullish(), 6 | password: z.string().min(6, { message: "Password must be at least 6 characters" }).max(32, { message: "Password must be sohortan 32 characters" }).nullish(), 7 | username: z.string().min(3, { message: "Username must be at least 3 characters" }), 8 | email: z.string(), 9 | emailVerified: z.date().nullish(), 10 | image: z.string().nullish(), 11 | gender: z.string().nullish(), 12 | brithDate: z.date().nullish(), 13 | phone: z.string().min(10, { message: "Phone number must be at least 10 characters" }).nullish(), 14 | phoneVerified: z.date().nullish(), 15 | aboutMe: z.string().nullish(), 16 | createdAt: z.date().nullish(), 17 | updatedAt: z.date().nullish(), 18 | roleId: z.number().int().nullish(), 19 | membershipId: z.number().int().nullish(), 20 | }) 21 | -------------------------------------------------------------------------------- /components/Layouts/FrontPage/NavbarMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import FrontPageNavbarMenuAuth from "./Auth"; 2 | import FrontPageNavbarMenuCart from "./Cart"; 3 | import FrontPageNavbarMenuMessage from "./Message"; 4 | import FrontPageNavbarMenuUser from "./User"; 5 | 6 | export default function FrontPageNavbarMenu({ session }) { 7 | return ( 8 | <> 9 | {/* Vertical Divider */} 10 |


11 | {/* Regular Menu Icon */} 12 |
13 | {session.status === "authenticated" ? ( 14 | <> 15 | 16 | 17 | 18 | 19 | ) : ( 20 | 21 | )} 22 |
23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/Skeleton/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { HTMLProps, ReactNode } from "react"; 3 | 4 | export type SkeletonProps = { 5 | as?: string; 6 | animate?: "ping" | "pulse"; 7 | show?: boolean; 8 | className?: string; 9 | children?: ReactNode; 10 | loadingChildren?: JSX.Element | JSX.Element[]; 11 | }; 12 | 13 | export default function Skeleton({ 14 | animate = "pulse", 15 | show = true, 16 | className, 17 | loadingChildren, 18 | children, 19 | ...props 20 | // rome-ignore lint/suspicious/noExplicitAny: 21 | }: SkeletonProps & HTMLProps): any { 22 | return show ? ( 23 |
34 | {loadingChildren} 35 |
36 | ) : ( 37 | children || null 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/Layouts/Overlays.tsx: -------------------------------------------------------------------------------- 1 | import { Transition } from "@headlessui/react"; 2 | import { useUpdateEffect } from "ahooks"; 3 | import clsx from "clsx"; 4 | 5 | export default function Overlays({ 6 | show = false, 7 | className, 8 | shouldFocus = false 9 | }) { 10 | useUpdateEffect(() => { 11 | if (shouldFocus) { 12 | let clz = ["overflow-hidden"]; 13 | if (show) { 14 | document.body.classList.add(...clz); 15 | } else { 16 | document.body.classList.remove(...clz); 17 | } 18 | } 19 | }, [show]); 20 | return ( 21 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /components/Layouts/FrontPage/NavbarMenu/Message.tsx: -------------------------------------------------------------------------------- 1 | import { NavbarMenu } from "@/components/Menu/NavbarMenu"; 2 | import { Button } from "konsta/react"; 3 | import { SessionContextValue } from "next-auth/react"; 4 | import { FaBell, FaComments } from "react-icons/fa"; 5 | 6 | export default function FrontPageNavbarMenuMessage({ 7 | session 8 | }: { 9 | session: SessionContextValue; 10 | }) { 11 | return ( 12 | 17 | 18 | 19 | } 20 | > 21 |
22 |
23 | 24 |
25 |
26 | No Message 27 |
28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/Layouts/FrontPage/NavbarMenu/Cart.tsx: -------------------------------------------------------------------------------- 1 | import SVGRaw from "@/components/Icon/SVGRaw"; 2 | import { NavbarMenu } from "@/components/Menu/NavbarMenu"; 3 | import { Button } from "konsta/react"; 4 | import { SessionContextValue } from "next-auth/react"; 5 | import { FaCartPlus, FaShoppingCart } from "react-icons/fa"; 6 | 7 | export default function FrontPageNavbarMenuCart({ session }: { session: SessionContextValue}) { 8 | return ( 9 | 14 | 15 | 16 | } 17 | > 18 |
19 |
20 | 21 |
22 |
Your Cart Is Empty
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /server/routers/adminRouter.ts: -------------------------------------------------------------------------------- 1 | import { t } from "@/server/trpc"; 2 | import { Prisma } from "@prisma/client"; 3 | 4 | export const adminRouter = t.router({ 5 | stats: t.procedure.query(async ({ ctx }) => { 6 | ctx.auth.mustBeReallyAdmin(); 7 | const userCount = await ctx.prisma.user.count(); 8 | const productCount = await ctx.prisma.product.count(); 9 | const storeCount = await ctx.prisma.store.count(); 10 | return { 11 | userCount, 12 | productCount, 13 | storeCount, 14 | }; 15 | }), 16 | userSignups: t.procedure.query(async ({ ctx }) => { 17 | ctx.auth.mustBeReallyAdmin(); 18 | const users = await ctx.prisma.user.findMany({ 19 | select: { 20 | createdAt: true, 21 | }, 22 | orderBy: { 23 | createdAt: "asc", 24 | }, 25 | }); 26 | 27 | const signups = users.reduce((acc, user) => { 28 | const date = user.createdAt.toISOString().split("T")[0]; 29 | if (!acc[date]) { 30 | acc[date] = 0; 31 | } 32 | acc[date]++; 33 | return acc; 34 | }, {}); 35 | 36 | return Object.entries(signups).map(([date, count]) => ({ 37 | date, 38 | count, 39 | })); 40 | }), 41 | }); 42 | -------------------------------------------------------------------------------- /prisma/seed-region.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../server/prisma"; 2 | import { getJsonData } from "./utils"; 3 | 4 | export async function createRegion() { 5 | let data = await getJsonData("id-region"); 6 | let country = await prisma.dataCountry.create({ 7 | data: { 8 | name: "Indonesia", 9 | }, 10 | }); 11 | let uMap = (i: { name?: string; longitude?: never; latitude?: never }) => ({ 12 | name: "".concat(i.name), 13 | lng: "".concat(i.longitude || null), 14 | lat: "".concat(i.latitude || null), 15 | }); 16 | for (let prov of data) { 17 | let province = await prisma.dataProvince.create({ 18 | data: { ...uMap(prov), countryId: country.id }, 19 | }); 20 | for (let cit of prov.regencies) { 21 | let city = await prisma.dataCity.create({ 22 | data: { ...uMap(cit), provinceId: province.id }, 23 | }); 24 | for (let dis of cit.districts) { 25 | let district = await prisma.dataDistrict.create({ 26 | data: { ...uMap(dis), cityId: city.id }, 27 | }); 28 | for (let vil of dis.villages) { 29 | await prisma.dataVillage.create({ 30 | data: { ...uMap(vil), districtId: district.id }, 31 | }); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | async function main() { 38 | await createRegion(); 39 | } 40 | 41 | main().catch((e) => { 42 | throw e; 43 | }); 44 | -------------------------------------------------------------------------------- /lib/trpc.ts: -------------------------------------------------------------------------------- 1 | import { type AppRouter } from "@/server/appRouter"; 2 | import { httpBatchLink } from "@trpc/client"; 3 | import { createTRPCNext } from "@trpc/next"; 4 | import SuperJSON from "superjson"; 5 | 6 | function getBaseUrl() { 7 | if (typeof window !== "undefined") 8 | // browser should use relative path 9 | return ""; 10 | 11 | if (process.env.VERCEL_URL) 12 | // reference for vercel.com 13 | return `https://${process.env.VERCEL_URL}`; 14 | 15 | if (process.env.RENDER_INTERNAL_HOSTNAME) 16 | // reference for render.com 17 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; 18 | 19 | // assume localhost 20 | return `http://localhost:${process.env.PORT ?? 3000}`; 21 | } 22 | 23 | export const trpc = createTRPCNext({ 24 | config({ ctx }) { 25 | return { 26 | transformer: SuperJSON, 27 | links: [ 28 | httpBatchLink({ 29 | /** 30 | * If you want to use SSR, you need to use the server's full URL 31 | * @link https://trpc.io/docs/ssr 32 | **/ 33 | url: `${getBaseUrl()}/api/trpc`, 34 | }), 35 | ], 36 | /** 37 | * @link https://tanstack.com/query/v4/docs/reference/QueryClient 38 | **/ 39 | queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, 40 | }; 41 | }, 42 | /** 43 | * @link https://trpc.io/docs/ssr 44 | **/ 45 | ssr: false, 46 | }); 47 | -------------------------------------------------------------------------------- /pages/admin/users/index.tsx: -------------------------------------------------------------------------------- 1 | import AdminPageLayout from "@/components/Layouts/AdminPage"; 2 | import { trpc } from "@/lib/trpc"; 3 | import { Badge, List, ListInput, ListItem } from "konsta/react"; 4 | import { MdPeople } from "react-icons/md"; 5 | 6 | export default function Page() { 7 | // const { data: users } = trpc.useQuery([ 8 | // "user.all", 9 | // { 10 | // limit: 10, 11 | // cursor: 0, 12 | // search: null 13 | // } 14 | // ]); 15 | // console.log(users); 16 | return ( 17 | }> 18 |
19 | 20 | 21 | 10} 26 | /> 27 | 10} 31 | /> 32 | 10} 36 | /> 37 | 10} 41 | /> 42 | 43 | 44 |
    45 | 46 | 47 |
48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/Dialog/DialogConfirm.tsx: -------------------------------------------------------------------------------- 1 | import { useKeyPress } from "ahooks"; 2 | import { Card, Segmented, SegmentedButton } from "konsta/react"; 3 | import { PropsWithChildren, useState } from "react"; 4 | 5 | export default function DialogConfirm( 6 | props: PropsWithChildren<{ 7 | title?: string; 8 | textNo?: string; 9 | textOk?: string; 10 | onConfirm: (reponse: boolean) => void; 11 | }> 12 | ) { 13 | const [active, setActive] = useState(false); 14 | const keyName = ["Enter", "ArrowLeft", "ArrowRight"]; 15 | useKeyPress( 16 | (i) => keyName.includes(i.key), 17 | ({ key }) => { 18 | if (key === keyName[0]) { 19 | props.onConfirm(active); 20 | } 21 | if (key === keyName[1]) setActive(false); 22 | if (key === keyName[2]) setActive(true); 23 | } 24 | ); 25 | return ( 26 | {props.title || "Are you sure?"}} 29 | > 30 | {props.children} 31 | 32 | { 35 | setActive(false); 36 | props.onConfirm(false); 37 | }} 38 | > 39 | {props.textNo || "No"} 40 | 41 | { 44 | setActive(true); 45 | props.onConfirm(true); 46 | }} 47 | > 48 | {props.textOk || "Ok"} 49 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "strictNullChecks": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmit": true, 16 | "esModuleInterop": true, 17 | "module": "commonjs", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "jsx": "preserve", 22 | "incremental": true, 23 | "baseUrl": "./", 24 | "paths": { 25 | "konsta/shared/*": [ 26 | "node_modules/konsta/react/esm/shared/*" 27 | ], 28 | "@/components/*": [ 29 | "components/*" 30 | ], 31 | "@/lib/*": [ 32 | "lib/*" 33 | ], 34 | "@/store/*": [ 35 | "store/*" 36 | ], 37 | "@/server/*": [ 38 | "server/*" 39 | ], 40 | "@/controllers/*": [ 41 | "controllers/*" 42 | ], 43 | "@/styles/*": [ 44 | "styles/*" 45 | ] 46 | }, 47 | "typeRoots": [ 48 | "./types" 49 | ], 50 | }, 51 | "exclude": [ 52 | "dist", 53 | ".next", 54 | "styles", 55 | "out", 56 | "node_modules", 57 | "next.config.js", 58 | "postcss.config.js", 59 | "tailwind.config.js", 60 | "**/*.spec.ts", 61 | "**/*.spec.tsx", 62 | "**/*.test.ts", 63 | "**/*.test.tsx", 64 | "coverage" 65 | ], 66 | "include": [ 67 | "next-env.d.ts", 68 | "**/*.ts", 69 | "**/*.tsx" 70 | ] 71 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const screenSize = require("./lib/screen-size"); 3 | const konstaConfig = require("konsta/config"); 4 | const colors = require("./lib/colors"); 5 | const getColors = (prefix) => { 6 | const out = {}; 7 | Object.keys(colors).forEach((i) => { 8 | if (i.startsWith(prefix)) { 9 | let name = i.split(prefix)[1]; 10 | out[name] = colors[i]; 11 | } 12 | }); 13 | return out; 14 | }; 15 | 16 | /** @type {import("tailwindcss").Config} */ 17 | const tailwindConfig = { 18 | darkMode: "class", 19 | theme: { 20 | fontFamily: { 21 | rubik: ["Rubik", "Arial", "Helvetica", "sans-serif"] 22 | }, 23 | screens: Object.entries(screenSize).reduce( 24 | (a, [k, v]) => ((a[k] = `${v}px`), a), 25 | {} 26 | ), 27 | container: { 28 | padding: { 29 | lg: "3rem", 30 | xl: "4rem", 31 | "2xl": "5rem" 32 | } 33 | }, 34 | colors: { 35 | green: getColors("green-"), 36 | gray: getColors("grey-"), 37 | purple: getColors("purple-"), 38 | blue: getColors("blue-"), 39 | primary: { 40 | DEFAULT: colors["green-500"], 41 | light: colors["green-400"], 42 | dark: colors["green-700"] 43 | } 44 | } 45 | }, 46 | content: [ 47 | "./lib/**/*.(ts|tsx)", 48 | "./components/**/*.(ts|tsx)", 49 | "./pages/**/*.(ts|tsx)", 50 | "./layouts/**/*.(ts|tsx)", 51 | "./hooks/**/*.(ts|tsx)" 52 | ], 53 | variants: { 54 | typography: ["dark"] 55 | }, 56 | plugins: [require("./styles/plugins/scrollbar")] 57 | }; 58 | 59 | module.exports = konstaConfig(tailwindConfig); 60 | -------------------------------------------------------------------------------- /components/Menu/NavbarMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Menu, Transition } from "@headlessui/react"; 2 | import clsx from "clsx"; 3 | import Link from "next/link"; 4 | import { Fragment, ReactNode } from "react"; 5 | 6 | export const NavbarMenuItem = ({ 7 | text, 8 | href, 9 | }: { text: string; href: string }) => { 10 | return ( 11 | 12 | 19 | {text} 20 | 21 | 22 | ); 23 | }; 24 | 25 | export function NavbarMenu(props: { 26 | button?: ReactNode; 27 | children?: JSX.Element | JSX.Element[]; 28 | menuClass?: string; 29 | className?: string; 30 | }) { 31 | const { button = "Menu", children, menuClass, className } = props; 32 | return ( 33 | 34 | 35 | {button} 36 | 37 | 46 | 52 | {children} 53 | 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /styles/plugins/scrollbar.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /** @type { import('tailwindcss/plugin') } */ 3 | const plugin = require("tailwindcss/plugin"); 4 | const Color = require("color"); 5 | 6 | const track = "&::-webkit-scrollbar-track"; 7 | const thumb = "&::-webkit-scrollbar-thumb"; 8 | 9 | module.exports = plugin(({ addUtilities, theme }) => { 10 | // console.log(new Color(theme("colors.gray.400")) 11 | // .darken(0.2) 12 | // .toString()) 13 | const createScrollBar = () => { 14 | let obj = { 15 | ".scrollbar": { 16 | "&::-webkit-scrollbar": { 17 | height: theme("spacing.2"), 18 | width: theme("spacing.2"), 19 | }, 20 | [track]: { 21 | backgroundColor: theme("colors.gray.100"), 22 | }, 23 | [thumb]: { 24 | borderRadius: theme("borderRadius.lg"), 25 | backgroundColor: theme("colors.gray.400"), 26 | "&:hover": { 27 | backgroundColor: new Color(theme("colors.gray.400")) 28 | .darken(0.2) 29 | .toString(), 30 | }, 31 | }, 32 | }, 33 | }; 34 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].forEach((k) => { 35 | obj[`.scrollbar-${k}`] = { 36 | "&::-webkit-scrollbar": { 37 | height: theme(`spacing.${k}`), 38 | width: theme(`spacing.${k}`), 39 | }, 40 | }; 41 | }); 42 | // @ts-ignore 43 | Object.entries(theme("colors")).forEach(([k, v]) => { 44 | if (typeof v === "object") { 45 | Object.entries(v).forEach(([o, l]) => { 46 | obj[`.scrollbar-${k}-${o}`] = { 47 | [track]: { 48 | backgroundColor: theme(`colors.${k}.100`), 49 | }, 50 | [thumb]: { 51 | backgroundColor: l, 52 | "&:hover": { 53 | backgroundColor: new Color(l).darken(0.2).toString(), 54 | }, 55 | }, 56 | }; 57 | }); 58 | } 59 | }); 60 | return obj; 61 | }; 62 | addUtilities(createScrollBar()); 63 | }); 64 | -------------------------------------------------------------------------------- /components/Form/Input.tsx: -------------------------------------------------------------------------------- 1 | import { noop } from "@/lib/utils"; 2 | import clsx from "clsx"; 3 | import { HTMLProps } from "react"; 4 | import { forwardRef, useState } from "react"; 5 | 6 | const TextInput = forwardRef>( 7 | (props, ref) => { 8 | const { 9 | className = "", 10 | type = "text", 11 | label = "", 12 | onFocus = noop, 13 | onBlur = noop, 14 | onChange = noop, 15 | ...rest 16 | } = props; 17 | const [focus, setFocus] = useState(false); 18 | const [isEmpty, setIsEmpty] = useState(true); 19 | const handleChange = (e) => { 20 | if (e.target.value.length !== 0) { 21 | setIsEmpty(false); 22 | } else { 23 | setIsEmpty(true); 24 | } 25 | return onChange(e); 26 | }; 27 | return ( 28 |
37 | (setFocus(true), onFocus(e))} 42 | onBlur={(e) => (setFocus(false), onBlur(e))} 43 | onChange={handleChange} 44 | {...rest} 45 | /> 46 | {label && ( 47 | 58 | {label} 59 | 60 | )} 61 |
62 | ); 63 | } 64 | ); 65 | export default TextInput; 66 | -------------------------------------------------------------------------------- /components/Banner/HomepageCarousel.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Slider from "react-slick"; 3 | import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; 4 | import clsx from "clsx"; 5 | import { HTMLProps } from "react"; 6 | 7 | function Arrow({ 8 | onClick, 9 | className, 10 | style, 11 | children 12 | }: HTMLProps) { 13 | return ( 14 | 17 | ); 18 | } 19 | function CarouselItem({ children }) { 20 | return
{children}
; 21 | } 22 | export default function HomepageCarousel(props = {}) { 23 | return ( 24 |
25 | 36 | 37 | 38 | ), 39 | nextArrow: ( 40 | 41 | 42 | 43 | ) 44 | }} 45 | > 46 | 47 |
48 | {/* */} 54 | 55 | 56 |
57 | {/* */} 63 | 64 | 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /server/utils.ts: -------------------------------------------------------------------------------- 1 | import { NextApiResponse, NextApiRequest, NextApiHandler } from "next"; 2 | import { getSession } from "next-auth/react"; 3 | import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; 4 | import { ZodError } from "zod"; 5 | import { Context } from "./context"; 6 | import { TRPCErrorShape } from "@trpc/server/rpc"; 7 | import { ErrorFormatter } from "@trpc/server/dist/error/formatter"; 8 | 9 | export const restAsyncHandler = 10 | (handler: (req: NextApiRequest, res: NextApiResponse) => Promise) => 11 | (req: NextApiRequest, res: NextApiResponse) => 12 | handler(req, res).catch((e: Error | string) => { 13 | if (e instanceof ZodError) { 14 | return res.status(409).json({ 15 | success: false, 16 | type: "validationError", 17 | path: e.name, 18 | errors: e.errors, 19 | }); 20 | } 21 | if (typeof e === "string") e = new Error(e); 22 | res.json({ success: false, msg: e.message }); 23 | }); 24 | 25 | export const withSession = (handler: NextApiHandler) => 26 | restAsyncHandler(async (req, res) => { 27 | const session = await getSession({ req }); 28 | req.session = session; 29 | req.user = session?.user; 30 | return handler(req, res) as never; 31 | }); 32 | 33 | export const errorFormater: ErrorFormatter> = ({ 34 | shape, 35 | error, 36 | }) => { 37 | let other: {} | unknown; 38 | if (error.cause instanceof ZodError) { 39 | other = { 40 | type: "validationError", 41 | name: error.cause.name, 42 | errors: error.cause.errors, 43 | }; 44 | } 45 | if (error.cause instanceof PrismaClientKnownRequestError) { 46 | if ( 47 | error.cause.message.includes("Unique constraint failed on the fields") 48 | ) { 49 | other = { 50 | type: "prismaError", 51 | name: error.cause.message, 52 | errors: [error.cause.message], 53 | }; 54 | } 55 | shape.message = "Prisma Error"; 56 | } 57 | return { 58 | ...shape, 59 | data: { 60 | ...shape.data, 61 | other, 62 | }, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /components/Layouts/FrontPage/FooterMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Tabbar, TabbarLink } from "konsta/react"; 3 | import { signIn, useSession } from "next-auth/react"; 4 | import { useRouter } from "next/router"; 5 | import { forwardRef, HTMLProps, useEffect, useState } from "react"; 6 | import { 7 | HiOutlineChatAlt2, 8 | HiOutlineHome, 9 | HiOutlineShoppingCart, 10 | HiOutlineUserCircle 11 | } from "react-icons/hi"; 12 | 13 | const FrontPageFooterMenu = forwardRef< 14 | HTMLDivElement, 15 | HTMLProps 16 | >((props, ref) => { 17 | const session = useSession(); 18 | const router = useRouter(); 19 | const { className } = props; 20 | const [activeTab, setActiveTab] = useState(11); 21 | 22 | useEffect(() => { 23 | if (router.asPath === "/accounts") { 24 | setActiveTab(44); 25 | } 26 | }, [activeTab, router.asPath]); 27 | return ( 28 |
29 | 30 | { 33 | setActiveTab(11); 34 | router.push("/"); 35 | }} 36 | label={} 37 | /> 38 | setActiveTab(22)} 41 | label={} 42 | /> 43 | setActiveTab(33)} 46 | label={} 47 | /> 48 | { 51 | if (session.status !== "authenticated") { 52 | signIn(); 53 | } else { 54 | router.push("/accounts"); 55 | } 56 | }} 57 | label={} 58 | /> 59 | 60 |
61 | ); 62 | }); 63 | 64 | export default FrontPageFooterMenu; 65 | -------------------------------------------------------------------------------- /components/Layouts/Auth/index.tsx: -------------------------------------------------------------------------------- 1 | import { Preloader } from "konsta/react"; 2 | import { useSession } from "next-auth/react"; 3 | import Head from "next/head"; 4 | import { useRouter } from "next/router"; 5 | import { useEffect } from "react"; 6 | import CopyrightFooter from "../CopyrightFooter"; 7 | 8 | const Spinner = ({ text }: { text?: string }) => ( 9 |
10 | 11 |
{text || "Loading"}
12 |
13 | ); 14 | 15 | export default function AuthLayout({ 16 | title, 17 | header, 18 | children, 19 | redirectIfauthenticated = false 20 | }: { 21 | title?: string; 22 | header?: JSX.Element | JSX.Element[]; 23 | children: JSX.Element | JSX.Element[]; 24 | redirectIfauthenticated?: boolean; 25 | }) { 26 | const router = useRouter(); 27 | const { status } = useSession(); 28 | let component: JSX.Element | JSX.Element[] = ; 29 | if (status === "unauthenticated") { 30 | component = children; 31 | } else if (status === "authenticated") { 32 | component = redirectIfauthenticated ? ( 33 | 34 | ) : ( 35 | children 36 | ); 37 | } 38 | useEffect(() => { 39 | if (redirectIfauthenticated && status === "authenticated") { 40 | const callbackUrl = 41 | (Array.isArray(router.query?.callbackUrl) 42 | ? router.query?.callbackUrl[0] 43 | : router.query?.callbackUrl) || "/"; 44 | router.push(callbackUrl); 45 | } 46 | }, [status, router.query?.callbackUrl]); 47 | 48 | return ( 49 | <> 50 | 51 | {title || "Authentication"} 52 | 56 | 57 |
58 |
{header}
59 |
{component}
60 | 61 |
62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /pages/product/[id].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { trpc } from "@/lib/trpc"; 3 | import FrontPageLayout from "@/components/Layouts/FrontPage"; 4 | import { Button, Card, Preloader } from "konsta/react"; 5 | import { useState } from "react"; 6 | 7 | export default function ProductPage() { 8 | const router = useRouter(); 9 | const { id } = router.query; 10 | const { data: product, isLoading } = trpc.product.query.useQuery({ id: Number(id) }, { enabled: !!id }); 11 | const addToCart = trpc.cart.add.useMutation(); 12 | const [quantity, setQuantity] = useState(1); 13 | 14 | const handleAddToCart = () => { 15 | addToCart.mutate({ productId: product.id, quantity }); 16 | // You might want to show a toast notification here 17 | }; 18 | 19 | if (isLoading || !product) { 20 | return ( 21 | 22 |
23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | return ( 30 | 31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 |

{product.name}

39 |

${product.price.toFixed(2)}

40 |

{product.description}

41 |
42 | 43 | {quantity} 44 | 45 |
46 | 49 |
50 |
51 | 52 |
53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /server/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { t } from "@/server/trpc"; 2 | import { userRouter } from "./userRouter"; 3 | import { accountsRouter } from "./accountsRouter"; 4 | import { membershipRouter } from "./membershipRouter"; 5 | import { roleRouter } from "./roleRouter"; 6 | import { permissionRouter } from "./permissionRouter"; 7 | import { storeRouter } from "./storeRouter"; 8 | import { storeTeamRouter } from "./storeTeamRouter"; 9 | import { productRouter } from "./productRouter"; 10 | import { storeFrontRouter } from "./storeFrontRouter"; 11 | import { productCategoriesRouter } from "./productCategoriesRouter"; 12 | import { productTagsRouter } from "./productTagsRouter"; 13 | import { productCommentsRouter } from "./productCommentsRouter"; 14 | import { dataCountryRouter } from "./dataCountryRouter"; 15 | import { dataProvinceRouter } from "./dataProvinceRouter"; 16 | import { dataCityRouter } from "./dataCityRouter"; 17 | import { dataDistrictRouter } from "./dataDistrictRouter"; 18 | import { dataVillageRouter } from "./dataVillageRouter"; 19 | import { storeLocationRouter } from "./storeLocationRouter"; 20 | import { userLocationRouter } from "./userLocationRouter"; 21 | import { dataBankRouter } from "./dataBankRouter"; 22 | import { cartRouter } from "./cartRouter"; 23 | import { adminRouter } from "./adminRouter"; 24 | 25 | export const appRouter = t.router({ 26 | admin: adminRouter, 27 | user: userRouter, 28 | accounts: accountsRouter, 29 | membership: membershipRouter, 30 | role: roleRouter, 31 | permission: permissionRouter, 32 | store: storeRouter, 33 | storeTeam: storeTeamRouter, 34 | product: productRouter, 35 | storeFront: storeFrontRouter, 36 | productCategories: productCategoriesRouter, 37 | productTags: productTagsRouter, 38 | productComments: productCommentsRouter, 39 | dataCountry: dataCountryRouter, 40 | dataProvince: dataProvinceRouter, 41 | dataCity: dataCityRouter, 42 | dataDistrict: dataDistrictRouter, 43 | dataVillage: dataVillageRouter, 44 | storeLocation: storeLocationRouter, 45 | userLocation: userLocationRouter, 46 | dataBank: dataBankRouter, 47 | cart: cartRouter 48 | }); 49 | 50 | export type AppRouter = typeof appRouter; 51 | -------------------------------------------------------------------------------- /components/Layouts/FrontPage/NavbarMenu/User.tsx: -------------------------------------------------------------------------------- 1 | import SVGRaw from "@/components/Icon/SVGRaw"; 2 | import { SessionContextValue, signOut } from "next-auth/react"; 3 | import { NavbarMenu, NavbarMenuItem } from "@/components/Menu/NavbarMenu"; 4 | import { Button, Link } from "konsta/react"; 5 | 6 | export default function FrontPageNavbarMenuUser({ 7 | session 8 | }: { 9 | session: SessionContextValue; 10 | }) { 11 | const { data: sessionData } = session; 12 | return ( 13 | 17 | {""} 22 | 23 | {sessionData.user.name} 24 | 25 | 26 | ) : ( 27 | 30 | ) 31 | } 32 | > 33 | {sessionData?.user?.name ? ( 34 | <> 35 |
36 |

Wellcome Back!

37 | {""} 42 | {sessionData.user.name} 43 |
44 |
45 | 46 | ) : ( 47 |
48 | )} 49 | 50 | 51 |
52 | (e.preventDefault(), signOut())} 54 | className="block px-3 py-2 hover:transition-all hover:pl-5 hover:bg-gray-100 dark:hover:bg-bars-ios-dark" 55 | > 56 | SignOut 57 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js simple eCommerce 2 | 3 | This is a simple eCommerce application built with Next.js, tRPC, Prisma, and Tailwind CSS. 4 | 5 | [![Demo](https://img.youtube.com/vi/Eqtq1SDo5ZI/0.jpg)](https://www.youtube.com/watch?v=Eqtq1SDo5ZI) 6 | 7 | ## Features 8 | 9 | * **Homepage:** Displays a grid of products. 10 | * **Customer/Store Page:** A full-featured store page with product filtering, sorting, and infinite scrolling. 11 | * **Product Detail Page:** View details for a single product. 12 | * **Cart:** Fully functional shopping cart. Add, remove, and update quantities. 13 | * **Admin Dashboard:** A dashboard for administrators to view site statistics, including user counts, product counts, and user signups over time. 14 | * **Seller Dashboard:** A dashboard for sellers to manage their products (list, add, edit, delete). 15 | * **Authentication:** Users can sign in with a GitHub account. 16 | 17 | ## Simple Usage 18 | 19 | The project is now configured to use SQLite, so no external database setup is required. The database file will be created automatically at `prisma/dev.db`. 20 | 21 | In this case we use "pnpm". It is recommended to install "pnpm" first. 22 | 23 | ```bash 24 | $_ npm -g i pnpm 25 | ``` 26 | 27 | Follow these instructions to get started: 28 | 29 | ```bash 30 | $ git clone https://github.com/arisris/next-toko.git 31 | $ cd next-toko 32 | $ pnpm install 33 | $ pnpm prisma migrate dev 34 | $ pnpm dev 35 | ``` 36 | 37 | The `prisma migrate dev` command will create the SQLite database and run the seed script to populate it with initial data. 38 | 39 | You can then access the application at http://localhost:3000. 40 | 41 | - **Test login:** Use the GitHub provider at http://localhost:3000/api/auth/signin 42 | - **Admin page:** http://localhost:3000/admin (you will need to be logged in as an admin) 43 | - **Seller page:** http://localhost:3000/seller (you will need to be logged in as a user with a store) 44 | 45 | ## TODO 46 | 47 | - [x] Prepare move from graphql to trpc 48 | - [x] Admin Page 49 | - [x] Customer Page 50 | - [x] Seller Page 51 | - [x] Cart Page 52 | - [x] Homepage 53 | - [ ] ....?? 54 | 55 | ## Contribute 56 | 57 | So I'm really looking forward to your contribution to this repository. 58 | 59 | ## Links 60 | 61 | [Arisris.com](https://arisris.com/) 62 | -------------------------------------------------------------------------------- /components/Menu/NestedListMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Disclosure } from "@headlessui/react"; 2 | import { PropsOf } from "@headlessui/react/dist/types"; 3 | import clsx from "clsx"; 4 | import { List, ListItem } from "konsta/react"; 5 | import { PropsWithChildren } from "react"; 6 | import { FaChevronDown } from "react-icons/fa"; 7 | import ListSkeleton from "../Skeleton/ListSkeleton"; 8 | import NextLink from "next/link"; 9 | 10 | export interface NestedListMenuItemProps extends PropsOf { 11 | defaultOpen?: boolean; 12 | subMenu?: NestedListMenuItemProps[]; 13 | } 14 | 15 | export function NestedListMenuItem(props: NestedListMenuItemProps) { 16 | const { subMenu, defaultOpen, ...otherProps } = props; 17 | return subMenu ? ( 18 | 19 | {({ open }) => ( 20 | <> 21 | 32 | 33 | 34 | } 35 | /> 36 | 37 | {subMenu.map((item, key) => ( 38 | 39 | ))} 40 | 41 | 42 | )} 43 | 44 | ) : otherProps.href ? ( 45 | 46 | 47 | 48 | ) : ( 49 | 50 | ); 51 | } 52 | 53 | export function NestedListMenu( 54 | props: PropsWithChildren<{ 55 | isLoading?: boolean; 56 | skeletonSize?: number; 57 | data: NestedListMenuItemProps[]; 58 | }> = { isLoading: false, data: [] }, 59 | ) { 60 | return !props.isLoading ? ( 61 | 62 | {props.data.map((item, key) => ( 63 | 64 | ))} 65 | 66 | ) : ( 67 | <> 68 |
69 | 73 | 74 | ); 75 | } 76 | 77 | export default NestedListMenu; 78 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import HomepageCarousel from "@/components/Banner/HomepageCarousel"; 2 | import Skeleton from "@/components/Skeleton/Skeleton"; 3 | import FrontPageLayout from "components/Layouts/FrontPage"; 4 | import { trpc } from "@/lib/trpc"; 5 | import { Card } from "konsta/react"; 6 | 7 | function ProductCard({ product }) { 8 | return ( 9 | 12 |
13 |
14 | {/* You can add an image here if your product has one */} 15 | {/* {product.name} */} 16 |
17 |
18 |
19 |

{product.name}

20 |

{product.description?.substring(0, 50)}...

21 |
22 |
23 | 24 | ); 25 | } 26 | 27 | export default function Index() { 28 | const { data, isLoading } = trpc.product.all.useQuery({ limit: 18, cursor: null }); 29 | 30 | return ( 31 | 32 |
33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 | ( 45 | 49 | ))} 50 | /> 51 | 52 | 53 | {isLoading && Array(18) 54 | .fill(null) 55 | .map((_, k) => ( 56 | 60 | ))} 61 | {data?.items.map((product) => ( 62 | 63 | ))} 64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /server/Authorization.ts: -------------------------------------------------------------------------------- 1 | import { Permission, PrismaClient, Role, User } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { TRPC_ERROR_CODE_KEY } from "@trpc/server/rpc"; 4 | import { Session } from "next-auth"; 5 | 6 | export const mustBeReally = ( 7 | condition: boolean, 8 | err?: { message?: string; code?: TRPC_ERROR_CODE_KEY } 9 | ) => { 10 | if (condition) return true; 11 | throw new TRPCError({ 12 | code: err.code ?? "BAD_REQUEST", 13 | message: err.message ?? "Bad Request" 14 | }); 15 | }; 16 | 17 | export class Authorization { 18 | #user: 19 | | (User & { 20 | role: Role & { 21 | permissions: Permission[]; 22 | }; 23 | }) 24 | | null = null; 25 | constructor(private prisma: PrismaClient) { 26 | this.#user = null; 27 | } 28 | hasRole(name: string): boolean { 29 | return !!this.user && this.user.role.name === name; 30 | } 31 | // assignRoleTo(name: string) {} 32 | 33 | // hasPermission(...values: string[]) {} 34 | // givePermissionTo(...values: string[]) {} 35 | 36 | // can(...values: string[]) {} 37 | // cant(...values: string[]) {} 38 | 39 | get user() { 40 | return this.#user; 41 | } 42 | isAdmin() { 43 | return this.hasRole("ADMIN"); 44 | } 45 | mustBeReallyAdmin() { 46 | mustBeReally(this.isAdmin(), { 47 | message: "You are Not administrator", 48 | code: "UNAUTHORIZED" 49 | }); 50 | } 51 | isModerator() { 52 | return this.hasRole("moderator") || this.isAdmin(); 53 | } 54 | mustBeReallyModerator() { 55 | mustBeReally(this.isModerator(), { 56 | message: "You are Not moderator", 57 | code: "UNAUTHORIZED" 58 | }); 59 | } 60 | isUser() { 61 | return this.hasRole("user") || this.isModerator(); 62 | } 63 | mustBeReallyUser() { 64 | mustBeReally(this.isUser(), { 65 | message: "You are Not user", 66 | code: "UNAUTHORIZED" 67 | }); 68 | } 69 | isGuest() { 70 | return !this.user; 71 | } 72 | // initialize first 73 | async init(session: Session) { 74 | if (!session?.user?.email) return null; 75 | try { 76 | this.#user = await this.prisma.user.findUnique({ 77 | where: { email: session.user.email }, 78 | include: { 79 | role: { 80 | include: { 81 | permissions: true 82 | } 83 | } 84 | } 85 | }); 86 | } catch (e) { 87 | this.#user = null; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-toko", 3 | "version": "1.0.0", 4 | "description": "My expreiment using next.js", 5 | "main": "index.js", 6 | "keywords": [ 7 | "Vercel", 8 | "Jamstack", 9 | "Serverless", 10 | "Next.js" 11 | ], 12 | "author": "Aris Riswanto", 13 | "license": "ISC", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/arisris/next-toko.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/arisris/next-toko/issues" 20 | }, 21 | "homepage": "https://github.com/arisris/next-toko#readme", 22 | "scripts": { 23 | "start": "next start", 24 | "dev": "next dev", 25 | "build": "next build", 26 | "export": "next export" 27 | }, 28 | "prisma": { 29 | "seed": "ts-node -T ./prisma/seed.ts", 30 | "seed:region": "ts-node -T ./prisma/seed-region.ts" 31 | }, 32 | "dependencies": { 33 | "@headlessui/react": "^1.4.2", 34 | "@hookform/resolvers": "^2.8.8", 35 | "@prisma/client": "^3.7.0", 36 | "@tanstack/react-query": "^4.3.8", 37 | "@trpc/client": "^10.9.0", 38 | "@trpc/next": "^10.9.0", 39 | "@trpc/react-query": "^10.9.0", 40 | "@trpc/server": "^10.9.0", 41 | "ahooks": "^3.7.4", 42 | "bcryptjs": "^2.4.3", 43 | "chart.js": "^3.7.0", 44 | "clsx": "^1.1.1", 45 | "color": "^4.2.0", 46 | "konsta": "^1.0.2", 47 | "lodash": "^4.17.21", 48 | "moment": "^2.29.1", 49 | "next": "^13.1.4", 50 | "next-auth": "^4.18.8", 51 | "react": "^18.2.0", 52 | "react-dom": "^18.2.0", 53 | "react-hook-form": "^7.42.1", 54 | "react-icons": "^4.3.1", 55 | "react-query": "^3.34.12", 56 | "react-slick": "^0.29.0", 57 | "slugify": "^1.6.5", 58 | "storeon": "^3.1.4", 59 | "superjson": "^1.8.0", 60 | "validator": "^13.7.0", 61 | "zod": "^3.20.2" 62 | }, 63 | "devDependencies": { 64 | "@tailwindcss/forms": "^0.3.4", 65 | "@tailwindcss/typography": "^0.5.9", 66 | "@types/bcryptjs": "^2.4.2", 67 | "@types/faker": "^5.5.9", 68 | "@types/lodash": "^4.14.178", 69 | "@types/node": "^18.11.18", 70 | "@types/react": "^17.0.38", 71 | "autoprefixer": "^10.2.5", 72 | "faker": "^5.5.3", 73 | "postcss": "^8.4.21", 74 | "prettier": "^2.8.3", 75 | "prisma": "^3.7.0", 76 | "tailwindcss": "^3.2.4", 77 | "ts-node": "^10.4.0", 78 | "typescript": "^4.5.4", 79 | "zod-prisma": "^0.5.4" 80 | }, 81 | "prettier": { 82 | "arrowParens": "always", 83 | "singleQuote": false, 84 | "tabWidth": 2, 85 | "trailingComma": "none" 86 | } 87 | } -------------------------------------------------------------------------------- /pages/seller/index.tsx: -------------------------------------------------------------------------------- 1 | import FrontPageLayout from "@/components/Layouts/FrontPage"; 2 | import { trpc } from "@/lib/trpc"; 3 | import { Button, Card, List, ListItem, Preloader } from "konsta/react"; 4 | import Link from "next/link"; 5 | 6 | export default function SellerPage() { 7 | const { data: store } = trpc.store.myStore.useQuery(); 8 | const deleteProduct = trpc.product.delete.useMutation({ onSuccess: () => refetch() }); 9 | const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = trpc.product.all.useInfiniteQuery( 10 | { 11 | limit: 10, 12 | storeId: store?.id, 13 | }, 14 | { 15 | enabled: !!store, 16 | getNextPageParam: (lastPage) => lastPage.next, 17 | } 18 | ); 19 | 20 | return ( 21 | 22 |
23 |
24 |

Your Products

25 | 26 | 27 | 28 |
29 | 30 | {isLoading && } 31 | 32 | 33 | 34 | {data?.pages.map((page) => 35 | page.items.map((product) => ( 36 | 42 | 43 | 44 | 45 | 50 |
51 | } 52 | /> 53 | )) 54 | )} 55 | 56 | 57 | 58 | {hasNextPage && ( 59 |
60 | 63 |
64 | )} 65 |
66 | 67 | ); 68 | } 69 | 70 | SellerPage.protected = true; 71 | -------------------------------------------------------------------------------- /components/User/Layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import SVGRaw from "@/components/Icon/SVGRaw"; 2 | import FrontPageLayout from "@/components/Layouts/FrontPage"; 3 | import Inlined from "@/components/Utils/Inlined"; 4 | import { useResponsive } from "ahooks"; 5 | import { Button, Page, Panel } from "konsta/react"; 6 | import { PropsWithChildren, ReactElement, useState } from "react"; 7 | import { FaBars, FaTimes } from "react-icons/fa"; 8 | import { IconType } from "react-icons/lib"; 9 | import SidebarUserMenu from "./SidebarUserMenu"; 10 | 11 | export default function UserLayout( 12 | props: PropsWithChildren<{ title?: string; icon?: ReactElement }> 13 | ) { 14 | const [opened, setOpened] = useState(false); 15 | const screen = useResponsive(); 16 | return ( 17 | 18 |
19 |
20 | {screen.lg ? ( 21 | 22 | ) : ( 23 | setOpened(false)} 27 | size="w-72 min-h-screen overflow-y-auto" 28 | colors={{ 29 | bg: "bg-block-strong-light dark:bg-block-strong-dark" 30 | }} 31 | > 32 | 33 | 34 | )} 35 |
36 |
37 | {props.title && ( 38 | 39 | 40 | {props.icon || null} {props.title} 41 | 42 | 43 | 56 | 57 | 58 | )} 59 | {props.children} 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /server/routers/cartRouter.ts: -------------------------------------------------------------------------------- 1 | import { t } from "@/server/trpc"; 2 | import { z } from "zod"; 3 | 4 | export const cartRouter = t.router({ 5 | get: t.procedure.query(async ({ ctx }) => { 6 | ctx.auth.mustBeReallyUser(); 7 | const userId = ctx.auth.user.id; 8 | 9 | let cart = await ctx.prisma.cart.findUnique({ 10 | where: { userId }, 11 | include: { 12 | items: { 13 | include: { 14 | product: true, 15 | }, 16 | }, 17 | }, 18 | }); 19 | 20 | if (!cart) { 21 | cart = await ctx.prisma.cart.create({ 22 | data: { userId }, 23 | include: { 24 | items: { 25 | include: { 26 | product: true, 27 | }, 28 | }, 29 | }, 30 | }); 31 | } 32 | 33 | return cart; 34 | }), 35 | 36 | add: t.procedure 37 | .input( 38 | z.object({ 39 | productId: z.number(), 40 | quantity: z.number().min(1), 41 | }) 42 | ) 43 | .mutation(async ({ ctx, input }) => { 44 | ctx.auth.mustBeReallyUser(); 45 | const userId = ctx.auth.user.id; 46 | const { productId, quantity } = input; 47 | 48 | const cart = await ctx.prisma.cart.findUnique({ where: { userId } }); 49 | if (!cart) { 50 | await ctx.prisma.cart.create({ data: { userId } }); 51 | } 52 | 53 | const cartItem = await ctx.prisma.cartItem.findFirst({ 54 | where: { cart: { userId }, productId }, 55 | }); 56 | 57 | if (cartItem) { 58 | return ctx.prisma.cartItem.update({ 59 | where: { id: cartItem.id }, 60 | data: { quantity: cartItem.quantity + quantity }, 61 | }); 62 | } else { 63 | return ctx.prisma.cartItem.create({ 64 | data: { 65 | cart: { connect: { userId } }, 66 | product: { connect: { id: productId } }, 67 | quantity, 68 | }, 69 | }); 70 | } 71 | }), 72 | 73 | remove: t.procedure 74 | .input(z.object({ cartItemId: z.number() })) 75 | .mutation(async ({ ctx, input }) => { 76 | ctx.auth.mustBeReallyUser(); 77 | return ctx.prisma.cartItem.delete({ 78 | where: { id: input.cartItemId }, 79 | }); 80 | }), 81 | 82 | updateQuantity: t.procedure 83 | .input( 84 | z.object({ 85 | cartItemId: z.number(), 86 | quantity: z.number().min(1), 87 | }) 88 | ) 89 | .mutation(async ({ ctx, input }) => { 90 | ctx.auth.mustBeReallyUser(); 91 | return ctx.prisma.cartItem.update({ 92 | where: { id: input.cartItemId }, 93 | data: { quantity: input.quantity }, 94 | }); 95 | }), 96 | }); 97 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/global.css"; 2 | import { StoreContext } from "storeon/react"; 3 | import { SessionProvider, useSession } from "next-auth/react"; 4 | import { App as KonstaApp, Card, Page, Preloader } from "konsta/react"; 5 | import store from "@/store/index"; 6 | import { AppProps, NextComponentTypeWithProps } from "next/app"; 7 | import { ReactElement } from "react"; 8 | import { trpc } from "@/lib/trpc"; 9 | import { configResponsive } from "ahooks/es/configResponsive"; 10 | import screenSize from "@/lib/screen-size"; 11 | import { UseHeadlessuiDialogContextProvider } from "@/lib/hooks/useHeadlessuiDialog"; 12 | import { ToastContextProvider } from "@/lib/hooks/useToast"; 13 | import Overlays from "@/components/Layouts/Overlays"; 14 | import { MdAppRegistration } from "react-icons/md"; 15 | 16 | configResponsive(screenSize); 17 | function App({ Component, ...props }: AppProps) { 18 | // force required authentication for /admin path 19 | if (props.router.asPath.startsWith("/admin")) { 20 | if (!Component.protected) { 21 | Component.protected = true; 22 | } 23 | } 24 | const pageProps = props?.pageProps ?? {}; 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | {Component.protected ? ( 32 | 33 | 34 | 35 | ) : ( 36 | 37 | )} 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | function AuthorizePage({ 47 | children, 48 | Component, 49 | ...props 50 | }: AppProps & { 51 | children: ReactElement; 52 | Component: NextComponentTypeWithProps; 53 | }) { 54 | const isFn = typeof Component.protected === "function"; 55 | const session = useSession({ 56 | required: !isFn, 57 | }); 58 | 59 | // rome-ignore lint/complexity/noExtraBooleanCast: 60 | if (!!session?.data?.user) return children; 61 | 62 | return isFn ? ( 63 | // @ts-ignore 64 | Component.protected(children, props) 65 | ) : ( 66 | 67 | 68 | 69 | 70 |

Loading...

71 |
72 |
73 | ); 74 | } 75 | 76 | export default trpc.withTRPC(App); 77 | -------------------------------------------------------------------------------- /pages/seller/products/add.tsx: -------------------------------------------------------------------------------- 1 | import FrontPageLayout from "@/components/Layouts/FrontPage"; 2 | import { trpc } from "@/lib/trpc"; 3 | import { Button, Card, Input, List, ListItem, Preloader, Textarea } from "konsta/react"; 4 | import { useRouter } from "next/router"; 5 | import { useForm } from "react-hook-form"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { ProductModel } from "@/lib/zod"; 8 | 9 | const ProductForm = ({ onSubmit, defaultValues, isSubmitting }) => { 10 | const { register, handleSubmit, formState: { errors } } = useForm({ 11 | resolver: zodResolver(ProductModel.omit({ id: true, authorId: true, storeId: true, storeFrontId: true })), 12 | defaultValues, 13 | }); 14 | 15 | return ( 16 |
17 | 18 | 19 | } 22 | /> 23 | {errors.name &&

{errors.name.message}

} 24 | } 27 | /> 28 | {errors.description &&

{errors.description.message}

} 29 | } 32 | /> 33 | {errors.price &&

{errors.price.message}

} 34 | } 37 | /> 38 | {errors.stock &&

{errors.stock.message}

} 39 |
40 |
41 | 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default function AddProductPage() { 51 | const router = useRouter(); 52 | const addProduct = trpc.product.store.useMutation({ 53 | onSuccess: () => router.push("/seller"), 54 | }); 55 | 56 | const onSubmit = (data) => { 57 | addProduct.mutate(data); 58 | }; 59 | 60 | return ( 61 | 62 |
63 |

Add Product

64 | 65 |
66 |
67 | ); 68 | } 69 | 70 | AddProductPage.protected = true; 71 | -------------------------------------------------------------------------------- /components/Layouts/FrontPage/CategoriesMenu.tsx: -------------------------------------------------------------------------------- 1 | import SVGRaw from "@/components/Icon/SVGRaw"; 2 | import { useUpdateEffect, useKeyPress } from "ahooks"; 3 | import clsx from "clsx"; 4 | import { Button } from "konsta/react"; 5 | import Link from "next/link"; 6 | import { useRouter } from "next/router"; 7 | import { useRef, useState } from "react"; 8 | 9 | export default function FrontPageCategoriesMenu(props: { 10 | isNavHidden?: boolean; 11 | }) { 12 | const ref = useRef(); 13 | const router = useRouter(); 14 | const [open, setOpen] = useState(false); 15 | 16 | const handleToggle = () => { 17 | if (open) { 18 | ref.current!.classList.add("p-0", "opacity-0"); 19 | let t = setTimeout(() => { 20 | setOpen(false); 21 | document.body.classList.remove("overflow-hidden"); 22 | clearTimeout(t); 23 | }, 300); 24 | } else { 25 | document.body.classList.add("overflow-hidden"); 26 | setOpen(true); 27 | } 28 | }; 29 | useKeyPress("Escape", () => (open ? handleToggle() : null)); 30 | useUpdateEffect(() => { 31 | if (open) { 32 | router.events.on("hashChangeStart", handleToggle); 33 | return () => router.events.off("routeChangeStart", handleToggle); 34 | } 35 | }, [router.asPath]); 36 | 37 | return ( 38 |
49 | 59 |
65 | {Array(4) 66 | .fill(null) 67 | .map((_, i) => ( 68 | 69 | 80 | 81 | ))} 82 |
83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /styles/homepage-carousel.css: -------------------------------------------------------------------------------- 1 | /* Slick Carousel */ 2 | .homepage-carousel { 3 | width: 100%; 4 | height: 100%; 5 | position: relative; 6 | 7 | .slick-slider { 8 | @apply relative block select-none box-border z-0; 9 | } 10 | 11 | .slick-list { 12 | @apply relative block overflow-hidden m-0 p-0; 13 | } 14 | 15 | .slick-list:focus { 16 | @apply outline-none; 17 | } 18 | 19 | .slick-list.dragging { 20 | @apply cursor-pointer; 21 | } 22 | 23 | .slick-slider .slick-track, 24 | .slick-slider .slick-list { 25 | transform: translate3d(0, 0, 0); 26 | } 27 | 28 | .slick-track { 29 | @apply relative top-0 left-0 block; 30 | } 31 | 32 | .slick-track:before, 33 | .slick-track:after { 34 | @apply table content-none; 35 | } 36 | 37 | .slick-track:after { 38 | @apply clear-both; 39 | } 40 | 41 | .slick-loading .slick-track { 42 | @apply invisible; 43 | } 44 | 45 | .slick-slide { 46 | @apply hidden float-left h-full min-h-[1px]; 47 | } 48 | 49 | [dir="rtl"] .slick-slide { 50 | @apply float-right; 51 | } 52 | 53 | .slick-slide img { 54 | @apply block; 55 | } 56 | 57 | .slick-slide.slick-loading img { 58 | @apply hidden; 59 | } 60 | 61 | .slick-slide.dragging img { 62 | @apply pointer-events-none; 63 | } 64 | 65 | .slick-initialized .slick-slide { 66 | @apply block; 67 | } 68 | 69 | .slick-loading .slick-slide { 70 | @apply invisible; 71 | } 72 | 73 | .slick-vertical .slick-slide { 74 | border: 1px solid transparent; 75 | @apply block h-auto; 76 | } 77 | 78 | .slick-arrow.slick-hidden { 79 | @apply hidden; 80 | } 81 | 82 | .slick-arrow { 83 | @apply hidden md:block w-10 h-10 absolute top-[40%] z-10 cursor-pointer bg-gray-100 dark:bg-gray-700 rounded-full p-3 transition-all duration-500 hover:bg-gray-50 dark:hover:bg-gray-900 hover:shadow-lg touch-ripple-black; 84 | 85 | &.slick-next { 86 | @apply right-0 md:-translate-x-8 opacity-0; 87 | } 88 | 89 | &.slick-prev { 90 | @apply left-0 md:translate-x-8 opacity-0; 91 | } 92 | } 93 | 94 | .slick-slider:hover, 95 | .slick-slider:focus { 96 | .slick-arrow { 97 | &.slick-next { 98 | @apply md:translate-x-5 opacity-100; 99 | } 100 | 101 | &.slick-prev { 102 | @apply md:-translate-x-5 opacity-100; 103 | } 104 | } 105 | } 106 | 107 | .slick-dots { 108 | @apply absolute bottom-0 inset-x-0 z-10 list-none p-2; 109 | 110 | li { 111 | @apply list-none inline-flex bg-gray-300 m-0.5 rounded-full; 112 | 113 | button { 114 | @apply block text-xs w-2 h-2; 115 | } 116 | } 117 | 118 | li.slick-active { 119 | @apply bg-gray-400; 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /pages/cart/index.tsx: -------------------------------------------------------------------------------- 1 | import { trpc } from "@/lib/trpc"; 2 | import FrontPageLayout from "@/components/Layouts/FrontPage"; 3 | import { Button, Card, List, ListItem, Preloader } from "konsta/react"; 4 | import { useMemo } from "react"; 5 | import Link from "next/link"; 6 | 7 | export default function CartPage() { 8 | const { data: cart, isLoading, refetch } = trpc.cart.get.useQuery(); 9 | const updateQuantity = trpc.cart.updateQuantity.useMutation({ onSuccess: () => refetch() }); 10 | const removeItem = trpc.cart.remove.useMutation({ onSuccess: () => refetch() }); 11 | 12 | const totalPrice = useMemo(() => { 13 | return cart?.items.reduce((total, item) => total + item.product.price * item.quantity, 0) ?? 0; 14 | }, [cart]); 15 | 16 | if (isLoading) { 17 | return ( 18 | 19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | return ( 27 | 28 |
29 |

Your Cart

30 | {cart?.items.length === 0 ? ( 31 |

Your cart is empty.

32 | ) : ( 33 | 34 | 35 | {cart?.items.map((item) => ( 36 | 42 | 49 | {item.quantity} 50 | 51 | 54 |
55 | } 56 | /> 57 | ))} 58 | 59 |
60 | )} 61 |
62 |

Total: ${totalPrice.toFixed(2)}

63 | 64 | 67 | 68 |
69 |
70 | 71 | ); 72 | } 73 | 74 | CartPage.protected = true; 75 | -------------------------------------------------------------------------------- /server/routers/roleRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { RoleModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const roleRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: RoleModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.role.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: RoleModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.role.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.role.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.RoleWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.role.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.role.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/userRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { UserModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const userRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: UserModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.user.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: UserModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.user.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.user.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.UserWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.user.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.user.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/accountsRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { AccountsModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const accountsRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: AccountsModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.accounts.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: AccountsModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.accounts.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.accounts.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.AccountsWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.accounts.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.accounts.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/dataBankRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { DataBankModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const dataBankRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: DataBankModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.dataBank.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: DataBankModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.dataBank.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.dataBank.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.DataBankWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.dataBank.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.dataBank.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/dataCityRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { DataCityModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const dataCityRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: DataCityModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.dataCity.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: DataCityModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.dataCity.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.dataCity.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.DataCityWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.dataCity.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.dataCity.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/storeTeamRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { StoreTeamModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const storeTeamRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: StoreTeamModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.storeTeam.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: StoreTeamModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.storeTeam.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.storeTeam.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.StoreTeamWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.storeTeam.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.storeTeam.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/membershipRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { MembershipModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const membershipRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: MembershipModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.membership.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: MembershipModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.membership.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.membership.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.MembershipWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.membership.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.membership.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/permissionRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { PermissionModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const permissionRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: PermissionModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.permission.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: PermissionModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.permission.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.permission.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.PermissionWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.permission.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.permission.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/storeFrontRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { StoreFrontModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const storeFrontRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: StoreFrontModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.storeFront.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: StoreFrontModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.storeFront.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.storeFront.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.StoreFrontWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.storeFront.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.storeFront.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/dataCountryRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { DataCountryModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const dataCountryRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: DataCountryModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.dataCountry.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: DataCountryModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.dataCountry.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.dataCountry.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.DataCountryWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.dataCountry.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.dataCountry.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/dataVillageRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { DataVillageModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const dataVillageRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: DataVillageModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.dataVillage.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: DataVillageModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.dataVillage.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.dataVillage.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.DataVillageWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.dataVillage.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.dataVillage.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/productTagsRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { ProductTagsModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const productTagsRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: ProductTagsModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.productTags.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: ProductTagsModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.productTags.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.productTags.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.ProductTagsWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.productTags.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.productTags.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/dataDistrictRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { DataDistrictModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const dataDistrictRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: DataDistrictModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.dataDistrict.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: DataDistrictModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.dataDistrict.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.dataDistrict.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.DataDistrictWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.dataDistrict.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.dataDistrict.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/dataProvinceRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { DataProvinceModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const dataProvinceRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: DataProvinceModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.dataProvince.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: DataProvinceModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.dataProvince.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.dataProvince.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.DataProvinceWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.dataProvince.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.dataProvince.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/userLocationRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { UserLocationModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const userLocationRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: UserLocationModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.userLocation.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: UserLocationModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.userLocation.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.userLocation.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.UserLocationWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.userLocation.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.userLocation.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/storeLocationRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { StoreLocationModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const storeLocationRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: StoreLocationModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.storeLocation.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: StoreLocationModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.storeLocation.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.storeLocation.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.StoreLocationWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.storeLocation.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.storeLocation.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /pages/accounts/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import ListSkeleton from "@/components/Skeleton/ListSkeleton"; 3 | import { Tab } from "@headlessui/react"; 4 | import { Button } from "konsta/react"; 5 | import UserLayout from "@/components/User/Layouts"; 6 | import { FaCogs } from "react-icons/fa"; 7 | import { trpc } from "@/lib/trpc"; 8 | 9 | const TabBtn = ({ label = "" }) => { 10 | return ( 11 | 14 | clsx("w-full relative z-0", { 15 | "border-b-[3px] border-primary-light": selected 16 | }) 17 | } 18 | > 19 | {({ selected }) => ( 20 | 31 | )} 32 | 33 | ); 34 | }; 35 | 36 | export default function AccountsPageIndex() { 37 | //const { data: userData } = trpc.useQuery(["user.me"]) 38 | // const region = trpc.useQuery(["region.findMany"]); 39 | // console.log(region.data) 40 | return ( 41 | }> 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 |
57 |
58 | 59 |
60 | 61 |
62 |
63 | 64 |
65 | 66 |
67 |
68 | 69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 | ); 77 | } 78 | 79 | AccountsPageIndex.protected = true; 80 | -------------------------------------------------------------------------------- /pages/store/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { trpc } from "@/lib/trpc"; 3 | import FrontPageLayout from "@/components/Layouts/FrontPage"; 4 | import { Card, Input, Button, Block, Preloader } from "konsta/react"; 5 | import Link from "next/link"; 6 | 7 | function ProductCard({ product }) { 8 | return ( 9 | 10 | 13 |
14 |
15 |
16 |
17 |
18 |

{product.name}

19 |

{product.description?.substring(0, 50)}...

20 |

${product.price.toFixed(2)}

21 |
22 |
23 | 24 | 25 | ); 26 | } 27 | 28 | export default function StorePageIndex() { 29 | const [search, setSearch] = useState(""); 30 | const [sortBy, setSortBy] = useState(null); 31 | 32 | const { 33 | data, 34 | fetchNextPage, 35 | hasNextPage, 36 | isFetchingNextPage, 37 | isLoading, 38 | } = trpc.product.all.useInfiniteQuery( 39 | { 40 | limit: 12, 41 | search, 42 | sortBy, 43 | }, 44 | { 45 | getNextPageParam: (lastPage) => lastPage.next, 46 | } 47 | ); 48 | 49 | return ( 50 | 51 |
52 |
53 | setSearch(e.target.value)} 58 | className="w-1/3" 59 | /> 60 | 69 |
70 | 71 | {isLoading && } 72 | 73 |
74 | {data?.pages.map((page) => 75 | page.items.map((product) => ( 76 | 77 | )) 78 | )} 79 |
80 | 81 | {hasNextPage && ( 82 |
83 | 86 |
87 | )} 88 |
89 |
90 | ); 91 | } -------------------------------------------------------------------------------- /server/routers/productCommentsRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { ProductCommentsModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const productCommentsRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: ProductCommentsModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.productComments.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: ProductCommentsModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.productComments.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.productComments.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.ProductCommentsWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.productComments.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.productComments.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /server/routers/productCategoriesRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { ProductCategoriesModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const productCategoriesRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: ProductCategoriesModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.productCategories.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: ProductCategoriesModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.productCategories.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.productCategories.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.ProductCategoriesWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.productCategories.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.productCategories.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /components/Admin/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import Inlined from "@/components/Utils/Inlined"; 2 | import { 3 | BarController, 4 | BarElement, 5 | CategoryScale, 6 | Chart, 7 | ChartDataset, 8 | LinearScale 9 | } from "chart.js"; 10 | import { Card, Link } from "konsta/react"; 11 | import { useEffect, useRef, useState } from "react"; 12 | import { MdGroup, MdHome, MdShoppingCart } from "react-icons/md"; 13 | import colors from "@/lib/colors"; 14 | import { trpc } from "@/lib/trpc"; 15 | 16 | Chart.register([LinearScale, BarController, CategoryScale, BarElement]); 17 | 18 | const BarChart = ({ data }) => { 19 | const canvas = useRef(); 20 | const chart = useRef(); 21 | 22 | useEffect(() => { 23 | if (chart.current) { 24 | chart.current.destroy(); 25 | } 26 | chart.current = new Chart(canvas.current, { 27 | type: "bar", 28 | data: { 29 | labels: data.map(d => d.date), 30 | datasets: [{ 31 | label: "User Signups", 32 | data: data.map(d => d.count), 33 | backgroundColor: [ 34 | colors["blue-500"], 35 | colors["red-500"], 36 | colors["green-500"], 37 | colors["purple-500"], 38 | colors["yellow-500"], 39 | colors["teal-500"] 40 | ] 41 | }] 42 | }, 43 | options: { 44 | responsive: true, 45 | scales: { 46 | y: { 47 | beginAtZero: true 48 | } 49 | } 50 | } 51 | }); 52 | }, [data]); 53 | 54 | return ; 55 | }; 56 | 57 | export function AdminPageDashboardIndex() { 58 | const { data: stats } = trpc.admin.stats.useQuery(); 59 | const { data: userSignups } = trpc.admin.userSignups.useQuery(); 60 | 61 | const statCards = [ 62 | { name: "Users", value: stats?.userCount, icon: }, 63 | { name: "Products", value: stats?.productCount, icon: }, 64 | { name: "Stores", value: stats?.storeCount, icon: }, 65 | ]; 66 | 67 | return ( 68 |
69 |
70 | {statCards.map((stat, i) => ( 71 | Read More »}> 72 | 73 |
74 |

{stat.value ?? "..."}

75 | {stat.name} 76 |
77 |
78 | {stat.icon} 79 |
80 |
81 |
82 | ))} 83 |
84 | 85 | {userSignups && } 86 | 87 | 88 | 89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /pages/seller/products/edit/[id].tsx: -------------------------------------------------------------------------------- 1 | import FrontPageLayout from "@/components/Layouts/FrontPage"; 2 | import { trpc } from "@/lib/trpc"; 3 | import { Button, Card, Input, List, ListItem, Preloader, Textarea } from "konsta/react"; 4 | import { useRouter } from "next/router"; 5 | import { useForm } from "react-hook-form"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { ProductModel } from "@/lib/zod"; 8 | 9 | const ProductForm = ({ onSubmit, defaultValues, isSubmitting }) => { 10 | const { register, handleSubmit, formState: { errors } } = useForm({ 11 | resolver: zodResolver(ProductModel.partial()), 12 | defaultValues, 13 | }); 14 | 15 | return ( 16 |
17 | 18 | 19 | } 22 | /> 23 | {errors.name &&

{errors.name.message}

} 24 | } 27 | /> 28 | {errors.description &&

{errors.description.message}

} 29 | } 32 | /> 33 | {errors.price &&

{errors.price.message}

} 34 | } 37 | /> 38 | {errors.stock &&

{errors.stock.message}

} 39 |
40 |
41 | 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default function EditProductPage() { 51 | const router = useRouter(); 52 | const { id } = router.query; 53 | const { data: product, isLoading } = trpc.product.query.useQuery({ id: Number(id) }, { enabled: !!id }); 54 | const updateProduct = trpc.product.update.useMutation({ 55 | onSuccess: () => router.push("/seller"), 56 | }); 57 | 58 | const onSubmit = (data) => { 59 | updateProduct.mutate({ id: Number(id), data }); 60 | }; 61 | 62 | if (isLoading || !product) { 63 | return ( 64 | 65 |
66 | 67 |
68 |
69 | ); 70 | } 71 | 72 | return ( 73 | 74 |
75 |

Edit Product

76 | 77 |
78 |
79 | ); 80 | } 81 | 82 | EditProductPage.protected = true; 83 | -------------------------------------------------------------------------------- /server/routers/storeRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { StoreModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const storeRouter = t.router({ 8 | store: t.procedure 9 | .input( 10 | z 11 | .object({ 12 | data: StoreModel.omit({ id: true }) 13 | }) 14 | .required() 15 | ) 16 | .mutation(async ({ ctx, input }) => { 17 | ctx.auth.mustBeReallyUser(); 18 | let items = await ctx.prisma.store.create({ 19 | // @ts-expect-error 20 | data: { 21 | // todo 22 | } 23 | }); 24 | return items; 25 | }), 26 | update: t.procedure 27 | .input( 28 | z.object({ 29 | id: z.number(), 30 | data: StoreModel 31 | }) 32 | ) 33 | .mutation(async ({ ctx, input }) => { 34 | ctx.auth.mustBeReallyUser(); 35 | let items = await ctx.prisma.store.update({ 36 | where: { id: input.id }, 37 | data: { 38 | // todo 39 | } 40 | }); 41 | return items; 42 | }), 43 | delete: t.procedure 44 | .input( 45 | z.object({ 46 | id: z.number() 47 | }) 48 | ) 49 | .mutation(async ({ ctx, input }) => { 50 | ctx.auth.mustBeReallyUser(); 51 | let items = await ctx.prisma.store.delete({ 52 | where: { id: input.id } 53 | }); 54 | return items; 55 | }), 56 | all: t.procedure 57 | .input( 58 | z.object({ 59 | search: z.string().nullish(), 60 | limit: z.number(), 61 | cursor: z.number() 62 | }) 63 | ) 64 | .query(async ({ ctx, input }) => { 65 | let limit = input.limit ?? 10; 66 | let cursor = input.cursor; 67 | let where: Prisma.StoreWhereInput | undefined; 68 | if (input.search) { 69 | // where.name = { 70 | // contains: input.search 71 | // }; 72 | // where.OR = { 73 | // name: { 74 | // startsWith: input.search 75 | // } 76 | // }; 77 | } 78 | let items = await ctx.prisma.store.findMany({ 79 | take: limit + 1, 80 | cursor: cursor ? { id: cursor } : undefined, 81 | where 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.store.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }), 102 | myStore: t.procedure.query(async ({ ctx }) => { 103 | ctx.auth.mustBeReallyUser(); 104 | return ctx.prisma.store.findUnique({ 105 | where: { ownerId: ctx.auth.user.id }, 106 | }); 107 | }), 108 | }); 109 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | export default class MyDocument extends Document { 3 | render() { 4 | return ( 5 | 6 | 7 | 8 | {/* 13 | 18 | 23 | 28 | 33 | 38 | 43 | 48 | 53 | 59 | 65 | 71 | 77 | 78 | 79 | */} 83 | 84 | 85 | 86 | 87 |
88 | 89 | 90 | 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /public/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/User/Layouts/SidebarUserMenu.tsx: -------------------------------------------------------------------------------- 1 | import NestedListMenu, { 2 | NestedListMenuItemProps, 3 | } from "@/components/Menu/NestedListMenu"; 4 | import Skeleton from "@/components/Skeleton/Skeleton"; 5 | import { trpc } from "@/lib/trpc"; 6 | import clsx from "clsx"; 7 | import { Card, List, ListItem } from "konsta/react"; 8 | import Image from "next/image"; 9 | import { FaCheck, FaMoneyBill } from "react-icons/fa"; 10 | 11 | const menuData: NestedListMenuItemProps[] = [ 12 | { 13 | title: "Inbox", 14 | defaultOpen: true, 15 | subMenu: [ 16 | { 17 | title: "Chat 1", 18 | subMenu: [ 19 | { 20 | title: "Chat 2", 21 | subMenu: [ 22 | { 23 | title: "Chat 3", 24 | subMenu: [ 25 | { 26 | title: "Chat 4", 27 | }, 28 | { 29 | title: "Item No Sub", 30 | }, 31 | ], 32 | }, 33 | { 34 | title: "Item No Sub", 35 | }, 36 | ], 37 | }, 38 | { 39 | title: "Item No Sub", 40 | }, 41 | ], 42 | }, 43 | { 44 | title: "Product Discussion", 45 | }, 46 | { 47 | title: "Review", 48 | }, 49 | { 50 | title: "Help Support", 51 | }, 52 | { 53 | title: "Complain", 54 | }, 55 | { 56 | title: "Update", 57 | }, 58 | ], 59 | }, 60 | { 61 | title: "Purchase", 62 | subMenu: [ 63 | { 64 | title: "Waiting Payment", 65 | }, 66 | { 67 | title: "Transaction List", 68 | }, 69 | ], 70 | }, 71 | { 72 | title: "My Profile", 73 | subMenu: [ 74 | { 75 | title: "Wishlist", 76 | }, 77 | { 78 | title: "Store Favorite", 79 | }, 80 | { 81 | title: "Settings", 82 | }, 83 | ], 84 | }, 85 | ]; 86 | 87 | export default function SidebarUserMenu(props: { className?: string }) { 88 | const { data: user } = trpc.useQuery(["user.me", ["email"]]); 89 | return ( 90 | 91 | {user ? ( 92 | 93 | 103 | } 104 | title={user.name} 105 | subtitle={ 106 |
107 | 108 | Verified Accounts 109 | 110 | 111 |
112 | } 113 | /> 114 | } 116 | mediaClassName="text-primary -mr-1" 117 | strongTitle 118 | title="Saldo" 119 | titleWrapClassName="text-xl tracking-wide" 120 | /> 121 | RP. 520.105} /> 122 |
123 | ) : ( 124 |
125 | 126 | 127 |
128 | )} 129 | 130 |
131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /lib/hooks/useHeadlessuiDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import { 3 | createContext, 4 | Dispatch, 5 | Fragment, 6 | ReactElement, 7 | ReactNode, 8 | SetStateAction, 9 | useContext, 10 | useEffect, 11 | useState 12 | } from "react"; 13 | 14 | const defaultClasses = { 15 | rootClasses: 16 | "fixed flex flex-col justify-center items-center inset-0 z-50 overflow-y-auto", 17 | overlayClasses: "fixed inset-0 z-10 bg-black bg-opacity-30 cursor-pointer", 18 | dialogClasses: 19 | "asbolute z-50 flex justify-center flex-col items-center p-6 bg-transparent", 20 | overlayTransitionClasses: { 21 | enter: "ease-out duration-200", 22 | enterFrom: "opacity-0", 23 | enterTo: "opacity-100", 24 | leave: "ease-in duration-200", 25 | leaveFrom: "opacity-100", 26 | leaveTo: "opacity-0" 27 | }, 28 | dialogTransitionClasses: { 29 | enter: "ease-out duration-300", 30 | enterFrom: "opacity-0 scale-0", 31 | enterTo: "opacity-100 scale-100", 32 | leave: "ease-in duration-300", 33 | leaveFrom: "opacity-100 scale-100", 34 | leaveTo: "opacity-0 scale-0" 35 | } 36 | }; 37 | 38 | const Context = createContext<{ 39 | state: [ReactNode, Dispatch>]; 40 | config: [ 41 | Partial, 42 | Dispatch>> 43 | ]; 44 | } | null>(null); 45 | export function useHeadlessuiDialog( 46 | config?: Omit, "useRoot"> 47 | ) { 48 | const ctx = useContext(Context); 49 | if (!ctx) throw new Error("Undefined"); 50 | const { 51 | config: [, setConfig], 52 | state: [body, setBody] 53 | } = ctx; 54 | useEffect(() => { 55 | if (config) { 56 | setConfig((prev) => ({ ...prev, ...config })); 57 | } 58 | }, [config]); 59 | return { 60 | opened: !!body, 61 | create: (element: ReactElement) => setBody(element), 62 | destroy: () => setBody(null) 63 | }; 64 | } 65 | 66 | export const UseHeadlessuiDialogComponent = () => { 67 | const ctx = useContext(Context); 68 | if (!ctx) throw new Error("Undefined"); 69 | const { 70 | config: [config], 71 | state: [body, setBody] 72 | } = ctx; 73 | return ( 74 | 75 | setBody(null)} 78 | className={config.rootClasses} 79 | > 80 | 81 | setBody(null)} 84 | /> 85 | 86 | 87 |
{body}
88 |
89 |
90 |
91 | ); 92 | }; 93 | 94 | export function UseHeadlessuiDialogContextProvider(props: { 95 | children: ReactElement; 96 | config?: Partial & { useRoot?: boolean }; 97 | }) { 98 | let config = useState({ ...defaultClasses, ...(props.config || {}) }); 99 | let state = useState(); 100 | return ( 101 | 102 | {props.children} 103 | {props.config?.useRoot && } 104 | 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../server/prisma"; 2 | import crypto from "crypto"; 3 | import faker from "faker"; 4 | import { fakeArray } from "../lib/utils"; 5 | import { hashSync } from "bcryptjs"; 6 | import { Role } from "../store/enums"; 7 | import { Prisma } from "@prisma/client"; 8 | 9 | async function createUser() { 10 | const admin = await prisma.user.create({ 11 | data: { 12 | name: "Admin", 13 | username: "admin", 14 | email: "admin@example.net", 15 | password: hashSync("password123", 10), 16 | emailVerified: new Date(), 17 | image: `https://0.gravatar.com/avatar/${crypto 18 | .createHash("md5") 19 | .update("admin@example.net") 20 | .digest("hex")}`, 21 | }, 22 | }); 23 | const fakeUser = (): Prisma.UserCreateInput => ({ 24 | name: `${faker.name.firstName()} ${faker.name.lastName()}`, 25 | username: faker.internet.userName(), 26 | email: faker.internet.email(), 27 | password: hashSync("password", 10), 28 | emailVerified: new Date(), 29 | image: `https://0.gravatar.com/avatar/${crypto 30 | .createHash("md5") 31 | .update(faker.internet.email()) 32 | .digest("hex")}`, 33 | }); 34 | const usersData = fakeArray(5).map(() => fakeUser()); 35 | const users = []; 36 | for (const userData of usersData) { 37 | const user = await prisma.user.create({ data: userData }); 38 | users.push(user); 39 | } 40 | return { 41 | admin, 42 | users, 43 | }; 44 | } 45 | 46 | async function createPermissionRole(users) { 47 | const roles = [ 48 | { 49 | name: Role.ADMIN, 50 | displayName: "Admin", 51 | }, 52 | { 53 | name: Role.USER, 54 | displayName: "User", 55 | }, 56 | ]; 57 | for (const role of roles) { 58 | await prisma.role.create({ data: role }); 59 | } 60 | await prisma.user.update({ 61 | where: { id: users.admin.id }, 62 | data: { 63 | role: { 64 | connect: { 65 | name: Role.ADMIN, 66 | }, 67 | }, 68 | }, 69 | }); 70 | await prisma.user.updateMany({ 71 | where: { roleId: null }, 72 | data: { 73 | roleId: (await prisma.role.findUnique({ where: { name: Role.USER } })).id, 74 | }, 75 | }); 76 | } 77 | 78 | async function createProducts(admin) { 79 | const store = await prisma.store.create({ 80 | data: { 81 | name: "Admin Store", 82 | ownerId: admin.id, 83 | }, 84 | }); 85 | 86 | const storeFront = await prisma.storeFront.create({ 87 | data: { 88 | name: "Main Storefront", 89 | storeId: store.id, 90 | description: "This is the main storefront", 91 | }, 92 | }); 93 | 94 | const productCategory = await prisma.productCategories.create({ 95 | data: { 96 | name: "Default Category", 97 | description: "Default category for all products", 98 | }, 99 | }); 100 | 101 | const fakeProduct = (): Prisma.ProductCreateManyInput => ({ 102 | name: faker.commerce.productName(), 103 | description: faker.commerce.productDescription(), 104 | price: parseFloat(faker.commerce.price()), 105 | stock: faker.datatype.number(100), 106 | storeId: store.id, 107 | authorId: admin.id, 108 | storeFrontId: storeFront.id, 109 | }); 110 | 111 | for (let i = 0; i < 20; i++) { 112 | await prisma.product.create({ 113 | data: { 114 | ...fakeProduct(), 115 | productCategories: { 116 | connect: { id: productCategory.id }, 117 | } 118 | } 119 | }); 120 | } 121 | } 122 | 123 | async function main() { 124 | const users = await createUser(); 125 | await createPermissionRole(users); 126 | await createProducts(users.admin); 127 | } 128 | 129 | main().catch((e) => { 130 | throw e; 131 | }); 132 | -------------------------------------------------------------------------------- /server/routers/productRouter.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { ProductModel } from "@/lib/zod"; 4 | import { t } from "@/server/trpc"; 5 | import { z } from "zod"; 6 | 7 | export const productRouter = t.router({ 8 | store: t.procedure 9 | .input(ProductModel.omit({ id: true, authorId: true, storeId: true, storeFrontId: true })) 10 | .mutation(async ({ ctx, input }) => { 11 | ctx.auth.mustBeReallyUser(); 12 | const store = await ctx.prisma.store.findUnique({ where: { ownerId: ctx.auth.user.id } }); 13 | if (!store) throw new TRPCError({ code: "FORBIDDEN", message: "You do not have a store." }); 14 | 15 | return ctx.prisma.product.create({ 16 | data: { 17 | ...input, 18 | authorId: ctx.auth.user.id, 19 | storeId: store.id, 20 | storeFrontId: store.storeFront[0].id, // a store should have a storefront 21 | }, 22 | }); 23 | }), 24 | update: t.procedure 25 | .input(z.object({ id: z.number(), data: ProductModel.partial() })) 26 | .mutation(async ({ ctx, input }) => { 27 | ctx.auth.mustBeReallyUser(); 28 | const product = await ctx.prisma.product.findUnique({ where: { id: input.id } }); 29 | if (!product || product.authorId !== ctx.auth.user.id) { 30 | throw new TRPCError({ code: "FORBIDDEN", message: "You do not own this product." }); 31 | } 32 | return ctx.prisma.product.update({ 33 | where: { id: input.id }, 34 | data: input.data, 35 | }); 36 | }), 37 | delete: t.procedure 38 | .input(z.object({ id: z.number() })) 39 | .mutation(async ({ ctx, input }) => { 40 | ctx.auth.mustBeReallyUser(); 41 | const product = await ctx.prisma.product.findUnique({ where: { id: input.id } }); 42 | if (!product || product.authorId !== ctx.auth.user.id) { 43 | throw new TRPCError({ code: "FORBIDDEN", message: "You do not own this product." }); 44 | } 45 | return ctx.prisma.product.delete({ 46 | where: { id: input.id }, 47 | }); 48 | }), 49 | all: t.procedure 50 | .input( 51 | z.object({ 52 | search: z.string().nullish(), 53 | limit: z.number(), 54 | cursor: z.number().nullish(), 55 | sortBy: z.string().nullish(), 56 | storeId: z.number().nullish(), 57 | }) 58 | ) 59 | .query(async ({ ctx, input }) => { 60 | let limit = input.limit ?? 10; 61 | let cursor = input.cursor; 62 | let where: Prisma.ProductWhereInput = {}; 63 | if (input.search) { 64 | where.name = { contains: input.search }; 65 | } 66 | if (input.storeId) { 67 | where.storeId = input.storeId; 68 | } 69 | 70 | let orderBy: Prisma.ProductOrderByWithRelationInput = {}; 71 | if (input.sortBy === 'price-asc') { 72 | orderBy = { price: 'asc' }; 73 | } else if (input.sortBy === 'price-desc') { 74 | orderBy = { price: 'desc' }; 75 | } 76 | 77 | let items = await ctx.prisma.product.findMany({ 78 | take: limit + 1, 79 | cursor: cursor ? { id: cursor } : undefined, 80 | where, 81 | orderBy, 82 | }); 83 | let next: typeof cursor | null = null; 84 | if (items.length > limit) { 85 | let nextItem = items.pop(); 86 | next = nextItem!.id; 87 | } 88 | return { items, next }; 89 | }), 90 | query: t.procedure 91 | .input( 92 | z.object({ 93 | id: z.number() 94 | }) 95 | ) 96 | .query(async ({ ctx, input }) => { 97 | let items = await ctx.prisma.product.findUnique({ 98 | where: { id: input.id } 99 | }); 100 | return items; 101 | }) 102 | }); 103 | -------------------------------------------------------------------------------- /components/Layouts/FrontPage/SearchForm.tsx: -------------------------------------------------------------------------------- 1 | import SVGRaw from "@/components/Icon/SVGRaw"; 2 | import { Transition } from "@headlessui/react"; 3 | import { useKeyPress, useClickAway } from "ahooks"; 4 | import clsx from "clsx"; 5 | import Link from "next/link"; 6 | import { useRef, useState } from "react"; 7 | import Overlays from "../Overlays"; 8 | 9 | export default function FrontPageSearchForm() { 10 | const ref = useRef(); 11 | const inputRef = useRef(); 12 | const [focus, setFocus] = useState(false); 13 | 14 | useClickAway(() => setFocus(false), [ref]); 15 | useKeyPress( 16 | "/", 17 | (e) => 18 | !focus && (e.preventDefault(), setFocus(true), inputRef.current!.focus()) 19 | ); 20 | useKeyPress( 21 | "Escape", 22 | (e) => 23 | focus && (e.preventDefault(), setFocus(false), inputRef.current!.blur()) 24 | ); 25 | 26 | return ( 27 | <> 28 | {/* Overlays */} 29 | 30 | {/* Search form */} 31 |
37 |
43 |
44 | setFocus(true)} 52 | onChange={(e) => { 53 | if (e.target.value.length > 0) { 54 | setFocus(true); 55 | } else { 56 | setFocus(false); 57 | } 58 | }} 59 | /> 60 | 66 |
67 | 79 |
84 | {Array(3) 85 | .fill(null) 86 | .map((_, i) => ( 87 | 92 | Product {i} 93 | 94 | ))} 95 |
96 | {[0].map((i) => ( 97 | 102 | Search product {i} 103 | 104 | ))} 105 |
106 |
107 |
108 | 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { Account } from "next-auth"; 2 | import GithubProvider from "next-auth/providers/github"; 3 | import CredentialsProvider from "next-auth/providers/credentials"; 4 | import { NextApiRequest, NextApiResponse } from "next"; 5 | import prisma from "@/server/prisma"; 6 | import { compare } from "bcryptjs"; 7 | import slugify from "slugify"; 8 | import { GUID } from "@/lib/utils"; 9 | 10 | export default async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse, 13 | ) { 14 | return await NextAuth(req, res, { 15 | providers: [ 16 | CredentialsProvider({ 17 | id: "credentials", 18 | name: "Credentials", 19 | type: "credentials", 20 | credentials: { 21 | email: { 22 | label: "Email", 23 | type: "email", 24 | placeholder: "john.doe@example.net", 25 | }, 26 | password: { 27 | label: "Password", 28 | type: "Password", 29 | placeholder: "Password", 30 | }, 31 | }, 32 | // @ts-expect-error 33 | authorize: async (cred) => { 34 | try { 35 | if (!cred) throw new Error("Error"); 36 | const user = await prisma.user.findUnique({ 37 | where: { email: cred.email }, 38 | }); 39 | if (!user) return null; 40 | if (!compare(cred.password, user.password || "")) return null; 41 | return user; 42 | } catch (e) { 43 | return null; 44 | } 45 | }, 46 | }), 47 | // GithubProvider({ 48 | // clientId: process.env.GITHUB_CLIENT_ID, 49 | // clientSecret: process.env.GITHUB_CLIENT_SECRET 50 | // }) 51 | ], 52 | callbacks: { 53 | async signIn(args) { 54 | if (args.account?.type === "credentials") return true; 55 | if ( 56 | args.account?.type === "oauth" && 57 | args.user.email && 58 | args.user.name 59 | ) { 60 | if (args.account.provider !== "github") return false; // only github for now 61 | 62 | // check existing user 63 | const existingUser = await prisma.user.findFirst({ 64 | where: { 65 | accounts: { 66 | some: { 67 | provider: args.account.provider, 68 | providerAccountId: args.account.providerAccountId, 69 | }, 70 | }, 71 | }, 72 | include: { 73 | accounts: true, 74 | }, 75 | }); 76 | 77 | if (existingUser) { 78 | // user exists now check token is same or not: if not same update them 79 | //console.log("User exists:") 80 | if ( 81 | !existingUser.accounts.some( 82 | (acc) => acc.access_token === args.account?.access_token, 83 | ) 84 | ) { 85 | //console.log("Updating token") 86 | let { 87 | access_token, 88 | expires_at, 89 | refresh_token, 90 | refresh_token_expires_in, 91 | } = args.account; 92 | await prisma.user.update({ 93 | where: { 94 | id: existingUser.id, 95 | }, 96 | data: { 97 | accounts: { 98 | // @ts-expect-error 99 | updateMany: { 100 | where: { 101 | userId: existingUser.id, 102 | providerAccountId: args.account.providerAccountId, 103 | }, 104 | data: { 105 | access_token, 106 | expires_at, 107 | refresh_token, 108 | refresh_token_expires_in, 109 | }, 110 | }, 111 | }, 112 | }, 113 | }); 114 | } 115 | return true; 116 | } 117 | 118 | // create new user if not exists 119 | let { userId, ...payload } = args.account; 120 | const newUser = await prisma.user.create({ 121 | data: { 122 | name: args.user.name, 123 | image: args.user.image, 124 | email: args.user.email, 125 | username: slugify(`${args.user.name.split(" ")[0]}.${GUID(3)}`, { 126 | lower: true, 127 | replacement: ".", 128 | }), 129 | accounts: { 130 | create: payload, 131 | }, 132 | role: { 133 | connect: { 134 | name: "user", 135 | }, 136 | }, 137 | }, 138 | }); 139 | if (newUser) return true; 140 | } 141 | return false; 142 | }, 143 | }, 144 | secret: process.env.APP_SECRET_KEY, 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /prisma/gen-crud-router.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { Prisma } from "@prisma/client"; 3 | import prisma from "../server/prisma"; 4 | import pkg from "../package.json"; 5 | import fs from "fs"; 6 | import path from "path"; 7 | import _ from "lodash"; 8 | import clsx from "clsx"; 9 | 10 | const modelName = _(Prisma.ModelName).map((i) => _.lowerFirst(i)); 11 | const dataModels = Prisma.dmmf.datamodel.models; 12 | const dataEnums = Prisma.dmmf.datamodel.enums; 13 | 14 | let output = ` 15 | /** 16 | * This is auto generated file 17 | **/ 18 | `; 19 | let arr: { name: string; fileName: string; output: string }[] = []; 20 | 21 | modelName.forEach((m) => { 22 | arr.push({ 23 | name: m, 24 | fileName: `${m}Router.ts`, 25 | output: `import { Prisma, PrismaClient } from "@prisma/client"; 26 | import { TRPCError } from "@trpc/server"; 27 | import { ${_.upperFirst(m)}Model } from "@/lib/zod"; 28 | import { t } from "@/server/trpc"; 29 | import { z } from "zod"; 30 | 31 | export const ${m}Router = t.router({ 32 | store: t.procedure 33 | .input( 34 | z.object({ 35 | data: ${_.upperFirst(m)}Model.omit({id: true}) 36 | }).required() 37 | ) 38 | .mutation(async ({ctx, input}) => { 39 | ctx.auth.mustBeReallyUser(); 40 | let items = await ctx.prisma.${m}.create({ 41 | // @ts-expect-error 42 | data: { 43 | // todo 44 | } 45 | }); 46 | return items; 47 | }), 48 | update: t.procedure 49 | .input( 50 | z.object({ 51 | id: z.number(), 52 | data: ${_.upperFirst(m)}Model 53 | }) 54 | ) 55 | .mutation(async ({ctx, input}) => { 56 | ctx.auth.mustBeReallyUser(); 57 | let items = await ctx.prisma.${m}.update({ 58 | where: { id: input.id }, 59 | data: { 60 | // todo 61 | } 62 | }); 63 | return items; 64 | }), 65 | delete: t.procedure 66 | .input( 67 | z.object({ 68 | id: z.number() 69 | }) 70 | ) 71 | .mutation(async ({ctx, input}) => { 72 | ctx.auth.mustBeReallyUser(); 73 | let items = await ctx.prisma.${m}.delete({ 74 | where: { id: input.id } 75 | }); 76 | return items; 77 | }), 78 | all: t.procedure 79 | .input( 80 | z.object({ 81 | search: z.string().nullish(), 82 | limit: z.number(), 83 | cursor: z.number() 84 | }) 85 | ) 86 | .query(async ({ctx, input}) => { 87 | let limit = input.limit ?? 10; 88 | let cursor = input.cursor; 89 | let where: Prisma.${_.upperFirst(m)}WhereInput | undefined; 90 | if (input.search) { 91 | // where.name = { 92 | // contains: input.search 93 | // }; 94 | // where.OR = { 95 | // name: { 96 | // startsWith: input.search 97 | // } 98 | // }; 99 | } 100 | let items = await ctx.prisma.${m}.findMany({ 101 | take: limit + 1, 102 | cursor: cursor ? { id: cursor } : undefined, 103 | where 104 | }); 105 | let next: typeof cursor | null = null; 106 | if (items.length > limit) { 107 | let nextItem = items.pop(); 108 | next = nextItem!.id; 109 | } 110 | return { items, next }; 111 | }), 112 | query: t.procedure 113 | .input( 114 | z.object({ 115 | id: z.number() 116 | }) 117 | ) 118 | .query(async ({ctx, input}) => { 119 | let items = await ctx.prisma.${m}.findUnique({ 120 | where: { id: input.id } 121 | }); 122 | return items; 123 | }) 124 | }) 125 | `, 126 | }); 127 | }); 128 | let format = (i: string) => 129 | prettier.format(i, { parser: "typescript", ...pkg.prettier }); 130 | try { 131 | const outputPath = path.join(process.cwd(), "/server/generated"); 132 | let exists = fs.existsSync(outputPath); 133 | if (!exists) fs.mkdirSync(outputPath); 134 | 135 | for (let i of arr) { 136 | fs.writeFileSync(path.join(outputPath, i.fileName), format(i.output)); 137 | } 138 | let indexs = ` 139 | import { t } from "@/server/trpc"; 140 | ${arr 141 | .map( 142 | (i) => `import { ${i.name}Router } from "./${i.name}Router";`, 143 | ) 144 | .join("")} 145 | 146 | export const appRouter = t.router({ 147 | ${arr 148 | .map( 149 | (i) => ` 150 | ${i.name}: ${i.name}Router 151 | `, 152 | ) 153 | .join(",")} 154 | }); 155 | 156 | export type AppRouter = typeof appRouter; 157 | `; 158 | fs.writeFileSync(path.join(outputPath, "/_app.ts"), format(indexs)); 159 | } catch (e) { 160 | throw new Error(e.message); 161 | } 162 | -------------------------------------------------------------------------------- /components/Layouts/AdminPage/NavbarMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import DialogConfirm from "@/components/Dialog/DialogConfirm"; 2 | import NestedListMenu, { 3 | NestedListMenuItemProps 4 | } from "@/components/Menu/NestedListMenu"; 5 | import Skeleton from "@/components/Skeleton/Skeleton"; 6 | import { useHeadlessuiDialog } from "@/lib/hooks/useHeadlessuiDialog"; 7 | import { trpc } from "@/lib/trpc"; 8 | import { Card, Link, List, ListItem } from "konsta/react"; 9 | import NextLink from "next/link"; 10 | import Image from "next/image"; 11 | import { FaCheck, FaMoneyBill } from "react-icons/fa"; 12 | import { 13 | MdAdd, 14 | MdCategory, 15 | MdDashboard, 16 | MdEdit, 17 | MdInventory, 18 | MdList, 19 | MdLogout, 20 | MdPeople 21 | } from "react-icons/md"; 22 | import { signOut } from "next-auth/react"; 23 | 24 | const menuItemsData: NestedListMenuItemProps[] = [ 25 | { 26 | title: "Dashboard", 27 | media: , 28 | menuListItemActive: true 29 | }, 30 | { 31 | title: "Users", 32 | colors: { 33 | primaryTextIos: "text-blue-500" 34 | }, 35 | media: , 36 | defaultOpen: true, 37 | subMenu: [ 38 | { 39 | title: "Manage User", 40 | href: "/admin/users", 41 | media: 42 | }, 43 | { 44 | title: "Add User", 45 | href: "/admin/users/add", 46 | media: 47 | } 48 | ] 49 | }, 50 | { 51 | title: "Product", 52 | colors: { 53 | primaryTextIos: "text-blue-500" 54 | }, 55 | media: , 56 | defaultOpen: false, 57 | subMenu: [ 58 | { 59 | title: "Manage Product", 60 | media: 61 | }, 62 | { 63 | title: "Add Product", 64 | media: 65 | } 66 | ] 67 | }, 68 | { 69 | title: "Categories", 70 | media: , 71 | subMenu: [ 72 | { 73 | title: "Manage Categories", 74 | media: 75 | }, 76 | { 77 | title: "Add Categories", 78 | media: 79 | } 80 | ] 81 | } 82 | ]; 83 | 84 | export default function SidebarAdminMenu() { 85 | const { data: user } = { data: {} as Record }; 86 | const dialog = useHeadlessuiDialog(); 87 | return ( 88 | 89 | {user ? ( 90 | 91 | 101 | } 102 | // @ts-ignore 103 | title={ 104 | 105 | {user.name} 106 | 107 | } 108 | subtitle={ 109 |
110 | 111 | Verified Accounts 112 | 113 | 114 |
115 | } 116 | after={ 117 | 122 | dialog.create( 123 | { 126 | if (ok) signOut(); 127 | dialog.destroy(); 128 | }} 129 | /> 130 | ) 131 | } 132 | > 133 | 134 | 135 | } 136 | /> 137 | } 139 | mediaClassName="text-green-500 -mr-1" 140 | strongTitle 141 | title="Saldo" 142 | titleWrapClassName="text-xl tracking-wide" 143 | /> 144 | RP. 520.105} /> 145 |
146 | ) : ( 147 |
148 | 149 | 150 |
151 | )} 152 | 153 |
154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /prisma/generateCrudRouter.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { Prisma } from "@prisma/client"; 3 | import prisma from "../server/prisma"; 4 | import pkg from "../package.json"; 5 | import fs from "fs"; 6 | import path from "path"; 7 | import _ from "lodash"; 8 | import clsx from "clsx"; 9 | 10 | const modelName = _(Prisma.ModelName).map((i) => _.lowerFirst(i)); 11 | const dataModels = Prisma.dmmf.datamodel.models; 12 | const dataEnums = Prisma.dmmf.datamodel.enums; 13 | 14 | let output = ` 15 | /** 16 | * This is auto generated file 17 | **/ 18 | `; 19 | let arr: { name: string; fileName: string; output: string }[] = []; 20 | 21 | modelName.forEach((m) => { 22 | arr.push({ 23 | name: m, 24 | fileName: `${m}Router.ts`, 25 | output: `import { Prisma, PrismaClient } from "@prisma/client"; 26 | import { TRPCError } from "@trpc/server"; 27 | import { ${_.upperFirst(m)}Model } from "@/lib/zod"; 28 | import { createRouter } from "@/server/createRouter"; 29 | import { z } from "zod"; 30 | export const ${m}Router = createRouter() 31 | .mutation("store", { 32 | input: z.object({ 33 | data: ${_.upperFirst(m)}Model.omit({id: true}) 34 | }).required(), 35 | async resolve({ ctx, input }) { 36 | ctx.auth.mustBeReallyUser(); 37 | let items = await ctx.prisma.${m}.create({ 38 | // @ts-expect-error 39 | data: { 40 | // todo 41 | } 42 | }); 43 | return items; 44 | } 45 | }) 46 | .mutation("update", { 47 | input: z.object({ 48 | id: z.number(), 49 | data: ${_.upperFirst(m)}Model 50 | }), 51 | async resolve({ ctx, input }) { 52 | ctx.auth.mustBeReallyUser(); 53 | let items = await ctx.prisma.${m}.update({ 54 | where: { id: input.id }, 55 | data: { 56 | // todo 57 | } 58 | }); 59 | return items; 60 | } 61 | }) 62 | .mutation("destroy", { 63 | input: z.object({ 64 | id: z.number() 65 | }), 66 | async resolve({ ctx, input }) { 67 | ctx.auth.mustBeReallyUser(); 68 | let items = await ctx.prisma.${m}.delete({ 69 | where: { id: input.id } 70 | }); 71 | return items; 72 | } 73 | }) 74 | .query("all", { 75 | input: z.object({ 76 | search: z.string().nullish(), 77 | limit: z.number(), 78 | cursor: z.number() 79 | }), 80 | async resolve({ ctx, input }) { 81 | let limit = input.limit ?? 10; 82 | let cursor = input.cursor; 83 | let where: Prisma.${_.upperFirst(m)}WhereInput; 84 | if (input.search) { 85 | // where.name = { 86 | // contains: input.search 87 | // }; 88 | // where.OR = { 89 | // name: { 90 | // startsWith: input.search 91 | // } 92 | // }; 93 | } 94 | let items = await ctx.prisma.${m}.findMany({ 95 | take: limit + 1, 96 | cursor: cursor ? { id: cursor } : undefined, 97 | where 98 | }); 99 | let next: typeof cursor | null = null; 100 | if (items.length > limit) { 101 | let nextItem = items.pop(); 102 | next = nextItem!.id; 103 | } 104 | return { items, next }; 105 | } 106 | }) 107 | .query("byId", { 108 | input: z.object({ 109 | id: z.number() 110 | }), 111 | async resolve({ ctx, input }) { 112 | let items = await ctx.prisma.${m}.findUnique({ 113 | where: { id: input.id } 114 | }); 115 | return items; 116 | } 117 | }); 118 | ` 119 | }); 120 | }); 121 | let format = (i: string) => 122 | prettier.format(i, { parser: "typescript", ...pkg.prettier }); 123 | try { 124 | const outputPath = path.join(process.cwd(), "/server/generated"); 125 | let exists = fs.existsSync(outputPath); 126 | if (!exists) fs.mkdirSync(outputPath); 127 | 128 | for (let i of arr) { 129 | fs.writeFileSync(path.join(outputPath, i.fileName), format(i.output)); 130 | } 131 | let indexs = ` 132 | import superjson from "superjson"; 133 | import { createRouter } from "@/server/createRouter"; 134 | import { errorFormater } from "@/server/utils"; 135 | ${arr 136 | .map( 137 | (i) => ` 138 | import { ${i.name}Router } from "./${i.name}Router" 139 | ` 140 | ) 141 | .join("")} 142 | 143 | export const appRouter = createRouter() 144 | .formatError(errorFormater) 145 | .transformer(superjson) 146 | // .middleware(async ({ ctx, next }) => { 147 | // return next(); 148 | // }) 149 | ${arr 150 | .map( 151 | (i) => ` 152 | .merge("${i.name}.", ${i.name}Router) 153 | ` 154 | ) 155 | .join("")} 156 | 157 | export type AppRouter = typeof appRouter; 158 | `; 159 | fs.writeFileSync(path.join(outputPath, "/index.ts"), format(indexs)); 160 | } catch (e) { 161 | throw new Error(e.message); 162 | } 163 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const random = function () { 2 | return Math.floor(Math.random() * Date.now()).toString(36); 3 | }; 4 | 5 | export const GUID = function (max: number = 40) { 6 | var str = ""; 7 | for (var i = 0; i < max / 3 + 1; i++) str += random(); 8 | return str.substring(0, max); 9 | }; 10 | 11 | export function wpcomImageLoader({ 12 | src, 13 | width, 14 | quality, 15 | }: { 16 | src: string; 17 | width: string | number; 18 | quality: string | number; 19 | }) { 20 | if (src.startsWith("https://")) { 21 | src = src.split("https://")[1]; 22 | } else if (src.startsWith("http://")) { 23 | src = src.split("http://")[1]; 24 | } 25 | return `https://i1.wp.com/${src}?w=${width}&quality=${quality || 70}`; 26 | } 27 | 28 | export function cleanHtml(str: string) { 29 | return str.replace(/<[^>]*>/gi, ""); 30 | } 31 | 32 | export function isServerless() { 33 | // rome-ignore lint/complexity/useSimplifiedLogicExpression: 34 | return !!(process.env.VERCEL || false) || !!(process.env.SERVERLESS || false); 35 | } 36 | 37 | export function friendlyDate(str: string) { 38 | const date = new Date(Date.parse(str)); 39 | const months = [ 40 | "", 41 | "Jan", 42 | "Feb", 43 | "Mar", 44 | "Apr", 45 | "May", 46 | "Jun", 47 | "Jul", 48 | "Aug", 49 | "Sep", 50 | "Oct", 51 | "Nov", 52 | "Dec", 53 | ]; 54 | const day = date.getDay() === 0 ? 1 : date.getDay(); 55 | return `${day} ${months[date.getMonth()]} ${date.getFullYear()}`; 56 | } 57 | 58 | export const randomizeArrayIndex = (arr: unknown[]) => 59 | arr.filter((i) => i)[Math.floor(Math.random() * arr.length)]; 60 | 61 | export function paginateArray( 62 | collection: [], 63 | page: number = 1, 64 | perPage: number = 10, 65 | ): { 66 | currentPage: number; 67 | perPage: number; 68 | total: number; 69 | totalPages: number; 70 | data: unknown[]; 71 | } { 72 | const currentPage = page; 73 | const offset = (page - 1) * perPage; 74 | const paginatedItems = collection.slice(offset, offset + perPage); 75 | 76 | return { 77 | currentPage, 78 | perPage, 79 | total: collection.length, 80 | totalPages: Math.ceil(collection.length / perPage), 81 | data: paginatedItems, 82 | }; 83 | } 84 | 85 | export function chunkedArray(arr: [], chunkSize: number) { 86 | const res = []; 87 | for (let i = 0; i < arr.length; i += chunkSize) { 88 | const chunk = arr.slice(i, i + chunkSize); 89 | // @ts-expect-error 90 | res.push(chunk); 91 | } 92 | return res; 93 | } 94 | 95 | export const letters = 96 | "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z".split(","); 97 | 98 | export const ucFirst = (str: string) => 99 | str 100 | .split("") 101 | .map((v, i) => (i === 0 ? v.toUpperCase() : v)) 102 | .join(""); 103 | export const lcFirst = (str: string) => 104 | str 105 | .split("") 106 | .map((v, i) => (i === 0 ? v.toLowerCase() : v)) 107 | .join(""); 108 | export const ucWords = (str: string) => 109 | str 110 | .split(" ") 111 | .map((i) => ucFirst(i)) 112 | .join(" "); 113 | 114 | export function timeAgo( 115 | timestamp: Date | number, 116 | options: { 117 | format: "medium" | "long" | "short"; 118 | } = { format: "medium" }, 119 | ) { 120 | const ranges = [ 121 | { min: 1, max: 60, name: { short: "s", medium: "sec", long: "second" } }, 122 | { max: 3600, name: { short: "m", medium: "min", long: "minute" } }, 123 | { max: 86400, name: { short: "h", medium: "hr", long: "hour" } }, 124 | { max: 86400 * 7, name: { short: "d", medium: "day", long: "day" } }, 125 | { max: 86400 * 28, name: { short: "w", medium: "wk", long: "week" } }, 126 | { 127 | min: 86400 * 31, 128 | max: 86400 * 365, 129 | name: { short: "m", medium: "mon", long: "month" }, 130 | }, 131 | { 132 | max: 86400 * 365 * 100, 133 | name: { short: "y", medium: "yr", long: "year" }, 134 | }, 135 | ]; 136 | 137 | let ts_diff: number; 138 | const now_ms = new Date().getTime(); 139 | 140 | if (timestamp instanceof Date) { 141 | ts_diff = (now_ms - timestamp.getTime()) / 1000; 142 | } else { 143 | ts_diff = now_ms / 1000 - timestamp; 144 | } 145 | 146 | const index = ranges.findIndex((item) => item.max > ts_diff); 147 | const range = ranges[index]; 148 | const prevIndex = index - 1; 149 | const min = range.min || ranges[prevIndex].max; 150 | const diff = Math.ceil(ts_diff / min); 151 | 152 | if (diff < 0) 153 | throw new Error( 154 | "The time difference is negative. The provided timestamp is in the future.", 155 | ); 156 | 157 | const plural = diff > 1 && options.format !== "short" ? "s" : ""; 158 | 159 | return `${diff}${options.format === "short" ? "" : " "}${ 160 | range.name[options.format] 161 | }${plural} ago`; 162 | } 163 | 164 | export const noop = () => {}; 165 | 166 | export function site_url(path: string) { 167 | if (process.title !== "node") { 168 | return path; 169 | } 170 | // reference for vercel.com 171 | if (process.env.VERCEL_URL) { 172 | return `https://${process.env.VERCEL_URL}${path}`; 173 | } 174 | // assume localhost 175 | return `http://localhost:${process.env.PORT ?? 3000}${path}`; 176 | } 177 | 178 | export const fakeArray = (size = 1) => 179 | Array(size) 180 | .fill(null) 181 | .map((_, k) => k); 182 | -------------------------------------------------------------------------------- /lib/hooks/useToast.tsx: -------------------------------------------------------------------------------- 1 | import { Transition } from "@headlessui/react"; 2 | import clsx from "clsx"; 3 | import { Link } from "konsta/react"; 4 | import { 5 | createContext, 6 | Dispatch, 7 | ReactElement, 8 | SetStateAction, 9 | useContext, 10 | useEffect, 11 | useState 12 | } from "react"; 13 | 14 | export const enum ToastPosition { 15 | TOP_LEFT = 0, 16 | TOP_CENTER = 1, 17 | TOP_RIGHT = 2, 18 | BOTTOM_LEFT = 3, 19 | BOTTOM_CENTER = 4, 20 | BOTTOM_RIGHT = 5, 21 | CENTER = 6 22 | } 23 | 24 | export const enum ToastType { 25 | DEFAULT = 0, 26 | ERROR = 1, 27 | WARNING = 2, 28 | SUCCESS = 3 29 | } 30 | 31 | type ToastMessageType = { 32 | type?: ToastType; 33 | timeOut?: number; 34 | title: string; 35 | message?: ReactElement | string; 36 | }; 37 | 38 | const Context = createContext<{ 39 | messages: ToastMessageType[]; 40 | setMessages: Dispatch>; 41 | position: ToastPosition; 42 | setPosition: Dispatch>; 43 | } | null>(null); 44 | 45 | export const useToast = () => { 46 | const ctx = useContext(Context); 47 | if (!ctx) throw new Error("Not Initialized"); 48 | const { messages, setMessages, setPosition } = ctx; 49 | const message = (message: ToastMessageType, position?: ToastPosition) => { 50 | setPosition(position || ToastPosition.BOTTOM_RIGHT); 51 | setMessages([ 52 | ...messages, 53 | { 54 | type: message.type || ToastType.DEFAULT, 55 | timeOut: Date.now() + (message.timeOut || 5000), 56 | title: message.title, 57 | message: message.message 58 | } 59 | ]); 60 | }; 61 | return { message }; 62 | }; 63 | 64 | const ToastMessage = ({ 65 | message, 66 | title, 67 | type, 68 | timeOut = 0 69 | }: ToastMessageType) => { 70 | const [show, setShow] = useState(false); 71 | const ctx = useContext(Context); 72 | if (!ctx) throw new Error("Not Initialized"); 73 | const { messages, setMessages, position } = ctx; 74 | const destroy = () => 75 | setMessages(messages.filter((i) => i.timeOut !== timeOut)); 76 | useEffect(() => { 77 | if (timeOut > Date.now()) setShow(true); 78 | let t = setInterval(() => { 79 | if (timeOut < Date.now()) { 80 | setShow(false); 81 | clearInterval(t); 82 | destroy(); 83 | } 84 | }, 1000); 85 | return () => t && clearInterval(t); 86 | }, [messages]); 87 | 88 | return ( 89 | 107 |
108 |
109 |
{title}
110 | {message || null} 111 |
112 |
113 | destroy()} 119 | > 120 | Close 121 | 122 |
123 |
124 |
125 | ); 126 | }; 127 | 128 | export const ToastContextProvider = (props: { children: ReactElement }) => { 129 | const [messages, setMessages] = useState([]); 130 | const [position, setPosition] = useState( 131 | ToastPosition.BOTTOM_CENTER 132 | ); 133 | const showMessage = () => { 134 | return ( 135 |
148 | {messages.map((toast, index) => ( 149 | 150 | ))} 151 |
152 | ); 153 | }; 154 | 155 | return ( 156 | i.timeOut || 0 > Date.now()), 159 | setMessages: setMessages, 160 | position, 161 | setPosition 162 | }} 163 | > 164 | {props.children} 165 | {messages.length > 0 && showMessage()} 166 | 167 | ); 168 | }; 169 | --------------------------------------------------------------------------------