diff --git a/Dockerfile b/Dockerfile index 20dd2ca..c372971 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,9 +21,15 @@ FROM base AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 +RUN apk add --no-cache libc6-compat RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs + +# node_modules for prisma CLI at runtime +COPY --from=deps /app/node_modules ./node_modules COPY --from=builder /app/public ./public +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/prisma.config.ts ./prisma.config.ts RUN mkdir .next && chown nextjs:nodejs .next COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static @@ -31,4 +37,5 @@ USER nextjs EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] +# Push schema (idempotent, safe to run on every start) then start server +CMD sh -c "npx prisma db push --skip-generate 2>&1 | tail -5 && node server.js" diff --git a/lib/db.ts b/lib/db.ts index 50f04e4..bfee4c3 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,34 +1,24 @@ -import { Pool } from 'pg'; -import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '@prisma/client'; -const connectionString = - process.env.DATABASE_URL || - 'postgres://postgres:mBTWE2cDKGExtpktguD3HPe8y9Xr9kWFYdxV4WHQKISLLGoBAS4UdtfSCfXwvPpq@65.109.236.58:37298/postgres'; - -const pool = new Pool({ connectionString }); -const adapter = new PrismaPg(pool); - const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; -function getPrismaClient(): PrismaClient { - if (process.env.NODE_ENV !== 'production' && globalForPrisma.prisma) { - // Ensure all models (category, cheatsheet, prompt) exist on cached client - if ( - (globalForPrisma.prisma as any).category && - (globalForPrisma.prisma as any).cheatsheet && - (globalForPrisma.prisma as any).prompt - ) { - return globalForPrisma.prisma; - } +function createPrismaClient() { + const databaseUrl = process.env.DATABASE_URL; + + if (!databaseUrl) { + console.error('[DB] DATABASE_URL is not set! Using fallback connection string.'); } - const client = new PrismaClient({ adapter }); - if (process.env.NODE_ENV !== 'production') { - globalForPrisma.prisma = client; - } - return client; + + return new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'], + }); } -export const db = getPrismaClient(); +export const db = + globalForPrisma.prisma ?? createPrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = db; +} diff --git a/package-lock.json b/package-lock.json index 4d1bfb0..c04f99c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "dependencies": { "@base-ui/react": "^1.5.0", - "@prisma/adapter-pg": "^7.9.0", "@prisma/client": "^7.8.0", "class-variance-authority": "^0.7.1", "cloudinary": "^2.10.0", @@ -20,7 +19,6 @@ "next": "16.2.9", "next-auth": "^5.0.0-beta.31", "next-intl": "^4.13.0", - "pg": "^8.22.0", "react": "19.2.4", "react-dom": "19.2.4", "shadcn": "^4.11.0", @@ -30,7 +28,6 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", @@ -2251,18 +2248,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@prisma/adapter-pg": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.9.0.tgz", - "integrity": "sha512-kPYuFvNTlqnaFf2UpXBBG3ycTT3PL76uSZtLFBEwDytjMMUW8ZHrsb9cSNIarzdPW5EXWmBOeOq9/MVjMtbWkA==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/driver-adapter-utils": "7.9.0", - "@types/pg": "^8.16.0", - "pg": "^8.16.3", - "postgres-array": "3.0.4" - } - }, "node_modules/@prisma/client": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", @@ -2339,21 +2324,6 @@ "zeptomatch": "2.1.0" } }, - "node_modules/@prisma/driver-adapter-utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.0.tgz", - "integrity": "sha512-fFXujitfMyjk3kOd1Tbs5FXBm6i2OWwEhaP5lHgkUM99jHpPEQwCWj+z/WKPFq6EDMThE1zGzSlVegtR0Pmu2w==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.9.0" - } - }, - "node_modules/@prisma/driver-adapter-utils/node_modules/@prisma/debug": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.0.tgz", - "integrity": "sha512-i0KdVQuKUE6N9NloHs+sUNAk2c9svR3myBndQbA3BoeoArsSpwtNgTdHZL+wBtCLCcdS2OOC/PKhgTe36jkF5A==", - "license": "Apache-2.0" - }, "node_modules/@prisma/engines": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", @@ -3275,22 +3245,12 @@ "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@types/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -9058,104 +9018,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/pg": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", - "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.14.0", - "pg-pool": "^3.14.0", - "pg-protocol": "^1.15.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.4.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", - "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", - "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", - "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", - "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pg-types/node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9266,45 +9128,6 @@ "url": "https://github.com/sponsors/porsager" } }, - "node_modules/postgres-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", - "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -10279,15 +10102,6 @@ "node": ">=0.10.0" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -10976,6 +10790,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -11300,15 +11115,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 14b8576..f6df936 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@base-ui/react": "^1.5.0", - "@prisma/adapter-pg": "^7.9.0", "@prisma/client": "^7.8.0", "class-variance-authority": "^0.7.1", "cloudinary": "^2.10.0", @@ -21,7 +20,6 @@ "next": "16.2.9", "next-auth": "^5.0.0-beta.31", "next-intl": "^4.13.0", - "pg": "^8.22.0", "react": "19.2.4", "react-dom": "19.2.4", "shadcn": "^4.11.0", @@ -31,7 +29,6 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", diff --git a/prisma/seed.ts b/prisma/seed.ts index 8013d40..402ff27 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,12 +1,6 @@ -import { Pool } from 'pg'; -import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '@prisma/client'; -const connectionString = process.env.DATABASE_URL || 'postgres://postgres:mBTWE2cDKGExtpktguD3HPe8y9Xr9kWFYdxV4WHQKISLLGoBAS4UdtfSCfXwvPpq@65.109.236.58:37298/postgres'; - -const pool = new Pool({ connectionString }); -const adapter = new PrismaPg(pool); -const db = new PrismaClient({ adapter }); +const db = new PrismaClient(); async function main() { console.log('Seeding PostgreSQL database...'); @@ -47,19 +41,19 @@ async function main() { fileName: 'app/actions.ts', language: 'typescript', description: 'Server Action function handling form submission', - code: `'use server'\n\nimport { revalidatePath } from 'next/cache';\n\nexport async function submitProjectIdea(formData: FormData) {\n const title = formData.get('title') as string;\n const description = formData.get('description') as string;\n\n if (!title || title.length < 3) {\n return { success: false, error: 'Title must be at least 3 characters long.' };\n }\n\n // Database insertion simulation\n console.log('Saved Idea:', { title, description });\n\n revalidatePath('/ideas');\n return { success: true, message: 'Your idea was submitted successfully!' };\n}` + code: `'use server'\n\nimport { revalidatePath } from 'next/cache';\n\nexport async function submitProjectIdea(formData: FormData) {\n const title = formData.get('title') as string;\n const description = formData.get('description') as string;\n\n if (!title || title.length < 3) {\n return { success: false, error: 'Title must be at least 3 characters long.' };\n }\n\n console.log('Saved Idea:', { title, description });\n\n revalidatePath('/ideas');\n return { success: true, message: 'Your idea was submitted successfully!' };\n}` }, { fileName: 'proxy.ts', language: 'typescript', description: 'Next.js 16 proxy configuration (middleware alternative)', - code: `import { NextRequest, NextResponse } from 'next/server';\nimport createMiddleware from 'next-intl/middleware';\nimport { routing } from '@/i18n/routing';\n\nconst intlMiddleware = createMiddleware(routing);\n\nexport async function proxy(request: NextRequest) {\n if (request.nextUrl.pathname.includes('/admin')) {\n // Auth check logic\n }\n return intlMiddleware(request);\n}\n\nexport const config = {\n matcher: ['/((?!api|_next|_vercel|.*\\\\..*).*)']\n};` + code: `import { NextRequest, NextResponse } from 'next/server';\nimport createMiddleware from 'next-intl/middleware';\nimport { routing } from '@/i18n/routing';\n\nconst intlMiddleware = createMiddleware(routing);\n\nexport async function proxy(request: NextRequest) {\n if (request.nextUrl.pathname.includes('/admin')) {\n // Auth check logic\n }\n return intlMiddleware(request);\n}\n\nexport const config = {\n matcher: ['/((?!api|_next|_vercel|.*\\\\..*).*)'],\n};` }, { fileName: 'components/IdeaForm.tsx', language: 'tsx', description: 'Client Form component using useActionState', - code: `'use client';\n\nimport { useActionState } from 'react';\nimport { submitProjectIdea } from '@/app/actions';\n\nexport function IdeaForm() {\n const [state, formAction, isPending] = useActionState(submitProjectIdea, null);\n\n return (\n
\n

