Skip to content

Commit 1db782c

Browse files
Added jobs api endpoint, details page + components.
1 parent faec32e commit 1db782c

File tree

7 files changed

+666
-0
lines changed

7 files changed

+666
-0
lines changed

ecosystem.config.cjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
apps: [{
3+
name: 'codebuilder-frontend',
4+
script: 'dist/main.js',
5+
env: {
6+
PORT: 3000,
7+
NODE_ENV: 'production',
8+
},
9+
}],
10+
};

src/app/api/jobs/fetch/route.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { NextResponse } from 'next/server'
2+
import { fetchRedditPosts, storeRedditJobPosts } from '@/lib/jobs/reddit'
3+
import { fetchWeb3CareerJobs, storeWeb3CareerJobs } from '@/lib/jobs/web3career'
4+
import prisma from '@/lib/db'
5+
import { logger } from '@/lib/logger'
6+
7+
// Subreddits to scan for jobs
8+
const SUBREDDITS = [
9+
'forhire',
10+
'jobs4bitcoins',
11+
'freelance',
12+
'remotejs',
13+
'jobs4dogecoins',
14+
'jobs4crypto',
15+
]
16+
17+
export async function GET() {
18+
logger.info('Starting the job fetch route...')
19+
try {
20+
// Fetch posts from Reddit
21+
logger.info('Fetching Reddit posts...')
22+
const redditPosts = await fetchRedditPosts(SUBREDDITS)
23+
logger.info(`Fetched ${redditPosts.length} posts. Filtering for "[Hiring]" in title...`)
24+
const hiringPosts = redditPosts.filter((post) => post.title.includes('[Hiring]'))
25+
logger.info(`Filtered down to ${hiringPosts.length} [Hiring] posts. Storing to database...`)
26+
await storeRedditJobPosts(hiringPosts)
27+
logger.info('Reddit jobs stored successfully.')
28+
29+
// Fetch posts from Web3Career
30+
logger.info('Fetching Web3Career jobs...')
31+
const web3Jobs = await fetchWeb3CareerJobs()
32+
logger.info(`Fetched ${web3Jobs.length} Web3Career jobs. Storing to database...`)
33+
await storeWeb3CareerJobs(web3Jobs)
34+
logger.info('Web3Career jobs stored successfully.')
35+
36+
logger.info('All jobs fetched and stored successfully.')
37+
return NextResponse.json({ message: 'All jobs fetched and stored successfully.' })
38+
} catch (error) {
39+
logger.error('Failed to fetch or store jobs in the jobs/fetch route.', error)
40+
return NextResponse.json({ error: 'An error occurred while fetching jobs.' }, { status: 500 })
41+
} finally {
42+
logger.info('Disconnecting Prisma client...')
43+
await prisma.$disconnect()
44+
}
45+
}

src/app/api/jobs/route.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import prisma from '@/lib/db'
3+
import { withLogging } from '@/lib/logger'
4+
5+
/**
6+
* Returns a paginated list of all jobs from all sources,
7+
* including company, tags, and metadata relations.
8+
* Query params: ?page=1&pageSize=10
9+
*/
10+
export const GET = withLogging(async (request: NextRequest) => {
11+
try {
12+
const { searchParams } = new URL(request.url)
13+
const page = parseInt(searchParams.get('page') || '1', 10)
14+
const pageSize = parseInt(searchParams.get('pageSize') || '10', 10)
15+
const skip = (page - 1) * pageSize
16+
17+
// Fetch jobs, newest first, including company, tags, and metadata
18+
const jobs = await prisma.job.findMany({
19+
orderBy: { createdAt: 'desc' },
20+
skip,
21+
take: pageSize,
22+
include: {
23+
company: true,
24+
tags: { include: { tag: true } },
25+
metadata: true,
26+
sources: true,
27+
},
28+
})
29+
30+
const totalCount = await prisma.job.count()
31+
32+
return NextResponse.json({
33+
data: jobs,
34+
page,
35+
pageSize,
36+
totalCount,
37+
})
38+
} catch (error) {
39+
return NextResponse.json({ error: (error as Error).message }, { status: 500 })
40+
}
41+
})

