-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
added new job #163
added new job #163
Changes from 6 commits
5bfac25
3a9541b
5e9bfce
c5a4049
0ac40f0
a342e21
84f9616
0882db0
37bf419
bf3265f
bc08f29
33d4bfd
b6ce656
71c62c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,35 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { createClient } from '@supabase/supabase-js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
import { NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
const supabaseKey = process.env.SUPABASE_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||||||
const supabase = createClient(supabaseUrl, supabaseKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+4
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: Remove hardcoded database URL and add environment variable validation
Apply this diff to fix these issues: -const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co';
-const supabaseKey = process.env.SUPABASE_KEY;
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const supabaseKey = process.env.SUPABASE_KEY;
+
+if (!supabaseUrl || !supabaseKey) {
+ throw new Error('Missing required environment variables for Supabase configuration');
+}
+
const supabase = createClient(supabaseUrl, supabaseKey); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
export async function GET(request, { params }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
const { data: job, error } = await supabase | ||||||||||||||||||||||||||||||||||||||||||||||||||
.from('jobs') | ||||||||||||||||||||||||||||||||||||||||||||||||||
.select('*') | ||||||||||||||||||||||||||||||||||||||||||||||||||
.eq('uuid', params.uuid) | ||||||||||||||||||||||||||||||||||||||||||||||||||
.single(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+8
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add input validation for UUID parameter The UUID parameter should be validated before querying the database to prevent invalid requests and potential SQL injection. Apply this diff: export async function GET(request, { params }) {
+ const uuid = params.uuid;
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+
+ if (!uuid || !uuidRegex.test(uuid)) {
+ return NextResponse.json(
+ { message: 'Invalid UUID format' },
+ { status: 400 }
+ );
+ }
+
try {
const { data: job, error } = await supabase
.from('jobs')
.select('*')
- .eq('uuid', params.uuid)
+ .eq('uuid', uuid)
.single(); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
if (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||
{ message: 'Error fetching job' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
{ status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
if (!job) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json({ message: 'Job not found' }, { status: 404 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json(job); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
console.error('Error fetching job:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||
{ message: 'Internal server error' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
{ status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,241 @@ | ||||||||||||||||||||||||||||||
'use client'; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import React, { useState, useEffect } from 'react'; | ||||||||||||||||||||||||||||||
import { motion, AnimatePresence } from 'framer-motion'; | ||||||||||||||||||||||||||||||
import { Search, MapPin, Briefcase, DollarSign, Filter } from 'lucide-react'; | ||||||||||||||||||||||||||||||
import { useRouter } from 'next/navigation'; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const ClientJobBoard = ({ initialJobs }) => { | ||||||||||||||||||||||||||||||
const [jobs] = useState(initialJobs); | ||||||||||||||||||||||||||||||
const [filteredJobs, setFilteredJobs] = useState(initialJobs); | ||||||||||||||||||||||||||||||
const [searchTerm, setSearchTerm] = useState(''); | ||||||||||||||||||||||||||||||
const [selectedJobType, setSelectedJobType] = useState(''); | ||||||||||||||||||||||||||||||
const [selectedExperience, setSelectedExperience] = useState(''); | ||||||||||||||||||||||||||||||
const [loading] = useState(false); | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Remove unused loading state. The loading state is initialized but never updated, making the loading UI unreachable. - const [loading] = useState(false); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||
let result = jobs; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (searchTerm) { | ||||||||||||||||||||||||||||||
result = result.filter( | ||||||||||||||||||||||||||||||
(job) => | ||||||||||||||||||||||||||||||
job.title?.toLowerCase().includes(searchTerm.toLowerCase()) || | ||||||||||||||||||||||||||||||
job.company?.toLowerCase().includes(searchTerm.toLowerCase()) || | ||||||||||||||||||||||||||||||
job.description?.toLowerCase().includes(searchTerm.toLowerCase()) | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (selectedJobType) { | ||||||||||||||||||||||||||||||
result = result.filter((job) => job.type === selectedJobType); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (selectedExperience) { | ||||||||||||||||||||||||||||||
result = result.filter((job) => job.experience === selectedExperience); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
setFilteredJobs(result); | ||||||||||||||||||||||||||||||
}, [searchTerm, selectedJobType, selectedExperience, jobs]); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||
<div className="flex flex-col md:flex-row gap-6"> | ||||||||||||||||||||||||||||||
<div className="w-full md:w-64"> | ||||||||||||||||||||||||||||||
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} /> | ||||||||||||||||||||||||||||||
<Filters | ||||||||||||||||||||||||||||||
selectedJobType={selectedJobType} | ||||||||||||||||||||||||||||||
setSelectedJobType={setSelectedJobType} | ||||||||||||||||||||||||||||||
selectedExperience={selectedExperience} | ||||||||||||||||||||||||||||||
setSelectedExperience={setSelectedExperience} | ||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
<div className="flex-1"> | ||||||||||||||||||||||||||||||
{loading ? ( | ||||||||||||||||||||||||||||||
<div className="text-center py-10"> | ||||||||||||||||||||||||||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mx-auto"></div> | ||||||||||||||||||||||||||||||
<p className="mt-4 text-gray-600">Loading jobs...</p> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
) : ( | ||||||||||||||||||||||||||||||
<JobList jobs={filteredJobs} /> | ||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const SearchBar = ({ searchTerm, setSearchTerm }) => { | ||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||
<div className="relative mb-6"> | ||||||||||||||||||||||||||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | ||||||||||||||||||||||||||||||
<Search className="h-5 w-5 text-gray-400" /> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
<input | ||||||||||||||||||||||||||||||
type="text" | ||||||||||||||||||||||||||||||
value={searchTerm} | ||||||||||||||||||||||||||||||
onChange={(e) => setSearchTerm(e.target.value)} | ||||||||||||||||||||||||||||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm" | ||||||||||||||||||||||||||||||
placeholder="Search jobs..." | ||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const Filters = ({ | ||||||||||||||||||||||||||||||
selectedJobType, | ||||||||||||||||||||||||||||||
setSelectedJobType, | ||||||||||||||||||||||||||||||
selectedExperience, | ||||||||||||||||||||||||||||||
setSelectedExperience, | ||||||||||||||||||||||||||||||
}) => { | ||||||||||||||||||||||||||||||
const jobTypes = ['Full-time', 'Part-time', 'Contract', 'Internship']; | ||||||||||||||||||||||||||||||
const experienceLevels = [ | ||||||||||||||||||||||||||||||
'Entry Level', | ||||||||||||||||||||||||||||||
'Mid Level', | ||||||||||||||||||||||||||||||
'Senior Level', | ||||||||||||||||||||||||||||||
'Lead', | ||||||||||||||||||||||||||||||
'Manager', | ||||||||||||||||||||||||||||||
]; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||
<div className="space-y-6"> | ||||||||||||||||||||||||||||||
<div> | ||||||||||||||||||||||||||||||
<div className="flex items-center mb-4"> | ||||||||||||||||||||||||||||||
<Filter className="h-5 w-5 text-gray-500 mr-2" /> | ||||||||||||||||||||||||||||||
<h2 className="text-lg font-medium text-black">Filters</h2> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
<div className="space-y-6"> | ||||||||||||||||||||||||||||||
<div> | ||||||||||||||||||||||||||||||
<h3 className="text-sm font-medium text-black mb-2">Job Type</h3> | ||||||||||||||||||||||||||||||
<div className="space-y-2"> | ||||||||||||||||||||||||||||||
{jobTypes.map((type) => ( | ||||||||||||||||||||||||||||||
<label key={type} className="flex items-center"> | ||||||||||||||||||||||||||||||
<input | ||||||||||||||||||||||||||||||
type="radio" | ||||||||||||||||||||||||||||||
name="jobType" | ||||||||||||||||||||||||||||||
value={type} | ||||||||||||||||||||||||||||||
checked={selectedJobType === type} | ||||||||||||||||||||||||||||||
onChange={(e) => setSelectedJobType(e.target.value)} | ||||||||||||||||||||||||||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" | ||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||
<span className="ml-2 text-sm text-black">{type}</span> | ||||||||||||||||||||||||||||||
</label> | ||||||||||||||||||||||||||||||
))} | ||||||||||||||||||||||||||||||
{selectedJobType && ( | ||||||||||||||||||||||||||||||
<button | ||||||||||||||||||||||||||||||
onClick={() => setSelectedJobType('')} | ||||||||||||||||||||||||||||||
className="text-sm text-blue-600 hover:text-blue-500 mt-1" | ||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||
Clear | ||||||||||||||||||||||||||||||
</button> | ||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
<div> | ||||||||||||||||||||||||||||||
<h3 className="text-sm font-medium text-black mb-2">Experience</h3> | ||||||||||||||||||||||||||||||
<div className="space-y-2"> | ||||||||||||||||||||||||||||||
{experienceLevels.map((level) => ( | ||||||||||||||||||||||||||||||
<label key={level} className="flex items-center"> | ||||||||||||||||||||||||||||||
<input | ||||||||||||||||||||||||||||||
type="radio" | ||||||||||||||||||||||||||||||
name="experience" | ||||||||||||||||||||||||||||||
value={level} | ||||||||||||||||||||||||||||||
checked={selectedExperience === level} | ||||||||||||||||||||||||||||||
onChange={(e) => setSelectedExperience(e.target.value)} | ||||||||||||||||||||||||||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" | ||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||
<span className="ml-2 text-sm text-black">{level}</span> | ||||||||||||||||||||||||||||||
</label> | ||||||||||||||||||||||||||||||
))} | ||||||||||||||||||||||||||||||
{selectedExperience && ( | ||||||||||||||||||||||||||||||
<button | ||||||||||||||||||||||||||||||
onClick={() => setSelectedExperience('')} | ||||||||||||||||||||||||||||||
className="text-sm text-blue-600 hover:text-blue-500 mt-1" | ||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||
Clear | ||||||||||||||||||||||||||||||
</button> | ||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
Comment on lines
+96
to
+159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve filter groups accessibility. Wrap radio groups in fieldset and legend elements for better screen reader support. <div className="space-y-6">
- <div>
+ <fieldset>
+ <legend className="text-sm font-medium text-black mb-2">Job Type</legend>
<div className="space-y-2">
{jobTypes.map((type) => (
<label key={type} className="flex items-center">
|
||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const JobList = ({ jobs }) => { | ||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||
<div className="space-y-6"> | ||||||||||||||||||||||||||||||
<AnimatePresence> | ||||||||||||||||||||||||||||||
{jobs.map((job) => ( | ||||||||||||||||||||||||||||||
<JobItem key={job.uuid} job={job} /> | ||||||||||||||||||||||||||||||
))} | ||||||||||||||||||||||||||||||
</AnimatePresence> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const JobItem = ({ job }) => { | ||||||||||||||||||||||||||||||
const router = useRouter(); | ||||||||||||||||||||||||||||||
const gptContent = | ||||||||||||||||||||||||||||||
job.gpt_content && job.gpt_content !== 'FAILED' | ||||||||||||||||||||||||||||||
? JSON.parse(job.gpt_content) | ||||||||||||||||||||||||||||||
: {}; | ||||||||||||||||||||||||||||||
Comment on lines
+176
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for JSON parsing. The current implementation might throw an error if - const gptContent =
- job.gpt_content && job.gpt_content !== 'FAILED'
- ? JSON.parse(job.gpt_content)
- : {};
+ const gptContent = (() => {
+ try {
+ return job.gpt_content && job.gpt_content !== 'FAILED'
+ ? JSON.parse(job.gpt_content)
+ : {};
+ } catch (error) {
+ console.error('Failed to parse job GPT content:', error);
+ return {};
+ }
+ })(); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Extract and format location | ||||||||||||||||||||||||||||||
const location = gptContent.location || {}; | ||||||||||||||||||||||||||||||
const locationString = [location.city, location.region, location.countryCode] | ||||||||||||||||||||||||||||||
.filter(Boolean) | ||||||||||||||||||||||||||||||
.join(', '); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Format salary if available | ||||||||||||||||||||||||||||||
const salary = gptContent.salary | ||||||||||||||||||||||||||||||
? `$${Number(gptContent.salary).toLocaleString()}/year` | ||||||||||||||||||||||||||||||
: 'Not specified'; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const handleClick = () => { | ||||||||||||||||||||||||||||||
router.push(`/jobs/${job.uuid}`); | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||
<motion.div | ||||||||||||||||||||||||||||||
layout | ||||||||||||||||||||||||||||||
initial={{ opacity: 0, y: 50 }} | ||||||||||||||||||||||||||||||
animate={{ opacity: 1, y: 0 }} | ||||||||||||||||||||||||||||||
exit={{ opacity: 0, y: -50 }} | ||||||||||||||||||||||||||||||
transition={{ duration: 0.3 }} | ||||||||||||||||||||||||||||||
className="bg-white p-6 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-shadow" | ||||||||||||||||||||||||||||||
onClick={handleClick} | ||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||
<div className="flex justify-between items-start mb-4"> | ||||||||||||||||||||||||||||||
<div> | ||||||||||||||||||||||||||||||
<h3 className="text-2xl font-bold text-black mb-2"> | ||||||||||||||||||||||||||||||
{gptContent.title || 'Untitled Position'} | ||||||||||||||||||||||||||||||
</h3> | ||||||||||||||||||||||||||||||
<div className="flex items-center text-black mb-2"> | ||||||||||||||||||||||||||||||
<Briefcase className="mr-2" size={16} /> | ||||||||||||||||||||||||||||||
<span>{gptContent.company || 'Company not specified'}</span> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
{locationString && ( | ||||||||||||||||||||||||||||||
<div className="flex items-center text-black"> | ||||||||||||||||||||||||||||||
<MapPin className="mr-2" size={16} /> | ||||||||||||||||||||||||||||||
<span>{locationString}</span> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
<div className="flex flex-col items-end"> | ||||||||||||||||||||||||||||||
<div className="text-black mb-2"> | ||||||||||||||||||||||||||||||
<DollarSign className="inline mr-1" size={16} /> | ||||||||||||||||||||||||||||||
{salary} | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
{gptContent.type && ( | ||||||||||||||||||||||||||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> | ||||||||||||||||||||||||||||||
{gptContent.type} | ||||||||||||||||||||||||||||||
</span> | ||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||
{gptContent.description && ( | ||||||||||||||||||||||||||||||
<p className="text-black line-clamp-3">{gptContent.description}</p> | ||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||
</motion.div> | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
Comment on lines
+174
to
+239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add PropTypes validation. Add PropTypes to validate the job object structure and prevent runtime errors. Add at the top of the file: import PropTypes from 'prop-types';
// ... component code ...
JobItem.propTypes = {
job: PropTypes.shape({
uuid: PropTypes.string.isRequired,
gpt_content: PropTypes.string,
}).isRequired,
}; |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
export default ClientJobBoard; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null check before filtering jobs
The filter operation could throw if
jobs
is null/undefined. Consider adding a default empty array.📝 Committable suggestion