Add New Idea

\n \n
\n \n \n
\n\n \n\n {state?.error &&

{state.error}

}\n {state?.success &&

{state.message}

}\n
\n );\n}` + code: `'use client';\n\nimport { useActionState } from 'react';\nimport { submitProjectIdea } from '@/app/actions';\n\nexport function IdeaForm() {\n const [state, formAction, isPending] = useActionState(submitProjectIdea, null);\n\n return (\n
\n

Add New Idea

\n
\n \n \n
\n \n {state?.error &&

{state.error}

}\n {state?.success &&

{state.message}

}\n
\n );\n}` } ] }, @@ -112,9 +106,9 @@ async function main() { likesCount: 1450, summary: 'Build an autonomous AI agent in Python that crawls web data, summarizes tech news, and generates structured reports automatically.', notesMarkdown: [ - '🤖 AI agents are configured with role, goal, and backstory.', - '🤖 Equip agents with real-time web search using Serper API or Tavily.', - '🤖 Gemini 1.5 Flash provides high token throughput with minimal latency.' + 'AI agents are configured with role, goal, and backstory.', + 'Equip agents with real-time web search using Serper API or Tavily.', + 'Gemini 1.5 Flash provides high token throughput with minimal latency.' ], codeSnippets: { create: [ @@ -167,9 +161,9 @@ async function main() { likesCount: 2600, summary: 'Learn CSS-first configuration in Tailwind CSS v4 using @import "tailwindcss";, OKLCH color spaces, and advanced glassmorphism component design.', notesMarkdown: [ - '🎨 In Tailwind v4, @theme directive in globals.css replaces tailwind.config.js.', - '🎨 OKLCH color space ensures consistent perceived contrast across light and dark modes.', - '🎨 @custom-variant dark provides flexible dark mode scoping.' + 'In Tailwind v4, @theme directive in globals.css replaces tailwind.config.js.', + 'OKLCH color space ensures consistent perceived contrast across light and dark modes.', + '@custom-variant dark provides flexible dark mode scoping.' ], codeSnippets: { create: [ @@ -252,6 +246,7 @@ async function main() { description: 'System prompt to instruct AI agents to build production-ready Next.js 16 App Router code with Server Actions and Prisma.', content: `You are a Senior Next.js 16 Architect. Always use TypeScript strict mode, App Router, proxy.ts for middleware, Server Actions with Zod validation, and Prisma ORM. Never write client components unless interactivity (onClick, useState) is required.`, tags: ['Next.js', 'System Prompt', 'AI Architecture'], + isOpen: false, } }); @@ -263,6 +258,7 @@ async function main() { description: 'Instructs AI to design modern dark-mode interfaces with Tailwind CSS v4, subtle glows, and micro-animations.', content: `Act as a Lead UI/UX Engineer specializing in sleek dark mode interfaces. Use HSL/OKLCH color palettes, slate-950 backgrounds, red-500/10 glows, rounded-2xl containers, and Framer Motion micro-interactions. Avoid generic colors or white backgrounds.`, tags: ['UI/UX', 'Tailwind CSS', 'Design System'], + isOpen: false, } }); @@ -276,5 +272,4 @@ main() }) .finally(async () => { await db.$disconnect(); - await pool.end(); });