- Project Overview
- Technology Stack
- Project Architecture
- Setup and Installation
- Core Concepts
- Component Structure
- State Management
- API Integration
- Styling and Design System
- Routing
- Performance Optimization
- Best Practices
- Future Enhancements
Career Cruise is a modern job search platform built with React, TypeScript, and Vite. The application allows users to search, filter, and browse job listings from various companies using the JSearch API (RapidAPI).
- π Job Search: Search jobs by title, keyword, and location
- π Category Filtering: Filter jobs by categories (All, Design, Engineering, Marketing)
- π Pagination: Efficient pagination with 10 jobs per page
- π¨ Modern UI: Clean, responsive design using Tailwind CSS and Shadcn UI
- β‘ Fast Performance: Built with Vite for lightning-fast development and builds
- π Smooth UX: Smooth scrolling and loading states for better user experience
- π± Responsive Design: Works seamlessly across desktop, tablet, and mobile devices
-
React 18.2.0: Modern JavaScript library for building user interfaces
- Hooks-based architecture (useState, useEffect, useRef)
- Functional components
- Component composition pattern
-
TypeScript 5.0.2: Strongly-typed superset of JavaScript
- Type safety
- Better IDE support
- Improved code maintainability
- Vite 7.3.1: Next-generation frontend tooling
- Hot Module Replacement (HMR)
- Lightning-fast builds
- Optimized production bundles
- ES modules native support
- React Router DOM 6.11.2: Client-side routing
- Declarative routing
- Nested routes support
- Navigation components
- Axios 1.7.9: Promise-based HTTP client
- Request/response interceptors
- Automatic JSON transformation
- Better error handling than fetch
-
Tailwind CSS 3.4.17: Utility-first CSS framework
- Responsive design utilities
- Custom color palette
- Animation utilities
-
Radix UI: Headless UI components
@radix-ui/react-slot: Composition utilities@radix-ui/react-tabs: Tab components@radix-ui/react-toast: Toast notifications
-
Shadcn UI: Re-usable component library built on Radix UI
- Badge, Button, Card, Input components
- Pagination components
- Fully customizable
-
Lucide React 0.221.0: Beautiful icon library
- Tree-shakeable icons
- Customizable size and color
-
React Icons 5.4.0: Additional icon library
- clsx 2.1.1: Utility for constructing className strings
- tailwind-merge 2.6.0: Merge Tailwind CSS classes without conflicts
- class-variance-authority 0.7.1: Type-safe variant styling
- Vercel Analytics 1.4.1: Web analytics for performance monitoring
- ESLint 8.38.0: Code linting
- TypeScript ESLint: TypeScript-specific linting rules
- PostCSS & Autoprefixer: CSS processing
career_cruise/
βββ src/
β βββ components/ # Reusable UI components
β β βββ ui/ # Shadcn UI base components
β β β βββ badge.tsx
β β β βββ button.tsx
β β β βββ card.tsx
β β β βββ input.tsx
β β β βββ pagination.tsx
β β β βββ tabs.tsx
β β β βββ toast.tsx
β β β βββ toaster.tsx
β β βββ company.tsx # Company logos carousel
β β βββ Header.tsx # Navigation header
β β βββ jobCard.tsx # Job listing card
β β βββ jobcardskeleton.tsx # Loading skeleton
β β βββ Pagination.tsx # Pagination controls
β β βββ search-form.tsx # Job search form
β β βββ theme-provider.tsx # Theme context
β βββ pages/ # Page components
β β βββ Home.tsx # Main landing page
β β βββ Jobs.tsx # Jobs listing page
β β βββ Map.tsx # Jobs map view
β βββ lib/ # Utility functions
β β βββ api.ts # API configuration
β β βββ utils.ts # Helper functions
β βββ types/ # TypeScript type definitions
β β βββ index.ts # Job and API types
β βββ styles/ # Static assets
β β βββ company-placeholder.png
β βββ App.tsx # Root component
β βββ main.tsx # Application entry point
β βββ index.css # Global styles
β βββ vite-env.d.ts # Vite type definitions
βββ components.json # Shadcn UI configuration
βββ eslint.config.js # ESLint configuration
βββ postcss.config.js # PostCSS configuration
βββ tailwind.config.js # Tailwind CSS configuration
βββ tsconfig.json # TypeScript configuration
βββ vite.config.ts # Vite configuration
βββ package.json # Project dependencies
The application follows a component-based architecture where the UI is divided into reusable, self-contained components.
- Container Components (e.g.,
Home.tsx): Handle data fetching and state management - Presentational Components (e.g.,
JobCard.tsx): Focus on how things look
Components are composed together to build complex UIs from simpler building blocks.
- Node.js (v16 or higher)
- npm or yarn package manager
- Git
- Clone the repository
git clone https://github.com/SoufianeMouajjeh/career_cruise.git
cd career_cruise- Install dependencies
npm install- Set up environment variables
Create a
.envfile in the root directory:
VITE_RAPIDAPI_KEY=your_rapidapi_key_here- Run development server
npm run devThe application will be available at http://localhost:5173
- Build for production
npm run build- Preview production build
npm run previewnpm run dev: Start development server with HMRnpm run build: Build for production (TypeScript compilation + Vite build)npm run lint: Run ESLint to check code qualitynpm run preview: Preview production build locally
Manages component state:
const [jobs, setJobs] = useState<Job[]>([])
const [isLoading, setIsLoading] = useState(true)
const [currentPage, setCurrentPage] = useState(1)
const [activeTab, setActiveTab] = useState('all')Handles side effects (API calls, subscriptions):
useEffect(() => {
const fetchJobs = async () => {
// Fetch jobs when activeTab or currentPage changes
}
fetchJobs()
}, [activeTab, currentPage])Creates mutable references that persist across renders:
const jobsSectionRef = useRef<HTMLDivElement>(null)
// Used for smooth scrolling to jobs sectionexport interface Job {
job_id: string;
employer_name: string;
employer_logo: string;
job_title: string;
job_location: string;
job_employment_type: string[];
job_apply_link: string;
job_description: string;
job_posted_at_timestamp: number;
job_posted_at: string;
job_min_salary?: string;
Qualifications: string[];
Responsibilities: string[];
}Benefits:
- Type safety at compile time
- Better IDE autocomplete
- Self-documenting code
- Reduced runtime errors
Modern JavaScript for handling asynchronous operations:
const fetchJobs = async () => {
try {
const response = await axios.request(options)
setJobs(response.data.data)
} catch (error) {
console.error('Error fetching jobs:', error)
} finally {
setIsLoading(false)
}
}Purpose: Main page that displays job listings with search and filtering capabilities.
State Management:
jobs: Array of job listingsisLoading: Loading state for API callserror: Error message if API failsactiveTab: Current filter categorycurrentPage: Current pagination pagetotalPages: Total number of pagesjobsSectionRef: Reference for smooth scrolling
Key Features:
- Job Search Form
- Category Tabs (All, Design, Engineering, Marketing)
- Job Cards Grid
- Pagination Controls
- Loading Skeletons
- Error Handling
Data Flow:
User Action β State Update β useEffect Trigger β API Call β State Update β UI Re-render
Purpose: Display individual job listing with company info, location, and apply button.
Props:
{ job: Job }Features:
- Company logo with fallback SVG
- Job title and company name
- Location and employment type badges
- Salary information
- Posted date
- Apply button with external link
- Hover effects for better UX
Design Patterns:
- Error handling for missing images
- Conditional rendering for optional data
- Lucide icons for visual enhancement
Purpose: Search interface for filtering jobs by keyword and location.
Props:
{ onSearch: (query: string, country: string) => void }State:
query: Search keywordcountry: Location filter
Features:
- Controlled form inputs
- Form submission handling
- Search and location icons
- Responsive design
Purpose: Navigate between pages of job listings.
Props:
{
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
className?: string;
}Features:
- Previous/Next buttons
- Page number buttons
- Ellipsis for skipped pages
- Active page highlighting
- Disabled state for edge cases
Logic:
// Show pages: 1, ..., current-1, current, current+1, ..., last
const pageNumbers = []
for (let i = 1; i <= totalPages; i++) {
if (
i === 1 ||
i === totalPages ||
(i >= currentPage - 1 && i <= currentPage + 1)
) {
pageNumbers.push(i)
} else if (
(i === currentPage - 2 && currentPage > 3) ||
(i === currentPage + 2 && currentPage < totalPages - 2)
) {
pageNumbers.push(null) // Ellipsis
}
}Purpose: Loading placeholder while fetching jobs.
Features:
- Shimmer animation effect
- Mimics JobCard layout
- Provides visual feedback during loading
Purpose: Infinite scrolling carousel of company logos.
Features:
- Infinite scroll animation (CSS)
- Duplicated list for seamless loop
- Gradient mask for smooth edges
CSS Animation:
@keyframes infinite-scroll {
from { transform: translateX(0) }
to { transform: translateX(-100%) }
}Purpose: Application navigation bar.
Features:
- Logo and brand name
- Navigation links (Jobs, Post a Job)
- Responsive layout
- React Router integration
Reusable button component with variants:
- Primary: Purple background (#7047EB)
- Secondary: Outline style
- Ghost: Transparent background
Container component for content:
- CardContent: Main content area
- Hover effects
- Border styling
Small label for status/category:
- Different color variants
- Rounded corners
- Typography scaling
Form input component:
- Custom placeholder color
- Border styling
- Focus states
Tab navigation component:
- TabsList: Container for tabs
- TabsTrigger: Individual tab button
- Active state styling
The application uses React's built-in state management (useState) for simplicity.
βββββββββββββββββββββββββββββββββββββββ
β Initial State β
β - jobs: [] β
β - isLoading: true β
β - currentPage: 1 β
β - activeTab: 'all' β
βββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββ
β useEffect Triggered β
β Dependencies: [activeTab, page] β
βββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββ
β API Call (Axios) β
β - Fetch jobs from JSearch API β
β - Include query parameters β
βββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββ
β State Update β
β - setJobs(data) β
β - setIsLoading(false) β
β - setTotalPages(10) β
βββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββ
β UI Re-render β
β - Display job cards β
β - Show pagination β
βββββββββββββββββββββββββββββββββββββββ
const fetchJobs = async () => {
setIsLoading(true) // Start loading
try {
const response = await axios.request(options)
setJobs(response.data.data) // Success
} catch (error) {
setError('Failed to fetch jobs') // Error
} finally {
setIsLoading(false) // Always stop loading
}
}const handlePageChange = (page: number) => {
setCurrentPage(page) // Update state
jobsSectionRef.current?.scrollIntoView({ // Side effect
behavior: 'smooth',
block: 'start'
})
}// Reset page when tab changes
useEffect(() => {
setCurrentPage(1)
}, [activeTab])const API_CONFIG = {
url: 'https://jsearch.p.rapidapi.com/search',
headers: {
'x-rapidapi-key': 'YOUR_API_KEY',
'x-rapidapi-host': 'jsearch.p.rapidapi.com'
}
}params: {
query: string, // Search term (e.g., 'developer', 'design')
page: string, // Current page number
num_pages: string, // Number of pages to fetch (set to '1')
country: string, // Country code (e.g., 'us')
date_posted: string // Filter by date ('all', 'today', 'week', 'month')
}{
status: "OK",
data: [
{
job_id: "abc123",
employer_name: "Tech Company",
employer_logo: "https://...",
job_title: "Senior Developer",
job_location: "New York, NY",
job_employment_type: ["FULLTIME"],
job_apply_link: "https://...",
job_description: "...",
job_posted_at: "2 days ago",
job_min_salary: "$120,000",
// ... more fields
}
]
}useEffect(() => {
const fetchJobs = async () => {
try {
setIsLoading(true)
setError(null)
const response = await axios.request({
method: 'GET',
url: 'https://jsearch.p.rapidapi.com/search',
params: {
query: activeTab === 'all' ? 'developer' : activeTab,
page: currentPage.toString(),
num_pages: '1',
country: 'us',
date_posted: 'all'
},
headers: {
'x-rapidapi-key': API_KEY,
'x-rapidapi-host': 'jsearch.p.rapidapi.com'
}
})
const jobsData = response?.data?.data || []
setJobs(Array.isArray(jobsData) ? jobsData.slice(0, 10) : [])
setTotalPages(10)
} catch (error) {
console.error('Error fetching jobs:', error)
setError('Failed to fetch jobs. Please try again later.')
setJobs([])
} finally {
setIsLoading(false)
}
}
fetchJobs()
}, [activeTab, currentPage])const handleSearch = async (query: string, country: string) => {
try {
setIsLoading(true)
setError(null)
setCurrentPage(1)
const response = await axios.request({
method: 'GET',
url: 'https://jsearch.p.rapidapi.com/search',
params: {
query,
country,
page: '1',
num_pages: '1',
date_posted: 'all'
},
headers: {
'x-rapidapi-key': API_KEY,
'x-rapidapi-host': 'jsearch.p.rapidapi.com'
}
})
const jobsData = response?.data?.data || []
setJobs(Array.isArray(jobsData) ? jobsData.slice(0, 10) : [])
setTotalPages(10)
} catch (error) {
console.error('Error fetching jobs:', error)
setError('Failed to fetch jobs. Please try again later.')
setJobs([])
} finally {
setIsLoading(false)
}
}- Network Errors: No internet connection
- API Errors: Invalid API key, rate limiting
- Data Errors: Malformed response data
try {
// API call
} catch (error) {
console.error('Error fetching jobs:', error)
setError('Failed to fetch jobs. Please try again later.')
setJobs([]) // Reset to empty array
} finally {
setIsLoading(false) // Always stop loading
}theme: {
extend: {
colors: {
customGray: '#b0b0b0',
// Using CSS variables for theme support
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
animation: {
'infinite-scroll': 'infinite-scroll 40s linear infinite'
},
keyframes: {
'infinite-scroll': {
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(-100%)' }
}
}
}
}- Primary Purple:
#7047EB- Used for: Buttons, active states, brand elements
- Hover state:
#402591
- Gray:
#b0b0b0- Used for: Placeholders, muted text
Mobile-first approach using Tailwind breakpoints:
<h1 className="text-4xl sm:text-5xl md:text-6xl">
Find Your Dream Job
</h1>Breakpoints:
sm: 640pxmd: 768pxlg: 1024pxxl: 1280px2xl: 1536px
<Card className="group hover:shadow-lg transition-all duration-300 hover:border-[#7047EB]">
<h3 className="group-hover:text-primary transition-colors">
{job_title}
</h3>
</Card>{isLoading ? (
<JobCardSkeleton />
) : (
<JobCard job={job} />
)}<section className="bg-gradient-to-b from-background to-secondary/20">
{/* Content */}
</section>Most styling is done with Tailwind utility classes:
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
Click Me
</button>Using class-variance-authority for variant management:
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border border-input bg-background"
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3",
lg: "h-11 px-8"
}
}
}
)Global CSS variables for theming:
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 262 83% 58%;
--radius: 0.5rem;
}import { BrowserRouter } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)function App() {
return (
<div className="min-h-screen flex flex-col">
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/jobs" element={<Jobs />} />
<Route path="/map" element={<Map />} />
</Routes>
<Analytics />
</div>
)
}<Link to="/" className="text-sm hover:text-[#7047EB]">
Jobs
</Link>import { useNavigate } from 'react-router-dom'
const navigate = useNavigate()
navigate('/jobs')Problem: Loading all jobs at once is slow and wasteful.
Solution: Fetch only 10 jobs per page.
const jobsPerPage = 10
params: {
page: currentPage.toString(),
num_pages: '1' // Fetch only 1 page at a time
}Benefits:
- Faster initial load
- Reduced API bandwidth
- Better user experience
<img
src={employer_logo}
alt={employer_name}
loading="lazy"
onError={handleImageError}
/>Prevent excessive API calls during typing:
const debounce = (func: Function, delay: number) => {
let timeoutId: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func(...args), delay)
}
}Optimize list rendering:
{jobs.map((job: Job) => (
<JobCard key={job.job_id} job={job} />
))}// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@radix-ui/react-slot', '@radix-ui/react-tabs']
}
}
}
}
})// Lazy load pages
const Home = lazy(() => import('./pages/Home'))
const Jobs = lazy(() => import('./pages/Jobs'))
const Map = lazy(() => import('./pages/Map'))import { useMemo } from 'react'
const filteredJobs = useMemo(() => {
return jobs.filter(job => job.job_location.includes(location))
}, [jobs, location])- β Define interfaces for all data structures
- β Use type annotations for function parameters
- β
Avoid
anytype - β Use strict mode
- β Keep components small and focused
- β Use functional components with hooks
- β Extract reusable logic into custom hooks
- β Separate concerns (UI vs. logic)
- β Keep state as close to where it's used as possible
- β Lift state up when needed by multiple components
- β Use useEffect dependencies carefully
- β Clean up side effects
- β Always handle API errors gracefully
- β Provide user-friendly error messages
- β Use try-catch blocks
- β Reset state on errors
- β Use semantic HTML elements
- β Provide alt text for images
- β Ensure keyboard navigation works
- β Use ARIA labels where needed
- β Implement pagination
- β Use lazy loading for images
- β Avoid unnecessary re-renders
- β Optimize bundle size
- β Group related files together
- β Use consistent naming conventions
- β Keep files under 300 lines
- β Comment complex logic
- Salary range filter
- Experience level filter
- Remote/Hybrid/On-site filter
- Company size filter
- Benefits filter
- User registration and login
- Save favorite jobs
- Job application tracking
- Profile management
- Full job description
- Company profile
- Similar jobs section
- Apply directly through platform
- Visualize jobs on a map
- Filter by geographic area
- Cluster jobs by location
- Nearby jobs feature
- Job alert subscriptions
- Daily/weekly digest
- Custom search alerts
- New job notifications
- Company information pages
- All jobs from company
- Company reviews
- Company culture info
- Create and edit resumes
- Upload existing resumes
- Resume templates
- ATS-friendly formatting
- Job market trends
- Salary insights
- Popular skills
- Industry statistics
- React Native version
- Push notifications
- Offline support
- Native features
- Job recommendations
- Resume matching
- Cover letter generator
- Interview prep
Problem: "Unauthorized" or "403 Forbidden" errors
Solution:
- Verify API key is correct
- Check API key hasn't expired
- Ensure API key has proper permissions
Problem: "Port 5173 is already in use"
Solution:
# Kill the process using the port
lsof -ti:5173 | xargs kill -9
# Or let Vite use another port automaticallyProblem: "Cannot find module '@/components/...'"
Solution:
- Verify
tsconfig.jsonhas correct path aliases - Restart TypeScript server
- Check
vite.config.tsresolve aliases
Problem: Page loads slowly
Solution:
- Implement pagination (already done)
- Enable caching
- Optimize images
- Reduce bundle size
- React Documentation
- TypeScript Documentation
- Vite Documentation
- Tailwind CSS Documentation
- Shadcn UI Documentation
- React Router Documentation
- Axios Documentation
- β Initial project setup with Vite + React + TypeScript
- β JSearch API integration
- β Job search and filtering
- β Category tabs
- β Pagination (10 jobs per page)
- β Smooth scrolling UX
- β Responsive design
- β Loading states and skeletons
- β Error handling
- β Company carousel
- β Vercel Analytics integration
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Follow ESLint rules
- Write meaningful commit messages
- Add comments for complex logic
- Update documentation
- Test thoroughly before submitting
This project is licensed under the MIT License.
- JSearch API for providing job data
- Shadcn UI for beautiful UI components
- Lucide for icon library
- Vercel for hosting and analytics
Project Repository: https://github.com/SoufianeMouajjeh/career_cruise
Developer: Soufiane Mouajjeh
Last Updated: January 10, 2026