├── .eslintrc.json
├── .gitignore
├── README.md
├── components
├── bs.tsx
├── button.tsx
├── floating-button.tsx
├── input.tsx
├── item.tsx
├── layout.tsx
├── message.tsx
├── product-list.tsx
└── textarea.tsx
├── libs
├── client
│ ├── useCoords.ts
│ ├── useMutation.ts
│ ├── useUser.ts
│ └── utils.ts
└── server
│ ├── client.ts
│ ├── withHandler.ts
│ └── withSession.ts
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── _middleware.ts
├── api
│ ├── files.ts
│ ├── posts
│ │ ├── [id]
│ │ │ ├── answers.ts
│ │ │ ├── index.ts
│ │ │ └── wonder.ts
│ │ └── index.ts
│ ├── products
│ │ ├── [id]
│ │ │ ├── fav.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── reviews.ts
│ ├── streams
│ │ ├── [id]
│ │ │ ├── index.ts
│ │ │ └── messages.ts
│ │ └── index.ts
│ └── users
│ │ ├── confirm.ts
│ │ ├── enter.ts
│ │ └── me
│ │ ├── favs.ts
│ │ ├── index.ts
│ │ ├── purchases.ts
│ │ └── sales.ts
├── blog
│ ├── [slug].tsx
│ └── index.tsx
├── chats
│ ├── [id].tsx
│ └── index.tsx
├── community
│ ├── [id].tsx
│ ├── index.tsx
│ └── write.tsx
├── enter.tsx
├── index.tsx
├── products
│ ├── [id].tsx
│ └── upload.tsx
├── profile
│ ├── bought.tsx
│ ├── edit.tsx
│ ├── index.tsx
│ ├── loved.tsx
│ └── sold.tsx
└── streams
│ ├── [id].tsx
│ ├── create.tsx
│ └── index.tsx
├── postcss.config.js
├── posts
├── 01-first-post.md
├── 02-my-trip-to-egypt.md
├── 03-back-home.md
└── 04-wow.md
├── prisma
├── schema.prisma
└── seed.ts
├── public
├── favicon.ico
├── local.jpeg
└── vercel.svg
├── styles
└── globals.css
├── tailwind.config.js
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 | .env
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Carrot Market
2 |
3 | Serverless Carrot Market Clone using NextJS, Tailwind, Prisma, PlanetScale and Cloudflare.
4 |
--------------------------------------------------------------------------------
/components/bs.tsx:
--------------------------------------------------------------------------------
1 | console.log("hello im bs");
2 | export default function Bs() {
3 | return
hello
;
4 | }
5 |
--------------------------------------------------------------------------------
/components/button.tsx:
--------------------------------------------------------------------------------
1 | import { cls } from "@libs/client/utils";
2 |
3 | interface ButtonProps {
4 | large?: boolean;
5 | text: string;
6 | [key: string]: any;
7 | }
8 |
9 | export default function Button({
10 | large = false,
11 | onClick,
12 | text,
13 | ...rest
14 | }: ButtonProps) {
15 | return (
16 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/floating-button.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 |
4 | interface FloatingButton {
5 | children: React.ReactNode;
6 | href: string;
7 | }
8 |
9 | export default function FloatingButton({ children, href }: FloatingButton) {
10 | return (
11 |
12 |
13 | {children}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/components/input.tsx:
--------------------------------------------------------------------------------
1 | import type { UseFormRegisterReturn } from "react-hook-form";
2 |
3 | interface InputProps {
4 | label: string;
5 | name: string;
6 | kind?: "text" | "phone" | "price";
7 | type: string;
8 | register: UseFormRegisterReturn;
9 | required: boolean;
10 | }
11 |
12 | export default function Input({
13 | label,
14 | name,
15 | kind = "text",
16 | register,
17 | type,
18 | required,
19 | }: InputProps) {
20 | return (
21 |
22 |
28 | {kind === "text" ? (
29 |
30 |
37 |
38 | ) : null}
39 | {kind === "price" ? (
40 |
41 |
42 | $
43 |
44 |
51 |
52 | KRW
53 |
54 |
55 | ) : null}
56 | {kind === "phone" ? (
57 |
58 |
59 | +82
60 |
61 |
68 |
69 | ) : null}
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/components/item.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | interface ItemProps {
5 | title: string;
6 | id: number;
7 | price: number;
8 | image: string;
9 | hearts: number;
10 | }
11 |
12 | export default function Item({ title, price, hearts, id, image }: ItemProps) {
13 | return (
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
{title}
26 | ${price}
27 |
28 |
29 |
30 |
31 |
45 |
{hearts}
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/components/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { cls } from "@libs/client/utils";
4 | import { useRouter } from "next/router";
5 | import Head from "next/head";
6 |
7 | interface LayoutProps {
8 | title?: string;
9 | canGoBack?: boolean;
10 | hasTabBar?: boolean;
11 | children: React.ReactNode;
12 | seoTitle?:string;
13 | }
14 |
15 | export default function Layout({
16 | title,
17 | canGoBack,
18 | hasTabBar,
19 | children,
20 | seoTitle
21 | }: LayoutProps) {
22 | const router = useRouter();
23 | const onClick = () => {
24 | router.back();
25 | };
26 | return (
27 |
190 | );
191 | }
192 |
--------------------------------------------------------------------------------
/components/message.tsx:
--------------------------------------------------------------------------------
1 | import { cls } from "@libs/client/utils";
2 |
3 | interface MessageProps {
4 | message: string;
5 | reversed?: boolean;
6 | avatarUrl?: string;
7 | }
8 |
9 | export default function Message({
10 | message,
11 | avatarUrl,
12 | reversed,
13 | }: MessageProps) {
14 | return (
15 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/components/product-list.tsx:
--------------------------------------------------------------------------------
1 | import { ProductWithCount } from "pages";
2 | import useSWR from "swr";
3 | import Item from "./item";
4 |
5 | interface ProductListProps {
6 | kind: "favs" | "sales" | "purchases";
7 | }
8 |
9 | interface Record {
10 | id: number;
11 | product: ProductWithCount;
12 | }
13 |
14 | interface ProductListResponse {
15 | [key: string]: Record[];
16 | }
17 |
18 | export default function ProductList({ kind }: ProductListProps) {
19 | const { data } = useSWR(`/api/users/me/${kind}`);
20 | return data ? (
21 | <>
22 | {data[kind]?.map((record) => (
23 |
31 | ))}
32 | >
33 | ) : null;
34 | }
35 |
--------------------------------------------------------------------------------
/components/textarea.tsx:
--------------------------------------------------------------------------------
1 | import { UseFormRegisterReturn } from "react-hook-form";
2 |
3 | interface TextAreaProps {
4 | label?: string;
5 | name?: string;
6 | register: UseFormRegisterReturn;
7 | [key: string]: any;
8 | }
9 |
10 | export default function TextArea({
11 | label,
12 | name,
13 | register,
14 | ...rest
15 | }: TextAreaProps) {
16 | return (
17 |
18 | {label ? (
19 |
25 | ) : null}
26 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/libs/client/useCoords.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | interface UseCoordState {
4 | longitude: number | null;
5 | latitude: number | null;
6 | }
7 |
8 | export default function useCoords() {
9 | const [coords, setCoords] = useState({
10 | latitude: null,
11 | longitude: null,
12 | });
13 | const onSuccess = ({
14 | coords: { latitude, longitude },
15 | }: GeolocationPosition) => {
16 | setCoords({ latitude, longitude });
17 | };
18 | useEffect(() => {
19 | navigator.geolocation.getCurrentPosition(onSuccess);
20 | }, []);
21 | return coords;
22 | }
23 |
--------------------------------------------------------------------------------
/libs/client/useMutation.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | interface UseMutationState {
4 | loading: boolean;
5 | data?: T;
6 | error?: object;
7 | }
8 | type UseMutationResult = [(data: any) => void, UseMutationState];
9 |
10 | export default function useMutation(
11 | url: string
12 | ): UseMutationResult {
13 | const [state, setSate] = useState>({
14 | loading: false,
15 | data: undefined,
16 | error: undefined,
17 | });
18 | function mutation(data: any) {
19 | setSate((prev) => ({ ...prev, loading: true }));
20 | fetch(url, {
21 | method: "POST",
22 | headers: {
23 | "Content-Type": "application/json",
24 | },
25 | body: JSON.stringify(data),
26 | })
27 | .then((response) => response.json().catch(() => {}))
28 | .then((data) => setSate((prev) => ({ ...prev, data, loading: false })))
29 | .catch((error) =>
30 | setSate((prev) => ({ ...prev, error, loading: false }))
31 | );
32 | }
33 | return [mutation, { ...state }];
34 | }
35 |
--------------------------------------------------------------------------------
/libs/client/useUser.ts:
--------------------------------------------------------------------------------
1 | import { User } from "@prisma/client";
2 | import { useRouter } from "next/router";
3 | import { useEffect } from "react";
4 | import useSWR from "swr";
5 |
6 | interface ProfileResponse {
7 | ok: boolean;
8 | profile: User;
9 | }
10 |
11 | export default function useUser() {
12 | const { data, error } = useSWR(
13 | typeof window === "undefined" ? null : "/api/users/me"
14 | );
15 | const router = useRouter();
16 | useEffect(() => {
17 | if (data && !data.ok) {
18 | router.replace("/enter");
19 | }
20 | }, [data, router]);
21 | return { user: data?.profile, isLoading: !data && !error };
22 | }
23 |
--------------------------------------------------------------------------------
/libs/client/utils.ts:
--------------------------------------------------------------------------------
1 | export function cls(...classnames: string[]) {
2 | return classnames.join(" ");
3 | }
4 |
--------------------------------------------------------------------------------
/libs/server/client.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | declare global {
4 | var client: PrismaClient | undefined;
5 | }
6 |
7 | const client = global.client || new PrismaClient();
8 |
9 | if (process.env.NODE_ENV === "development") global.client = client;
10 |
11 | export default client;
12 |
--------------------------------------------------------------------------------
/libs/server/withHandler.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export interface ResponseType {
4 | ok: boolean;
5 | [key: string]: any;
6 | }
7 |
8 | type method = "GET" | "POST" | "DELETE";
9 |
10 | interface ConfigType {
11 | methods: method[];
12 | handler: (req: NextApiRequest, res: NextApiResponse) => void;
13 | isPrivate?: boolean;
14 | }
15 |
16 | export default function withHandler({
17 | methods,
18 | isPrivate = true,
19 | handler,
20 | }: ConfigType) {
21 | return async function (
22 | req: NextApiRequest,
23 | res: NextApiResponse
24 | ): Promise {
25 | if (req.method && !methods.includes(req.method as any)) {
26 | return res.status(405).end();
27 | }
28 | if (isPrivate && !req.session.user) {
29 | return res.status(401).json({ ok: false, error: "Plz log in." });
30 | }
31 | try {
32 | await handler(req, res);
33 | } catch (error) {
34 | console.log(error);
35 | return res.status(500).json({ error });
36 | }
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/libs/server/withSession.ts:
--------------------------------------------------------------------------------
1 | import { IronSessionOptions } from "iron-session";
2 | import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next";
3 |
4 | declare module "iron-session" {
5 | interface IronSessionData {
6 | user?: {
7 | id: number;
8 | };
9 | }
10 | }
11 |
12 | const cookieConfig: IronSessionOptions = {
13 | cookieName: "carrotsession",
14 | password: process.env.COOKIE_PASSWORD!,
15 | cookieOptions: {
16 | secure: process.env.NODE_ENV === "production",
17 | },
18 | };
19 |
20 | export function withApiSession(fn: any) {
21 | return withIronSessionApiRoute(fn, cookieConfig);
22 | }
23 |
24 | export function withSsrSession(handler: any) {
25 | return withIronSessionSsr(handler, cookieConfig);
26 | }
27 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | reactStrictMode: true,
4 | experimental: {
5 | reactRoot: true,
6 | /* runtime: "nodejs",
7 | serverComponents: true, */
8 | },
9 | images: {
10 | domains: ["imagedelivery.net", "videodelivery.net"],
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "carrot-market",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev",
6 | "build": "next build",
7 | "start": "next start",
8 | "lint": "next lint"
9 | },
10 | "dependencies": {
11 | "@prisma/client": "^3.8.1",
12 | "@sendgrid/mail": "^7.6.0",
13 | "fetch-suspense": "^1.2.2",
14 | "gray-matter": "^4.0.3",
15 | "iron-session": "^6.0.5",
16 | "next": "^12.1.0",
17 | "react": "^18.0.0-rc.0",
18 | "react-dom": "^18.0.0-rc.0",
19 | "react-fetch": "^0.0.9",
20 | "react-hook-form": "^7.25.0",
21 | "remark-html": "^15.0.1",
22 | "remark-parse": "^10.0.1",
23 | "swr": "^1.2.1",
24 | "to-vfile": "^7.2.3",
25 | "ts-node": "^10.4.0",
26 | "twilio": "^3.73.0",
27 | "unified": "^10.1.1"
28 | },
29 | "devDependencies": {
30 | "@tailwindcss/forms": "^0.4.0",
31 | "@types/node": "17.0.8",
32 | "@types/react": "17.0.38",
33 | "autoprefixer": "^10.4.1",
34 | "eslint": "8.6.0",
35 | "eslint-config-next": "12.0.7",
36 | "postcss": "^8.4.5",
37 | "prisma": "^3.9.1",
38 | "tailwindcss": "^3.0.11",
39 | "typescript": "4.5.4"
40 | },
41 | "prisma": {
42 | "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import type { AppProps } from "next/app";
3 | import { SWRConfig } from "swr";
4 | import Script from "next/script";
5 |
6 | function MyApp({ Component, pageProps }: AppProps) {
7 | console.log("APP IS RUNNING");
8 | return (
9 |
12 | fetch(url).then((response) => response.json()),
13 | }}
14 | >
15 |
16 |
17 |
18 | {/*
22 |