Skip to content
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

Merged
merged 14 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/registry/app/[username]/jobs/JobList.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ const JobDescription = ({ job, makeCoverletter }) => {
};

const JobList = ({ jobs, makeCoverletter }) => {
const fullJobs = jobs?.map((job) => {
const validJobs = jobs?.filter(
(job) => job.gpt_content && job.gpt_content !== 'FAILED'
);
Comment on lines +176 to +178
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null check before filtering jobs

The filter operation could throw if jobs is null/undefined. Consider adding a default empty array.

- const validJobs = jobs?.filter(
+ const validJobs = (jobs || []).filter(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const validJobs = jobs?.filter(
(job) => job.gpt_content && job.gpt_content !== 'FAILED'
);
const validJobs = (jobs || []).filter(
(job) => job.gpt_content && job.gpt_content !== 'FAILED'
);

const fullJobs = validJobs?.map((job) => {
const fullJob = JSON.parse(job.gpt_content);
fullJob.raw = job;
return fullJob;
Expand Down
35 changes: 35 additions & 0 deletions apps/registry/app/api/jobs/[uuid]/route.js
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security: Remove hardcoded database URL and add environment variable validation

  1. The Supabase URL should not be hardcoded as it appears to be a production credential.
  2. The SUPABASE_KEY environment variable should be validated before use.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co';
const supabaseKey = process.env.SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
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);


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
Copy link
Contributor

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function GET(request, { params }) {
try {
const { data: job, error } = await supabase
.from('jobs')
.select('*')
.eq('uuid', params.uuid)
.single();
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', uuid)
.single();


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 }
);
}
}
241 changes: 241 additions & 0 deletions apps/registry/app/jobs/ClientJobBoard.js
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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [loading] = useState(false);


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
Copy link
Contributor

Choose a reason for hiding this comment

The 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">

Committable suggestion skipped: line range outside the PR's diff.

};

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for JSON parsing.

The current implementation might throw an error if gptContent is malformed JSON.

-  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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 {};
}
})();


// 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
Loading
Loading