├── .eslintrc.json ├── app ├── favicon.ico ├── globals.css ├── utils │ └── supabaseClient.ts ├── api │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── purchases │ │ └── [userId] │ │ │ └── route.ts │ ├── checkout │ │ ├── route.ts │ │ └── success │ │ │ └── route.ts │ └── thumbnail │ │ └── upload │ │ └── route.ts ├── lib │ ├── next-auth │ │ ├── provider.tsx │ │ ├── actions.ts │ │ └── options.ts │ ├── prisma.ts │ └── microcms │ │ └── client.ts ├── hooks │ └── useHighlightCode.ts ├── types │ └── types.ts ├── loading.tsx ├── components │ ├── CreateBookButton.tsx │ ├── PurchaseProduct.tsx │ ├── Header.tsx │ └── Book.tsx ├── layout.tsx ├── book │ ├── [id] │ │ └── page.tsx │ └── checkout-success │ │ └── page.tsx ├── profile │ └── page.tsx ├── login │ └── page.tsx ├── createBook │ └── page.tsx └── page.tsx ├── public ├── default_icon.png ├── thumbnails │ ├── notion-udemy.png │ ├── discord-clone-udemy.png │ └── openai-chatapplication-udem.png ├── vercel.svg └── next.svg ├── postcss.config.js ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20231128125545_init │ │ └── migration.sql └── schema.prisma ├── tailwind.config.ts ├── .gitignore ├── next.config.js ├── tsconfig.json ├── copy ├── loading.tsx ├── Profile.tsx ├── PurchaseProduct.tsx ├── checkout-success.tsx ├── DetailPage.tsx ├── Header.tsx ├── createBook.tsx ├── login.tsx ├── Book.tsx └── homepage.tsx ├── package.json └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shin-sibainu/shincode-book-commerce/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/default_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shin-sibainu/shincode-book-commerce/HEAD/public/default_icon.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background-color: #f3f3f3; 7 | } 8 | -------------------------------------------------------------------------------- /public/thumbnails/notion-udemy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shin-sibainu/shincode-book-commerce/HEAD/public/thumbnails/notion-udemy.png -------------------------------------------------------------------------------- /public/thumbnails/discord-clone-udemy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shin-sibainu/shincode-book-commerce/HEAD/public/thumbnails/discord-clone-udemy.png -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /public/thumbnails/openai-chatapplication-udem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shin-sibainu/shincode-book-commerce/HEAD/public/thumbnails/openai-chatapplication-udem.png -------------------------------------------------------------------------------- /app/utils/supabaseClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | 3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; 4 | const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; 5 | 6 | export const supabase = createClient(supabaseUrl, supabaseAnonKey); 7 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { nextAuthOptions } from "@/app/lib/next-auth/options"; 2 | import NextAuth from "next-auth"; 3 | const handler = NextAuth(nextAuthOptions); 4 | 5 | // https://next-auth.js.org/configuration/initialization#route-handlers-app 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /app/lib/next-auth/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SessionProvider } from "next-auth/react"; 4 | 5 | import type { FC, PropsWithChildren } from "react"; 6 | 7 | export const NextAuthProvider: FC = ({ children }) => { 8 | return {children}; 9 | }; 10 | -------------------------------------------------------------------------------- /app/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | let prisma: PrismaClient; 4 | 5 | const globalForPrisma = global as unknown as { 6 | prisma: PrismaClient | undefined; 7 | }; 8 | 9 | if (!globalForPrisma.prisma) { 10 | globalForPrisma.prisma = new PrismaClient(); 11 | } 12 | prisma = globalForPrisma.prisma; 13 | 14 | export default prisma; 15 | -------------------------------------------------------------------------------- /app/hooks/useHighlightCode.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import hljs from "highlight.js"; 5 | 6 | const useHighlightCode = () => { 7 | useEffect(() => { 8 | document.querySelectorAll("pre code").forEach((block: any) => { 9 | hljs.highlightBlock(block); 10 | }); 11 | }, []); 12 | }; 13 | 14 | export default useHighlightCode; 15 | -------------------------------------------------------------------------------- /app/types/types.ts: -------------------------------------------------------------------------------- 1 | type BookType = { 2 | id: number; 3 | title: string; 4 | price: number; 5 | content: string; 6 | thumbnail: { url: string }; 7 | created_at: string; 8 | updated_at: string; 9 | }; 10 | 11 | type Purchase = { 12 | id: string; 13 | userId: string; 14 | bookId: string; 15 | sessionId: string; 16 | createdAt: string; 17 | }; 18 | 19 | type User = { 20 | id: string; 21 | name: string; 22 | email: string; 23 | image: string; 24 | }; 25 | 26 | export type { BookType, Purchase, User }; 27 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | .vercel 40 | .env*.local 41 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/purchases/[userId]/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/lib/prisma"; 2 | import { NextResponse } from "next/server"; 3 | 4 | //https://nextjs.org/docs/app/building-your-application/routing/route-handlers#dynamic-route-segments 5 | export async function GET( 6 | request: Request, 7 | { params }: { params: { userId: string } } 8 | ) { 9 | const userId = params.userId; 10 | 11 | try { 12 | const purchase = await prisma.purchase.findMany({ 13 | where: { userId: userId }, 14 | }); 15 | console.log(purchase); 16 | 17 | return NextResponse.json(purchase); 18 | } catch (err) { 19 | return NextResponse.json(err); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "avatars.githubusercontent.com", 8 | }, 9 | { 10 | protocol: "https", 11 | hostname: "book-thumbnail-bucket.s3.ap-northeast-1.amazonaws.com", 12 | }, 13 | { 14 | protocol: "https", 15 | hostname: "images.microcms-assets.io", 16 | }, 17 | { 18 | protocol: "https", 19 | hostname: "images.microcms-assets.io", 20 | }, 21 | ], 22 | }, 23 | reactStrictMode: false, 24 | }; 25 | 26 | module.exports = nextConfig; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { ClipLoader } from "react-spinners"; 5 | 6 | const LoadingSpinner = () => { 7 | // スピナーのサイズや色をカスタマイズできます 8 | const size = 50; 9 | const color = "#123abc"; 10 | 11 | return ( 12 |
13 | 14 | 15 | {/* スタイル */} 16 | 24 |
25 | ); 26 | }; 27 | 28 | export default LoadingSpinner; 29 | -------------------------------------------------------------------------------- /copy/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { ClipLoader } from "react-spinners"; 5 | 6 | const LoadingSpinner = () => { 7 | // スピナーのサイズや色をカスタマイズできます 8 | const size = 50; 9 | const color = "#123abc"; 10 | 11 | return ( 12 |
13 | 14 | 15 | {/* スタイル */} 16 | 24 |
25 | ); 26 | }; 27 | 28 | export default LoadingSpinner; 29 | -------------------------------------------------------------------------------- /app/lib/microcms/client.ts: -------------------------------------------------------------------------------- 1 | import { BookType } from "@/app/types/types"; 2 | import { MicroCMSQueries, createClient } from "microcms-js-sdk"; 3 | 4 | export const client = createClient({ 5 | serviceDomain: process.env.NEXT_PUBLIC_SERVICE_DOMAIN!, 6 | apiKey: process.env.NEXT_PUBLIC_API_KEY!, 7 | }); 8 | 9 | export const getAllBooks = async () => { 10 | const allBooks = await client.get({ 11 | endpoint: "bookcommerce", 12 | queries: { 13 | offset: 0, 14 | limit: 10, 15 | }, 16 | }); 17 | 18 | return allBooks; 19 | }; 20 | 21 | export const getDetailBook = async (contentId: string) => { 22 | const detailBook = await client.getListDetail({ 23 | endpoint: "bookcommerce", 24 | contentId, 25 | }); 26 | 27 | return detailBook; 28 | }; 29 | -------------------------------------------------------------------------------- /copy/Profile.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export default async function ProfilePage() { 4 | return ( 5 |
6 |

プロフィール

7 | 8 |
9 |
10 | user profile_icon 18 |

お名前:

19 |
20 |
21 | 22 | 購入した記事 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /copy/PurchaseProduct.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | 5 | const PurchaseProduct = () => { 6 | return ( 7 | 11 | {""} 19 |
20 |

21 | {/*

この本は○○...

*/} 22 |

値段:円

23 |
24 | 25 | ); 26 | }; 27 | 28 | export default PurchaseProduct; 29 | -------------------------------------------------------------------------------- /copy/checkout-success.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | 4 | const PurchaseSuccess = () => { 5 | return ( 6 |
7 |
8 |

9 | 購入ありがとうございます! 10 |

11 |

12 | ご購入いただいた内容の詳細は、登録されたメールアドレスに送信されます。 13 |

14 |
15 | 19 | 購入した記事を読む 20 | 21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default PurchaseSuccess; 28 | -------------------------------------------------------------------------------- /app/lib/next-auth/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { signIn } from "@/auth"; 4 | 5 | export async function authenticateWithGitHub() { 6 | try { 7 | const result: any = await signIn("github", { 8 | callbackUrl: "/your-callback-url", 9 | }); 10 | if (result?.url) { 11 | window.location.href = result.url; 12 | return false; // リダイレクトを待つ 13 | } 14 | return true; 15 | } catch (error) { 16 | console.error("GitHub認証エラー", error); 17 | return false; 18 | } 19 | } 20 | 21 | // export async function authenticate(prevState: boolean, formData: FormData) { 22 | // try { 23 | // await signIn("credentials", Object.fromEntries(formData)); 24 | // return true; 25 | // } catch (error) { 26 | // if ((error as Error).message.includes("CredentialsSignin")) { 27 | // return false; 28 | // } 29 | // throw error; 30 | // } 31 | // } 32 | -------------------------------------------------------------------------------- /app/components/CreateBookButton.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | const CreateBookButton = () => { 5 | return ( 6 |
7 | 11 | 19 | 24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default CreateBookButton; 31 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Noto_Sans_JP } from "next/font/google"; 3 | import "./globals.css"; 4 | import Header from "./components/Header"; 5 | import { NextAuthProvider } from "./lib/next-auth/provider"; 6 | import { Suspense } from "react"; 7 | import Loading from "./loading"; 8 | 9 | const notoSansJP = Noto_Sans_JP({ weight: "400", subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Book Commerce", 13 | description: "Generated by create next app", 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 23 | 24 | 25 |
26 | }> 27 | {children} 28 | {/* */} 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /copy/DetailPage.tsx: -------------------------------------------------------------------------------- 1 | // import Image from "next/image"; 2 | // import React from "react"; 3 | 4 | // const DetailBook = async () => { 5 | // return ( 6 | //
7 | //
8 | // 13 | //
14 | //

Test Book

15 | //
Test" }} 18 | // /> 19 | 20 | //
21 | // 公開日: 22 | // 最終更新: 23 | //
24 | //
25 | //
26 | //
27 | // ); 28 | // }; 29 | 30 | // export default DetailBook; 31 | -------------------------------------------------------------------------------- /app/components/PurchaseProduct.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Link from "next/link"; 3 | import { BookType } from "../types/types"; 4 | import Image from "next/image"; 5 | 6 | type detailBookProps = { 7 | detailBook: BookType; 8 | }; 9 | 10 | const PurchaseProduct = ({ detailBook }: detailBookProps) => { 11 | return ( 12 | 16 | {detailBook.title} 24 |
25 |

{detailBook.title}

26 | {/*

この本は○○...

*/} 27 |

28 | 値段:{detailBook.price}円 29 |

30 |
31 | 32 | ); 33 | }; 34 | 35 | export default PurchaseProduct; 36 | -------------------------------------------------------------------------------- /copy/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | 6 | const Header = () => { 7 | return ( 8 |
9 | 37 |
38 | ); 39 | }; 40 | 41 | export default Header; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "prisma generate && next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-s3": "^3.454.0", 13 | "@next-auth/prisma-adapter": "^1.0.7", 14 | "@prisma/client": "^5.5.2", 15 | "@supabase/supabase-js": "^2.38.4", 16 | "@vercel/blob": "^0.15.1", 17 | "@vercel/postgres": "^0.5.1", 18 | "aws-sdk": "^2.1498.0", 19 | "form-data": "^4.0.0", 20 | "formidable": "^3.5.1", 21 | "highlight.js": "^11.9.0", 22 | "microcms-js-sdk": "^2.7.0", 23 | "next": "14.0.1", 24 | "next-auth": "^4.24.5", 25 | "react": "^18", 26 | "react-dom": "^18", 27 | "react-hook-form": "^7.48.2", 28 | "react-spinners": "^0.13.8", 29 | "stripe": "^14.5.0" 30 | }, 31 | "devDependencies": { 32 | "@types/formidable": "^3.4.5", 33 | "@types/node": "^20", 34 | "@types/react": "^18", 35 | "@types/react-dom": "^18", 36 | "autoprefixer": "^10.0.1", 37 | "eslint": "^8", 38 | "eslint-config-next": "14.0.1", 39 | "postcss": "^8", 40 | "prisma": "^5.5.2", 41 | "tailwindcss": "^3.3.0", 42 | "typescript": "^5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/api/checkout/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { NextResponse } from "next/server"; 3 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); 4 | 5 | const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; 6 | 7 | export async function POST(request: Request, response: Response) { 8 | const { title, price, bookId, userId } = await request.json(); 9 | 10 | try { 11 | // チェックアウトセッションの作成 12 | const session = await stripe.checkout.sessions.create({ 13 | payment_method_types: ["card"], 14 | metadata: { 15 | bookId: bookId, 16 | }, 17 | client_reference_id: userId, 18 | line_items: [ 19 | { 20 | price_data: { 21 | currency: "jpy", 22 | product_data: { 23 | name: title, 24 | }, 25 | unit_amount: price, 26 | }, 27 | quantity: 1, 28 | }, 29 | ], 30 | mode: "payment", 31 | success_url: `${baseUrl}/book/checkout-success?session_id={CHECKOUT_SESSION_ID}`, 32 | cancel_url: `${baseUrl}`, 33 | }); 34 | return NextResponse.json({ 35 | checkout_url: session.url, 36 | }); 37 | } catch (err: any) { 38 | return NextResponse.json({ message: err.message }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /copy/createBook.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | const CreateBook = () => { 6 | return ( 7 |
8 |
9 | 10 | 14 |
15 | 16 |
17 | 18 |