├── .eslintrc.json ├── src ├── components │ ├── page.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── table.tsx │ │ └── dropdown-menu.tsx │ ├── Nav.tsx │ └── ProductCard.tsx ├── lib │ ├── utils.ts │ ├── cache.ts │ ├── isValidPassword.ts │ └── formatters.ts ├── app │ ├── admin │ │ ├── _components │ │ │ └── PageHeader.tsx │ │ ├── loading.tsx │ │ ├── products │ │ │ ├── new │ │ │ │ └── page.tsx │ │ │ ├── [id] │ │ │ │ ├── edit │ │ │ │ │ └── page.tsx │ │ │ │ └── download │ │ │ │ │ └── route.ts │ │ │ ├── _components │ │ │ │ ├── ProductActions.tsx │ │ │ │ └── ProductForm.tsx │ │ │ └── page.tsx │ │ ├── _actions │ │ │ ├── users.ts │ │ │ ├── orders.ts │ │ │ └── products.ts │ │ ├── layout.tsx │ │ ├── users │ │ │ ├── _components │ │ │ │ └── UserActions.tsx │ │ │ └── page.tsx │ │ ├── orders │ │ │ ├── _components │ │ │ │ └── OrderActions.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── actions │ │ └── orders.ts │ ├── (customerFacing) │ │ ├── products │ │ │ ├── download │ │ │ │ ├── expired │ │ │ │ │ └── page.tsx │ │ │ │ └── [downloadVerificationId] │ │ │ │ │ └── route.ts │ │ │ ├── [id] │ │ │ │ └── purchase │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── _components │ │ │ │ │ └── CheckoutForm.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── orders │ │ │ └── page.tsx │ │ ├── stripe │ │ │ └── purchase-success │ │ │ │ └── page.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── globals.css │ └── webhooks │ │ └── stripe │ │ └── route.tsx ├── db │ └── db.ts ├── middleware.ts ├── email │ ├── PurchaseReceipt.tsx │ ├── OrderHistory.tsx │ └── components │ │ └── OrderInformation.tsx └── actions │ └── orders.tsx ├── next.config.mjs ├── postcss.config.js ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20240227191221_init │ │ └── migration.sql └── schema.prisma ├── components.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── tailwind.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/components/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return

Hi

3 | } 4 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /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 = "sqlite" -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/app/admin/_components/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | 3 | export function PageHeader({ children }: { children: ReactNode }) { 4 | return

{children}

5 | } 6 | -------------------------------------------------------------------------------- /src/app/admin/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react" 2 | 3 | export default function AdminLoading() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/actions/orders.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import db from "@/db/db" 4 | 5 | export async function userOrderExists(email: string, productId: string) { 6 | return ( 7 | (await db.order.findFirst({ 8 | where: { user: { email }, productId }, 9 | select: { id: true }, 10 | })) != null 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/app/admin/products/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { PageHeader } from "../../_components/PageHeader" 2 | import { ProductForm } from "../_components/ProductForm" 3 | 4 | export default function NewProductPage() { 5 | return ( 6 | <> 7 | Add Product 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/admin/_actions/users.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import db from "@/db/db" 4 | import { notFound } from "next/navigation" 5 | 6 | export async function deleteUser(id: string) { 7 | const user = await db.user.delete({ 8 | where: { id }, 9 | }) 10 | 11 | if (user == null) return notFound() 12 | 13 | return user 14 | } 15 | -------------------------------------------------------------------------------- /src/app/admin/_actions/orders.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import db from "@/db/db" 4 | import { notFound } from "next/navigation" 5 | 6 | export async function deleteOrder(id: string) { 7 | const order = await db.order.delete({ 8 | where: { id }, 9 | }) 10 | 11 | if (order == null) return notFound() 12 | 13 | return order 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/cache.ts: -------------------------------------------------------------------------------- 1 | import { unstable_cache as nextCache } from "next/cache" 2 | import { cache as reactCache } from "react" 3 | 4 | type Callback = (...args: any[]) => Promise 5 | export function cache( 6 | cb: T, 7 | keyParts: string[], 8 | options: { revalidate?: number | false; tags?: string[] } = {} 9 | ) { 10 | return nextCache(reactCache(cb), keyParts, options) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/products/download/expired/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import Link from "next/link" 3 | 4 | export default function Expired() { 5 | return ( 6 | <> 7 |

Download link expired

