I'm encountering an issue with my Next.js application when running npm run build or deploying to Vercel. The build process fails with an error originating from the .next folder, though the specific error message points to a problem with one of my components. Interestingly, everything works perfectly fine in my development environment when using npm run
dev. I've already tried clearing the .next folder, removing node_modules and running a fresh install, but the issue persists. I'm using Next.js version 15.2.1.
Has anyone encountered similar build failures that only appear in production builds but not in development? What debugging steps should I take to identify the root cause? This is particularly frustrating since the error doesn't provide a clear indication of which file or component is causing the problem.
The error i got
.next/types/app/applications/[id]/page.ts:34:29
Type error: Type '{ params: { id: string; }; }' does not satisfy the constraint 'PageProps'.
Types of property 'params' are incompatible.
Type '{ id: string; }' is missing the following properties from type 'Promise<any>': then, catch, finally, [Symbol.toStringTag]
32 |
33 | // Check the prop type of the entry function
> 34 | checkFields<Diff<PageProps, FirstArg<TEntry['default']>, 'default'>>()
| ^
35 |
36 | // Check the arguments and return type of the generateMetadata function
37 | if ('generateMetadata' in entry) {
Next.js build worker exited with code: 1 and signal: null
applications/[id]/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { JobApplication, JobPosting } from '@/lib/types';
import Link from 'next/link';
import { appwriteService } from '@/app/api/AppwriteService';
import { Badge } from '@/components/ui/badge';
import { cn, getApplicationStatusColor } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { format } from 'date-fns';
export default function ApplicationDetailsPage({ params }: { params: { id: string } }) {
const router = useRouter();
const [application, setApplication] = useState<JobApplication | null>(null);
const [job, setJob] = useState<JobPosting | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [actionMenuOpen, setActionMenuOpen] = useState(false);
const [resumeUrl, setResumeUrl] = useState<string | null>(null);
useEffect(() => {
async function fetchApplicationDetails() {
try {
setLoading(true);
// Fetch application details
const appData = await appwriteService.getApplication(params.id);
setApplication(appData);
setResumeUrl(appData.resume_id ? (await appwriteService.getResumeUrl(appData.resume_id)).toString() : null);
// Fetch associated job posting
const jobData = await appwriteService.getJobPosting(appData.job_posting_id);
setJob(jobData);
} catch (error) {
console.error('Error fetching application details:', error);
setError('Failed to load application details. The application may not exist.');
} finally {
setLoading(false);
}
}
fetchApplicationDetails();
}, [params.id]);
async function handleStatusChange(newStatus: string) {
if (!application) return;
try {
const updatedApplication = await appwriteService.updateApplication(application.$id, {
status: newStatus as 'Pending' | 'Reviewed' | 'Interviewing' | 'Rejected' | 'Hired'
});
setApplication(updatedApplication);
setActionMenuOpen(false);
} catch (error) {
console.error('Error updating application status:', error);
}
}
async function handleDeleteApplication() {
if (!application) return;
try {
await appwriteService.deleteApplication(application.$id, application.resume_id);
router.push('/applications');
} catch (error) {
console.error('Error deleting application:', error);
}
}
if (loading) {
return (
<div className="p-8">
<div className="flex justify-center items-center h-64">
<p>Loading application details...</p>
</div>
</div>
);
}
if (error || !application || !job) {
return (
<div className="p-8">
<div className="bg-red-50 p-4 rounded-md">
<h2 className="text-lg font-medium text-red-800 mb-2">Error</h2>
<p className="text-red-700">{error || 'Application not found'}</p>
<Link href="/applications" className="mt-4 inline-block text-red-600 hover:text-red-500">
Back to applications
</Link>
</div>
</div>
);
}
return (
<div className="p-8">
<div className="mb-6">
<Link href="/applications" className="text-blue-600 hover:underline flex items-center">
<svg
className="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
></path>
</svg>
Back to Applications
</Link>
</div>
<div className="flex justify-between items-start mb-6">
<div>
<h1 className="text-2xl font-bold">{application.applicant_name}</h1>
<div className="flex items-center mt-2">
<Link href={`/jobs/${job.$id}`} className="text-blue-600 hover:underline">
{job.title}
</Link>
</div>
</div>
<div className="flex items-center">
<Badge className={cn(getApplicationStatusColor(application.status), 'mr-4')}>
{application.status}
</Badge>
<div className="relative">
<Button
variant="outline"
onClick={() => setActionMenuOpen(!actionMenuOpen)}
>
Actions
<svg
className="ml-1 h-4 w-4"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M19 9l-7 7-7-7"></path>
</svg>
</Button>
{actionMenuOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-10">
{application.status !== 'Reviewed' && (
<button
onClick={() => handleStatusChange('Reviewed')}
className="block w-full text-left px-4 py-2 text-sm text-blue-700 hover:bg-gray-100"
>
Mark as Reviewed
</button>
)}
{application.status !== 'Interviewing' && (
<button
onClick={() => handleStatusChange('Interviewing')}
className="block w-full text-left px-4 py-2 text-sm text-purple-700 hover:bg-gray-100"
>
Move to Interview
</button>
)}
{application.status !== 'Rejected' && (
<button
onClick={() => handleStatusChange('Rejected')}
className="block w-full text-left px-4 py-2 text-sm text-red-700 hover:bg-gray-100"
>
Reject Application
</button>
)}
{application.status !== 'Hired' && (
<button
onClick={() => handleStatusChange('Hired')}
className="block w-full text-left px-4 py-2 text-sm text-green-700 hover:bg-gray-100"
>
Mark as Hired
</button>
)}
<button
onClick={() => {
if (confirm('Are you sure you want to delete this application? This action cannot be undone.')) {
handleDeleteApplication();
}
}}
className="block w-full text-left px-4 py-2 text-sm text-red-700 hover:bg-gray-100"
>
Delete Application
</button>
</div>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card className="mb-6">
<CardContent>
<h2 className="text-lg font-semibold mb-4">Applicant Information</h2>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-medium text-gray-500">Full Name</h3>
<p className="mt-1">{application.applicant_name}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Email</h3>
<p className="mt-1">
<a href={`mailto:${application.email}`} className="text-blue-600 hover:underline">
{application.email}
</a>
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-medium text-gray-500">Phone</h3>
<p className="mt-1">
<a href={`tel:${application.phone}`} className="text-blue-600 hover:underline">
{application.phone}
</a>
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">LinkedIn</h3>
<p className="mt-1">
{application.linkedin_url ? (
<a href={application.linkedin_url} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
View Profile
</a>
) : (
<span className="text-gray-500">Not provided</span>
)}
</p>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Portfolio</h3>
<p className="mt-1">
{application.portfolio_url ? (
<a href={application.portfolio_url} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
View Portfolio
</a>
) : (
<span className="text-gray-500">Not provided</span>
)}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Resume</h3>
<p className="mt-1">
{resumeUrl ? (
<a href={resumeUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline flex items-center">
<svg
className="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
></path>
</svg>
Download Resume
</a>
) : (
<span className="text-gray-500">Not provided</span>
)}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent>
<h2 className="text-lg font-semibold mb-4">Cover Letter</h2>
<div className="prose max-w-none">
{application.cover_letter ? (
<p className="whitespace-pre-line text-gray-700">{application.cover_letter}</p>
) : (
<p className="text-gray-500">No cover letter provided.</p>
)}
</div>
</CardContent>
</Card>
</div>
<div className="lg:col-span-1">
<Card className="mb-6">
<CardContent>
<h2 className="text-lg font-semibold mb-4">Application Details</h2>
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-500">Applied For</h3>
<p className="mt-1">
<Link href={`/jobs/${job.$id}`} className="text-blue-600 hover:underline">
{job.title}
</Link>
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Application Date</h3>
<p className="mt-1"> {format(new Date(application.$createdAt), 'dd MMMM yyyy | hh:mm aa')}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Last Updated</h3>
<p className="mt-1"> {format(new Date(application.$updatedAt), 'dd MMMM yyyy | hh:mm aa')}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Status</h3>
<p className="mt-1">
<Badge className={getApplicationStatusColor(application.status)}>
{application.status}
</Badge>
</p>
</div>
</div>
<div className="mt-6 border-t pt-4">
<h3 className="text-sm font-medium text-gray-500 mb-2">Update Status</h3>
<div className="space-y-2">
{['Pending', 'Reviewed', 'Interviewing', 'Rejected', 'Hired'].map(status => (
<Button
key={status}
variant={application.status === status ? 'default' : 'outline'}
size="sm"
onClick={() => handleStatusChange(status)}
disabled={application.status === status}
className="mr-2 mb-2"
>
{status}
</Button>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent>
<h2 className="text-lg font-semibold mb-4">Notes</h2>
<div className="mb-4">
<textarea
disabled
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
placeholder="Add private notes about this applicant..."
></textarea>
</div>
<Button className="w-full">Save Notes</Button>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
package.json
{
"name": "ranwip-admin",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.2.1",
"@radix-ui/react-slot": "^1.1.2",
"@tiptap/extension-bold": "^2.11.5",
"@tiptap/extension-bullet-list": "^2.11.5",
"@tiptap/extension-heading": "^2.11.5",
"@tiptap/extension-italic": "^2.11.5",
"@tiptap/extension-link": "^2.11.5",
"@tiptap/extension-list-item": "^2.11.5",
"@tiptap/extension-ordered-list": "^2.11.5",
"@tiptap/extension-placeholder": "^2.11.5",
"@tiptap/extension-text-align": "^2.11.5",
"@tiptap/pm": "^2.11.5",
"@tiptap/react": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5",
"appwrite": "^14.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cookies-next": "^5.1.0",
"date-fns": "^4.1.0",
"lucide-react": "^0.477.0",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"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/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
The issue is coming from how you're defining your params
object in your page props. From NextJS 15, the params
and searchParams
objects are now Promises rather than synchronous objects as they were in the versions preceding NextJS 15.
You'll need to change the ApplicationDetailsPage
props from the current synchronous object to a Promise and you'll need to await the params
object before using it in the appwriteService.getApplication
function but since you're using client component at the page level (which is not recommended), you can use the use
hook from React to unwrap the promise.
'use client'
import { use, useEffect } from 'react'
type ApplicationDetailsPageProps = {
params: Promise<{id: string}>
}
export default function ApplicationDetailsPage({ params }: ApplicationDetailsPageProps) {
const { id } = use(params);
useEffect(() => {
async function fetchApplicationDetails() {
try {
setLoading(true);
// Fetch application details
const appData = await appwriteService.getApplication(id);
setApplication(appData);
setResumeUrl(appData.resume_id ? (await appwriteService.getResumeUrl(appData.resume_id)).toString() : null);
// Fetch associated job posting
const jobData = await appwriteService.getJobPosting(appData.job_posting_id);
setJob(jobData);
} catch (error) {
console.error('Error fetching application details:', error);
setError('Failed to load application details. The application may not exist.');
} finally {
setLoading(false);
}
}
fetchApplicationDetails();
}, [id]);
return (
//JSX
)
}