;
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 |
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 |
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 |
25 |
26 | );
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
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 | [](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 | {/*

*/}
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------