src/app/jobs/[id]/not-found.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Link from 'next/link'
2+
3+
export default function JobNotFound() {
4+
return (
5+
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
6+
<div className="text-center">
7+
<div className="mb-8">
8+
<h1 className="text-6xl font-bold text-gray-400 mb-4">404</h1>
9+
<h2 className="text-2xl font-semibold text-gray-700 mb-2">Job Not Found</h2>
10+
<p className="text-gray-600">
11+
The job you're looking for doesn't exist or has been removed.
12+
</p>
13+
</div>
14+
15+
<div className="space-y-4">
16+
<Link
17+
href="/jobs"
18+
className="inline-block px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200"
19+
>
20+
View All Jobs
21+
</Link>
22+
23+
<div className="text-sm text-gray-500">
24+
<p>or</p>
25+
<Link
26+
href="/"
27+
className="text-blue-600 hover:text-blue-800 underline"
28+
>
29+
Go to Homepage
30+
</Link>
31+
</div>
32+
</div>
33+
</div>
34+
</div>
35+
)
36+
}

src/app/jobs/[id]/page.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import React from 'react'
2+
import Link from 'next/link'
3+
import { notFound } from 'next/navigation'
4+
import prisma from '@/lib/db'
5+
import { Prisma } from '@prisma/client'
6+
import JobDetails from '@/components/jobs/JobDetails'
7+
8+
// Type for job with relations
9+
type JobWithRelations = Prisma.JobGetPayload<{
10+
include: {
11+
company: true
12+
tags: { include: { tag: true } }
13+
metadata: true
14+
sources: true
15+
}
16+
}>
17+
18+
interface JobPageProps {
19+
params: Promise<{ id: string }>
20+
}
21+
22+
export default async function JobPage({ params }: JobPageProps) {
23+
const { id } = await params
24+
const jobId = parseInt(id, 10)
25+
26+
if (isNaN(jobId)) {
27+
notFound()
28+
}
29+
30+
const job = (await prisma.job.findUnique({
31+
where: { id: jobId },
32+
include: {
33+
company: true,
34+
tags: { include: { tag: true } },
35+
metadata: true,
36+
sources: true,
37+
},
38+
})) as JobWithRelations | null
39+
40+
if (!job) {
41+
notFound()
42+
}
43+
44+
await prisma.$disconnect()
45+
46+
return (
47+
<div className="flex flex-col inset-0 z-50 bg-primary transition-transform">
48+
<section className={`bg-gray-100 py-4 md:py-6`}>
49+
<div className="container mx-auto py-16 px-8 md:px-20 lg:px-32">
50+
{/* Navigation */}
51+
<nav className="mb-6">
52+
<Link
53+
href="/jobs"
54+
className="text-blue-600 hover:text-blue-800 transition-colors duration-200"
55+
>
56+
← Back to Jobs
57+
</Link>
58+
</nav>
59+
60+
{/* Job Details */}
61+
<JobDetails job={job} />
62+
</div>
63+
</section>
64+
</div>
65+
)
66+
}
67+
68+
// Generate metadata for SEO
69+
export async function generateMetadata({ params }: JobPageProps) {
70+
const { id } = await params
71+
const jobId = parseInt(id, 10)
72+
73+
if (isNaN(jobId)) {
74+
return {
75+
title: 'Job Not Found',
76+
}
77+
}
78+
79+
const job = await prisma.job.findUnique({
80+
where: { id: jobId },
81+
select: {
82+
title: true,
83+
company: { select: { name: true } },
84+
description: true,
85+
},
86+
})
87+
88+
if (!job) {
89+
return {
90+
title: 'Job Not Found',
91+
}
92+
}
93+
94+
const companyName = job.company?.name || 'Unknown Company'
95+
const title = `${job.title} - ${companyName}`
96+
const description =
97+
job.description?.substring(0, 160) || `${job.title} job opportunity at ${companyName}`
98+
99+
return {
100+
title,
101+
description,
102+
openGraph: {
103+
title,
104+
description,
105+
type: 'website',
106+
},
107+
}
108+
}

0 commit comments

Comments
 (0)