From 8bb436ea4f3e7b74a58f95d70b1ccfa89b7c8f2b Mon Sep 17 00:00:00 2001 From: orohi Date: Sat, 6 Jun 2026 14:59:02 +0300 Subject: [PATCH] first commit Co-authored-by: Cursor --- .dockerignore | 10 ++ .env.example | 1 + .eslintrc.json | 3 + .gitignore | 41 +++++++ Dockerfile | 35 ++++++ README.md | 69 ++++++++++++ docker-compose.yml | 39 +++++++ next.config.ts | 14 +++ package.json | 33 ++++++ postcss.config.mjs | 7 ++ prisma/schema.prisma | 21 ++++ public/icon.svg | 11 ++ public/uploads/.gitkeep | 1 + src/app/api/photos/route.ts | 15 +++ src/app/api/upload/route.ts | 71 ++++++++++++ src/app/globals.css | 64 +++++++++++ src/app/layout.tsx | 27 +++++ src/app/page.tsx | 43 ++++++++ src/components/Features.tsx | 95 ++++++++++++++++ src/components/Footer.tsx | 27 +++++ src/components/Gallery.tsx | 98 +++++++++++++++++ src/components/Header.tsx | 50 +++++++++ src/components/Hero.tsx | 64 +++++++++++ src/components/UploadZone.tsx | 200 ++++++++++++++++++++++++++++++++++ src/lib/prisma.ts | 15 +++ tsconfig.json | 21 ++++ 26 files changed, 1075 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prisma/schema.prisma create mode 100644 public/icon.svg create mode 100644 public/uploads/.gitkeep create mode 100644 src/app/api/photos/route.ts create mode 100644 src/app/api/upload/route.ts create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/components/Features.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Gallery.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/Hero.tsx create mode 100644 src/components/UploadZone.tsx create mode 100644 src/lib/prisma.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..853abbc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.next +.git +.gitignore +.env +.env*.local +README.md +docker-compose*.yml +Dockerfile +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f86dc22 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgresql://photohost:photohost_secret@localhost:5432/photohost?schema=public" diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3722418 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40e0cdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env +.env*.local + +# uploads +/public/uploads/* +!/public/uploads/.gitkeep + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..645933e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM node:22-alpine AS base +RUN apk add --no-cache libc6-compat openssl +WORKDIR /app + +FROM base AS deps +COPY package.json package-lock.json* ./ +COPY prisma ./prisma/ +RUN npm install + +FROM base AS builder +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npx prisma generate +RUN npm run build + +FROM base AS runner +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +RUN mkdir -p public/uploads && chown nextjs:nodejs public/uploads + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/next.config.ts ./next.config.ts + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["sh", "-c", "npx prisma db push --skip-generate && npm start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..96f5758 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# PhotoHost — Фотохостинг на Next.js + +Современный фотохостинг с красивым интерфейсом, построенный на Next.js 15, PostgreSQL и Docker Compose. + +## Возможности + +- Drag & drop загрузка изображений +- Мгновенное получение прямой ссылки +- Галерея последних загрузок +- Тёмная тема с современным дизайном +- PostgreSQL для хранения метаданных +- Docker Compose для простого деплоя + +## Быстрый старт (Docker) + +```bash +docker compose up --build +``` + +Приложение будет доступно на [http://localhost:3000](http://localhost:3000) + +## Локальная разработка + +1. Скопируйте `.env.example` в `.env`: + +```bash +cp .env.example .env +``` + +2. Запустите PostgreSQL: + +```bash +docker compose up db -d +``` + +3. Установите зависимости и запустите: + +```bash +npm install +npx prisma db push +npm run dev +``` + +## Стек + +- **Frontend:** Next.js 15, React 19, Tailwind CSS 4 +- **Backend:** Next.js API Routes +- **База данных:** PostgreSQL 16 + Prisma ORM +- **Инфраструктура:** Docker Compose + +## API + +| Метод | Путь | Описание | +|-------|------|----------| +| POST | `/api/upload` | Загрузка изображения (multipart/form-data) | +| GET | `/api/photos` | Список последних фото | + +## Структура + +``` +├── docker-compose.yml # PostgreSQL + App +├── Dockerfile # Production сборка +├── prisma/schema.prisma # Схема БД +├── src/ +│ ├── app/ # Next.js App Router +│ ├── components/ # UI компоненты +│ └── lib/ # Prisma клиент +└── public/uploads/ # Загруженные файлы +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3790e40 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + db: + image: postgres:16-alpine + container_name: photohost-db + restart: unless-stopped + environment: + POSTGRES_USER: photohost + POSTGRES_PASSWORD: photohost_secret + POSTGRES_DB: photohost + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U photohost -d photohost"] + interval: 5s + timeout: 5s + retries: 5 + + app: + build: + context: . + dockerfile: Dockerfile + container_name: photohost-app + restart: unless-stopped + ports: + - "3000:3000" + environment: + DATABASE_URL: postgresql://photohost:photohost_secret@db:5432/photohost?schema=public + NODE_ENV: production + volumes: + - uploads_data:/app/public/uploads + depends_on: + db: + condition: service_healthy + +volumes: + postgres_data: + uploads_data: diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..e0c829f --- /dev/null +++ b/next.config.ts @@ -0,0 +1,14 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "**", + }, + ], + }, +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..9ae062a --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "photohost", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev", + "postinstall": "prisma generate" + }, + "dependencies": { + "@prisma/client": "^6.9.0", + "lucide-react": "^0.513.0", + "next": "^15.3.3", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.8", + "@types/node": "^22.15.29", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.5", + "eslint": "^9.28.0", + "eslint-config-next": "^15.3.3", + "prisma": "^6.9.0", + "tailwindcss": "^4.1.8", + "typescript": "^5.8.3" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..869f5b1 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,21 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Photo { + id String @id @default(cuid()) + filename String + originalName String + mimeType String + size Int + url String + views Int @default(0) + createdAt DateTime @default(now()) + + @@index([createdAt(sort: Desc)]) +} diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..55a6667 --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/uploads/.gitkeep b/public/uploads/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/public/uploads/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/app/api/photos/route.ts b/src/app/api/photos/route.ts new file mode 100644 index 0000000..0eb5f19 --- /dev/null +++ b/src/app/api/photos/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + try { + const photos = await prisma.photo.findMany({ + orderBy: { createdAt: "desc" }, + take: 50, + }); + return NextResponse.json(photos); + } catch (error) { + console.error("Photos fetch error:", error); + return NextResponse.json([], { status: 200 }); + } +} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts new file mode 100644 index 0000000..ae01a07 --- /dev/null +++ b/src/app/api/upload/route.ts @@ -0,0 +1,71 @@ +import { NextRequest, NextResponse } from "next/server"; +import { writeFile, mkdir } from "fs/promises"; +import path from "path"; +import { prisma } from "@/lib/prisma"; + +const ALLOWED_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/svg+xml", +]; +const MAX_SIZE = 10 * 1024 * 1024; + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const file = formData.get("file") as File | null; + + if (!file) { + return NextResponse.json( + { error: "Файл не найден" }, + { status: 400 } + ); + } + + if (!ALLOWED_TYPES.includes(file.type)) { + return NextResponse.json( + { error: "Недопустимый тип файла" }, + { status: 400 } + ); + } + + if (file.size > MAX_SIZE) { + return NextResponse.json( + { error: "Файл слишком большой (макс. 10 МБ)" }, + { status: 400 } + ); + } + + const ext = file.name.split(".").pop()?.toLowerCase() || "jpg"; + const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}.${ext}`; + const uploadDir = path.join(process.cwd(), "public", "uploads"); + + await mkdir(uploadDir, { recursive: true }); + + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + await writeFile(path.join(uploadDir, filename), buffer); + + const url = `/uploads/${filename}`; + + const photo = await prisma.photo.create({ + data: { + filename, + originalName: file.name, + mimeType: file.type, + size: file.size, + url, + }, + }); + + return NextResponse.json(photo); + } catch (error) { + console.error("Upload error:", error); + return NextResponse.json( + { error: "Ошибка сервера при загрузке" }, + { status: 500 } + ); + } +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..84bf940 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,64 @@ +@import "tailwindcss"; + +@theme { + --color-background: #0a0a0f; + --color-surface: #12121a; + --color-surface-hover: #1a1a26; + --color-border: #2a2a3a; + --color-muted: #71717a; + --color-foreground: #fafafa; + --color-accent: #6366f1; + --color-accent-light: #818cf8; + --color-accent-glow: rgba(99, 102, 241, 0.4); + --color-success: #22c55e; + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground antialiased; + font-family: var(--font-sans); + } + + ::selection { + background: rgba(99, 102, 241, 0.3); + } +} + +@layer utilities { + .gradient-text { + @apply bg-gradient-to-r from-indigo-400 via-violet-400 to-purple-400 bg-clip-text text-transparent; + } + + .glass { + @apply bg-surface/60 backdrop-blur-xl border border-border/50; + } + + .glow { + box-shadow: 0 0 60px -12px var(--color-accent-glow); + } +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +.animate-shimmer { + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent); + background-size: 200% 100%; + animation: shimmer 2s infinite; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..1f21831 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ + subsets: ["latin", "cyrillic"], + variable: "--font-inter", +}); + +export const metadata: Metadata = { + title: "PhotoHost — Быстрый фотохостинг", + description: + "Загружайте, храните и делитесь фотографиями мгновенно. Бесплатный современный фотохостинг.", + icons: { icon: "/icon.svg" }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..d38a805 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,43 @@ +import { prisma } from "@/lib/prisma"; +import { Header } from "@/components/Header"; +import { Hero } from "@/components/Hero"; +import { UploadZone } from "@/components/UploadZone"; +import { Gallery } from "@/components/Gallery"; +import { Features } from "@/components/Features"; +import { Footer } from "@/components/Footer"; + +export const dynamic = "force-dynamic"; + +async function getRecentPhotos() { + try { + return await prisma.photo.findMany({ + orderBy: { createdAt: "desc" }, + take: 12, + }); + } catch { + return []; + } +} + +export default async function HomePage() { + const photos = await getRecentPhotos(); + + return ( +
+
+
+
+
+
+ +
+
+ + + + +
+
+
+ ); +} diff --git a/src/components/Features.tsx b/src/components/Features.tsx new file mode 100644 index 0000000..961428e --- /dev/null +++ b/src/components/Features.tsx @@ -0,0 +1,95 @@ +import { + Zap, + Link2, + Shield, + Globe, + HardDrive, + Sparkles, +} from "lucide-react"; + +const features = [ + { + icon: Zap, + title: "Мгновенная загрузка", + description: + "Drag & drop или выбор файла — прямая ссылка готова за секунду.", + color: "text-amber-400", + bg: "bg-amber-400/10", + }, + { + icon: Link2, + title: "Прямые ссылки", + description: + "Получайте прямую ссылку на изображение для форумов, чатов и сайтов.", + color: "text-indigo-400", + bg: "bg-indigo-400/10", + }, + { + icon: Shield, + title: "Безопасность", + description: + "Файлы хранятся на защищённом сервере с контролем типов и размера.", + color: "text-emerald-400", + bg: "bg-emerald-400/10", + }, + { + icon: Globe, + title: "Доступ отовсюду", + description: + "Загружайте с любого устройства — работает на всех платформах.", + color: "text-sky-400", + bg: "bg-sky-400/10", + }, + { + icon: HardDrive, + title: "Надёжное хранение", + description: + "PostgreSQL + Docker — ваши фото в безопасности и всегда доступны.", + color: "text-violet-400", + bg: "bg-violet-400/10", + }, + { + icon: Sparkles, + title: "Без регистрации", + description: + "Не нужен аккаунт — просто загрузите и поделитесь ссылкой.", + color: "text-pink-400", + bg: "bg-pink-400/10", + }, +]; + +export function Features() { + return ( +
+
+
+

+ Почему PhotoHost? +

+

+ Всё, что нужно для быстрого обмена фотографиями +

+
+ +
+ {features.map((feature) => ( +
+
+ +
+

{feature.title}

+

+ {feature.description} +

+
+ ))} +
+
+
+ ); +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..ab3a0cd --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,27 @@ +import { Camera, Heart } from "lucide-react"; + +export function Footer() { + return ( +
+
+
+
+ +
+ + PhotoHost + +
+ +

+ Сделано с на Next.js + + PostgreSQL +

+ +

+ © {new Date().getFullYear()} PhotoHost +

+
+
+ ); +} diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx new file mode 100644 index 0000000..c51adfe --- /dev/null +++ b/src/components/Gallery.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { Images, Eye, Calendar } from "lucide-react"; + +interface Photo { + id: string; + filename: string; + originalName: string; + url: string; + views: number; + createdAt: string; +} + +interface GalleryProps { + initialPhotos: Photo[]; +} + +export function Gallery({ initialPhotos }: GalleryProps) { + const [photos, setPhotos] = useState(initialPhotos); + + useEffect(() => { + const handler = (e: Event) => { + const customEvent = e as CustomEvent; + setPhotos((prev) => [customEvent.detail, ...prev].slice(0, 12)); + }; + window.addEventListener("photo-uploaded", handler); + return () => window.removeEventListener("photo-uploaded", handler); + }, []); + + return ( + + ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..2c7ba0e --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { Camera, Upload } from "lucide-react"; +import Link from "next/link"; + +export function Header() { + return ( +
+ +
+ ); +} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx new file mode 100644 index 0000000..2eb64c4 --- /dev/null +++ b/src/components/Hero.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Sparkles, Zap, Shield } from "lucide-react"; + +export function Hero() { + return ( +
+
+
+ + Бесплатный фотохостинг нового поколения +
+ +

+ Загружайте фото{" "} + мгновенно +

+ +

+ Перетащите изображение — получите прямую ссылку за секунду. + Без регистрации, без ограничений, с красивой галереей. +

+ +
+
+ + Мгновенная загрузка +
+
+ + Безопасное хранение +
+
+ + Прямые ссылки +
+
+
+ +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+ ))} +
+
+ ); +} diff --git a/src/components/UploadZone.tsx b/src/components/UploadZone.tsx new file mode 100644 index 0000000..31e18d9 --- /dev/null +++ b/src/components/UploadZone.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { useCallback, useRef, useState } from "react"; +import { + Upload, + ImageIcon, + Link2, + Check, + Loader2, + X, + Copy, +} from "lucide-react"; + +interface UploadedPhoto { + id: string; + url: string; + originalName: string; +} + +export function UploadZone() { + const [isDragging, setIsDragging] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [uploaded, setUploaded] = useState(null); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + const inputRef = useRef(null); + + const uploadFile = useCallback(async (file: File) => { + if (!file.type.startsWith("image/")) { + setError("Можно загружать только изображения"); + return; + } + + if (file.size > 10 * 1024 * 1024) { + setError("Максимальный размер файла — 10 МБ"); + return; + } + + setError(null); + setIsUploading(true); + setUploaded(null); + + const formData = new FormData(); + formData.append("file", file); + + try { + const res = await fetch("/api/upload", { + method: "POST", + body: formData, + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "Ошибка загрузки"); + } + + const photo = await res.json(); + setUploaded(photo); + window.dispatchEvent(new CustomEvent("photo-uploaded", { detail: photo })); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка загрузки"); + } finally { + setIsUploading(false); + } + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (file) uploadFile(file); + }, + [uploadFile] + ); + + const handleCopy = async () => { + if (!uploaded) return; + const fullUrl = `${window.location.origin}${uploaded.url}`; + await navigator.clipboard.writeText(fullUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const reset = () => { + setUploaded(null); + setError(null); + setCopied(false); + }; + + return ( +
+
+
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + onClick={() => !isUploading && !uploaded && inputRef.current?.click()} + className={`group relative cursor-pointer rounded-3xl border-2 border-dashed p-12 text-center transition-all duration-300 ${ + isDragging + ? "border-indigo-400 bg-indigo-500/10 glow" + : "border-border hover:border-indigo-500/50 hover:bg-surface/50" + } ${uploaded ? "cursor-default border-emerald-500/30 bg-emerald-500/5" : ""}`} + > + { + const file = e.target.files?.[0]; + if (file) uploadFile(file); + e.target.value = ""; + }} + /> + + {isUploading ? ( +
+ +

Загрузка...

+
+ ) : uploaded ? ( +
+
+ +
+
+

+ Фото загружено! +

+

{uploaded.originalName}

+
+
+ + + +
+ +
+ ) : ( +
+
+ +
+
+

+ Перетащите фото сюда +

+

+ или нажмите для выбора файла +

+
+
+ + + JPG, PNG, GIF, WebP + + до 10 МБ +
+
+ )} +
+ + {error && ( +
+ + {error} +
+ )} +
+
+ ); +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..bd05edf --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,15 @@ +import { PrismaClient } from "@prisma/client"; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"], + }); + +if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = prisma; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dab011d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./src/*"] }, + "target": "ES2017" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}