8 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/db/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client" 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient() 5 | } 6 | 7 | declare global { 8 | var prisma: undefined | ReturnType 9 | } 10 | 11 | const db = globalThis.prisma ?? prismaClientSingleton() 12 | 13 | export default db 14 | 15 | if (process.env.NODE_ENV !== "production") globalThis.prisma = db 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/lib/isValidPassword.ts: -------------------------------------------------------------------------------- 1 | export async function isValidPassword( 2 | password: string, 3 | hashedPassword: string 4 | ) { 5 | return (await hashPassword(password)) === hashedPassword 6 | } 7 | 8 | async function hashPassword(password: string) { 9 | const arrayBuffer = await crypto.subtle.digest( 10 | "SHA-512", 11 | new TextEncoder().encode(password) 12 | ) 13 | 14 | return Buffer.from(arrayBuffer).toString("base64") 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/formatters.ts: -------------------------------------------------------------------------------- 1 | const CURRENCY_FORMATTER = new Intl.NumberFormat("en-US", { 2 | currency: "USD", 3 | style: "currency", 4 | minimumFractionDigits: 0, 5 | }) 6 | 7 | export function formatCurrency(amount: number) { 8 | return CURRENCY_FORMATTER.format(amount) 9 | } 10 | 11 | const NUMBER_FORMATTER = new Intl.NumberFormat("en-US") 12 | 13 | export function formatNumber(number: number) { 14 | return NUMBER_FORMATTER.format(number) 15 | } 16 | -------------------------------------------------------------------------------- /src/app/admin/products/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import db from "@/db/db" 2 | import { PageHeader } from "../../../_components/PageHeader" 3 | import { ProductForm } from "../../_components/ProductForm" 4 | 5 | export default async function EditProductPage({ 6 | params: { id }, 7 | }: { 8 | params: { id: string } 9 | }) { 10 | const product = await db.product.findUnique({ where: { id } }) 11 | 12 | return ( 13 | <> 14 | Edit Product 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Nav, NavLink } from "@/components/Nav" 2 | 3 | export const dynamic = "force-dynamic" 4 | 5 | export default function Layout({ 6 | children, 7 | }: Readonly<{ 8 | children: React.ReactNode 9 | }>) { 10 | return ( 11 | <> 12 | 17 |
{children}
18 | 19 | ) 20 | } 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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | /products 38 | /public/products 39 | .env 40 | /prisma/dev.db -------------------------------------------------------------------------------- /src/app/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Nav, NavLink } from "@/components/Nav" 2 | 3 | export const dynamic = "force-dynamic" 4 | 5 | export default function AdminLayout({ 6 | children, 7 | }: Readonly<{ 8 | children: React.ReactNode 9 | }>) { 10 | return ( 11 | <> 12 | 18 |
{children}
19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/app/admin/users/_components/UserActions.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu" 4 | import { useTransition } from "react" 5 | import { deleteUser } from "../../_actions/users" 6 | import { useRouter } from "next/navigation" 7 | 8 | export function DeleteDropDownItem({ id }: { id: string }) { 9 | const [isPending, startTransition] = useTransition() 10 | const router = useRouter() 11 | 12 | return ( 13 | 17 | startTransition(async () => { 18 | await deleteUser(id) 19 | router.refresh() 20 | }) 21 | } 22 | > 23 | Delete 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Inter } from "next/font/google" 3 | import "./globals.css" 4 | import { cn } from "@/lib/utils" 5 | 6 | const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }) 7 | 8 | export const metadata: Metadata = { 9 | title: "Create Next App", 10 | description: "Generated by create next app", 11 | } 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: Readonly<{ 16 | children: React.ReactNode 17 | }>) { 18 | return ( 19 | 20 | 26 | {children} 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/app/admin/orders/_components/OrderActions.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu" 4 | import { useTransition } from "react" 5 | import { deleteOrder } from "../../_actions/orders" 6 | import { useRouter } from "next/navigation" 7 | 8 | export function DeleteDropDownItem({ id }: { id: string }) { 9 | const [isPending, startTransition] = useTransition() 10 | const router = useRouter() 11 | 12 | return ( 13 | 17 | startTransition(async () => { 18 | await deleteOrder(id) 19 | router.refresh() 20 | }) 21 | } 22 | > 23 | Delete 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/Nav.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { cn } from "@/lib/utils" 4 | import Link from "next/link" 5 | import { usePathname } from "next/navigation" 6 | import { ComponentProps, ReactNode } from "react" 7 | 8 | export function Nav({ children }: { children: ReactNode }) { 9 | return ( 10 | 13 | ) 14 | } 15 | 16 | export function NavLink(props: Omit, "className">) { 17 | const pathname = usePathname() 18 | return ( 19 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |