From 94ca344842612881447e7572f32be6ac921b28cd Mon Sep 17 00:00:00 2001 From: DnN04 Date: Thu, 2 Jul 2026 19:47:04 +0530 Subject: [PATCH 1/6] fix: responsive testimonial slider with navigation controls (#308) --- frontend/src/LandingPage.jsx | 120 ++++++++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 8 deletions(-) diff --git a/frontend/src/LandingPage.jsx b/frontend/src/LandingPage.jsx index d95cd8a..b3aa246 100644 --- a/frontend/src/LandingPage.jsx +++ b/frontend/src/LandingPage.jsx @@ -17,7 +17,7 @@ import SignUp from "./pages/Auth/SignUp"; import { UserContext } from "./context/userContext"; import { motion, AnimatePresence } from "framer-motion"; import ServicesMarquee from "./components/ServicesMarquee"; -import { Star } from "lucide-react"; // Import Star icon for testimonials +import { Star, ChevronLeft, ChevronRight } from "lucide-react"; // Import icons for testimonials import TermsandConditions from "./pages/Terms/TermsandConditions"; // ← Add this @@ -208,7 +208,7 @@ const StarRating = ({ count }) => ( Testimonial Card Component ───────────────────────────────────────────── */ const TestimonialCard = ({ testimonial }) => ( -
+
@@ -243,6 +243,60 @@ const LandingPage = () => { const [activeStep, setActiveStep] = useState(1); const [showScrollTop, setShowScrollTop] = useState(false); + const [visibleCards, setVisibleCards] = useState(3); + const [currentIndex, setCurrentIndex] = useState(0); + const [isPaused, setIsPaused] = useState(false); + + useEffect(() => { + const handleResize = () => { + if (window.innerWidth < 640) { + setVisibleCards(1); + } else if (window.innerWidth < 1024) { + setVisibleCards(2); + } else { + setVisibleCards(3); + } + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + useEffect(() => { + const maxIndex = Math.max(0, TESTIMONIALS.length - visibleCards); + if (currentIndex > maxIndex) { + setCurrentIndex(maxIndex); + } + }, [visibleCards, currentIndex]); + + useEffect(() => { + if (isPaused) return; + const interval = setInterval(() => { + setCurrentIndex((prevIndex) => { + const nextIndex = prevIndex + 1; + const maxIndex = TESTIMONIALS.length - visibleCards; + return nextIndex > maxIndex ? 0 : nextIndex; + }); + }, 4000); + return () => clearInterval(interval); + }, [visibleCards, isPaused]); + + const handlePrev = () => { + setCurrentIndex((prevIndex) => { + const nextIndex = prevIndex - 1; + const maxIndex = TESTIMONIALS.length - visibleCards; + return nextIndex < 0 ? maxIndex : nextIndex; + }); + }; + + const handleNext = () => { + setCurrentIndex((prevIndex) => { + const nextIndex = prevIndex + 1; + const maxIndex = TESTIMONIALS.length - visibleCards; + return nextIndex > maxIndex ? 0 : nextIndex; + }); + }; + const handleCTA = () => { if (!user) { setOpenAuthModal(true); @@ -783,16 +837,66 @@ const LandingPage = () => { {/* Testimonials Scroller */} -
+
setIsPaused(true)} + onMouseLeave={() => setIsPaused(false)} + > {/* Fading overlays */}
-
- {/* Duplicate testimonials for seamless loop */} - {[...TESTIMONIALS, ...TESTIMONIALS].map((testimonial, index) => ( - - ))} + {/* Slider Track Wrapper */} +
+
+ {TESTIMONIALS.map((testimonial) => ( +
+ +
+ ))} +
+
+ + {/* Navigation Arrows and Dot Indicators */} +
+ + + {/* Dots indicators */} +
+ {Array.from({ length: TESTIMONIALS.length - visibleCards + 1 }).map((_, idx) => ( +
+ +
From 2ed4097955ff589b78def01687c867b4499861c0 Mon Sep 17 00:00:00 2001 From: tanishkkkkk Date: Thu, 2 Jul 2026 20:28:35 +0530 Subject: [PATCH 2/6] fix: resolve unresponsive close button on auth modal in responsive views --- frontend/src/components/Loader/Modal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Loader/Modal.jsx b/frontend/src/components/Loader/Modal.jsx index 318606f..633785e 100644 --- a/frontend/src/components/Loader/Modal.jsx +++ b/frontend/src/components/Loader/Modal.jsx @@ -50,7 +50,7 @@ const Modal = ({ children, isOpen, onClose, title, hideHeader }) => { type="button" aria-label="Close authentication modal" title="Close" - className="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white bg-gray-50 dark:bg-white/5 hover:bg-gray-100 dark:hover:bg-white/10 rounded-full p-1.5 flex items-center justify-center absolute top-4 right-4 transition-all duration-200 z-10" + className="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white bg-gray-50 dark:bg-white/5 hover:bg-gray-100 dark:hover:bg-white/10 rounded-full w-11 h-11 flex items-center justify-center absolute top-4 right-4 transition-all duration-200 z-50" onClick={onClose} > ✕ From 8d4e77284283dd89d97d1b57a37e0d1cd59405b8 Mon Sep 17 00:00:00 2001 From: GitGuru-sudo Date: Fri, 3 Jul 2026 12:04:00 +0530 Subject: [PATCH 3/6] feat: add Jobs for You section with Adzuna API integration --- backend/.env.example | 7 + backend/config/validateEnv.js | 2 + backend/controllers/jobController.js | 85 +++++++++ backend/models/JobCache.js | 21 +++ backend/routes/jobRoutes.js | 8 + backend/server.js | 7 + frontend/src/App.jsx | 11 ++ frontend/src/components/Layouts/Sidebar.jsx | 14 ++ frontend/src/pages/Jobs/JobsForYou.jsx | 184 ++++++++++++++++++++ frontend/src/utils/apiPaths.js | 5 +- 10 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 backend/controllers/jobController.js create mode 100644 backend/models/JobCache.js create mode 100644 backend/routes/jobRoutes.js create mode 100644 frontend/src/pages/Jobs/JobsForYou.jsx diff --git a/backend/.env.example b/backend/.env.example index 473f977..c4fd6a8 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -54,3 +54,10 @@ FRONTEND_URL=http://localhost:5173 # NODE_ENV=production (for production deployments) # NODE_ENV=development (default for local) NODE_ENV=development + +# Adzuna Job API (free tier — register at https://developer.adzuna.com) +# No credit card required. Get App ID + API Key after signing up. +ADZUNA_APP_ID=your_adzuna_app_id +ADZUNA_API_KEY=your_adzuna_api_key +# Country code for job listings: "in"=India, "us"=USA, "gb"=UK, "au"=Australia etc. +ADZUNA_COUNTRY=in diff --git a/backend/config/validateEnv.js b/backend/config/validateEnv.js index 418ed8d..afeae5d 100644 --- a/backend/config/validateEnv.js +++ b/backend/config/validateEnv.js @@ -2,6 +2,8 @@ const requiredEnvVars = Object.freeze([ "MONGO_URI", "JWT_SECRET", "GEMINI_API_KEY", + "ADZUNA_APP_ID", + "ADZUNA_API_KEY", ]); const validateEnv = () => { diff --git a/backend/controllers/jobController.js b/backend/controllers/jobController.js new file mode 100644 index 0000000..c19a55d --- /dev/null +++ b/backend/controllers/jobController.js @@ -0,0 +1,85 @@ +const axios = require("axios"); +const Session = require("../models/Session"); +const JobCache = require("../models/JobCache"); + +const ADZUNA_APP_ID = process.env.ADZUNA_APP_ID; +const ADZUNA_API_KEY = process.env.ADZUNA_API_KEY; +const ADZUNA_COUNTRY = process.env.ADZUNA_COUNTRY || "in"; +const CACHE_TTL_MS = 24 * 60 * 60 * 1000; + +async function fetchFromAdzuna(role, country = ADZUNA_COUNTRY) { + const url = `https://api.adzuna.com/v1/api/jobs/${country}/search/1`; + const { data } = await axios.get(url, { + params: { + app_id: ADZUNA_APP_ID, + app_key: ADZUNA_API_KEY, + what: role, + results_per_page: 10, + content_type: "application/json", + }, + }); + return (data.results || []).map((j) => ({ + id: j.id, + title: j.title, + company: j.company?.display_name || "Unknown", + location: j.location?.display_name || "Remote", + salary_min: j.salary_min || null, + salary_max: j.salary_max || null, + description: j.description || "", + redirect_url: j.redirect_url, + created: j.created, + })); +} + +exports.getJobs = async (req, res) => { + try { + const userId = req.user._id; + + const latestSession = await Session.findOne({ user: userId }) + .sort({ createdAt: -1 }) + .select("role"); + + const role = latestSession?.role || req.query.role || "software engineer"; + const country = req.query.country || ADZUNA_COUNTRY; + const cacheKey = `${role.toLowerCase()}|${country}`; + + const cached = await JobCache.findOne({ cacheKey }); + if (cached && Date.now() - cached.fetchedAt.getTime() < CACHE_TTL_MS) { + return res.json({ jobs: cached.jobs, role, source: "cache" }); + } + + const jobs = await fetchFromAdzuna(role, country); + + await JobCache.findOneAndUpdate( + { cacheKey }, + { jobs, fetchedAt: new Date() }, + { upsert: true, new: true } + ); + + return res.json({ jobs, role, source: "api" }); + } catch (err) { + console.error("[Jobs] getJobs error:", err.message); + res.status(500).json({ message: "Failed to fetch jobs", error: err.message }); + } +}; + +exports.refreshJobCache = async () => { + try { + const roles = await Session.distinct("role", { + createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }, + }); + + for (const role of roles) { + const cacheKey = `${role.toLowerCase()}|${ADZUNA_COUNTRY}`; + const jobs = await fetchFromAdzuna(role); + await JobCache.findOneAndUpdate( + { cacheKey }, + { jobs, fetchedAt: new Date() }, + { upsert: true, new: true } + ); + console.log(`[JobCron] Refreshed cache for: ${role}`); + } + } catch (err) { + console.error("[JobCron] Refresh failed:", err.message); + } +}; diff --git a/backend/models/JobCache.js b/backend/models/JobCache.js new file mode 100644 index 0000000..6c04b24 --- /dev/null +++ b/backend/models/JobCache.js @@ -0,0 +1,21 @@ +const mongoose = require("mongoose"); + +const JobCacheSchema = new mongoose.Schema({ + cacheKey: { type: String, required: true, unique: true }, + jobs: [ + { + id: String, + title: String, + company: String, + location: String, + salary_min: Number, + salary_max: Number, + description: String, + redirect_url: String, + created: String, + }, + ], + fetchedAt: { type: Date, default: Date.now }, +}); + +module.exports = mongoose.model("JobCache", JobCacheSchema); diff --git a/backend/routes/jobRoutes.js b/backend/routes/jobRoutes.js new file mode 100644 index 0000000..b2c1f52 --- /dev/null +++ b/backend/routes/jobRoutes.js @@ -0,0 +1,8 @@ +const express = require("express"); +const router = express.Router(); +const { protect } = require("../middlewares/authMiddleware"); +const { getJobs } = require("../controllers/jobController"); + +router.get("/", protect, getJobs); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index 2c287c5..18a5dc6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -18,6 +18,7 @@ const questionRoutes = require("./routes/questionRoutes"); const aiRoutes = require("./routes/aiRoutes"); const resumeRoutes = require("./routes/resumeRoutes"); const aptitudeQuestionsRoutes = require("./routes/AptitudeQuestions.js"); +const jobRoutes = require("./routes/jobRoutes"); const { generalLimiter, aiLimiter } = require("./middlewares/rateLimiter"); const { generalHeaders, sensitiveRouteHeaders } = require("./middlewares/securityHeaders"); // Remove ES Module import for cors. Use CommonJS require below. @@ -129,6 +130,7 @@ app.use( ); app.use("/api/books", generalLimiter, booksRoutes); +app.use("/api/jobs", generalLimiter, jobRoutes); //Serve uploads folder app.use("/uploads", express.static(path.join(__dirname, "uploads"), {})); @@ -140,6 +142,11 @@ app.get("/api/test", (req, res) => { // Remove duplicate CORS middleware (already set above) +// Daily job cache refresh — warm on boot, then every 24 hours +const { refreshJobCache } = require("./controllers/jobController"); +refreshJobCache(); +setInterval(refreshJobCache, 24 * 60 * 60 * 1000); + // Start Server const PORT = process.env.PORT || 5000; const server = app.listen(PORT, "0.0.0.0", () => { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1896b3c..6be6be4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -34,6 +34,7 @@ import RepositoryHive from "./pages/OpenSource/RepositoryHive"; import OSSBlog from "./pages/OpenSource/OSSBlog"; import OpenSourceEvents from "./pages/OpenSource/OpenSourceEvents"; import NotesBooks from "./pages/NotesBooks/NotesBooks"; +import JobsForYou from "./pages/Jobs/JobsForYou"; import HelpSupport from "./pages/Support/HelpSupport"; import Settings from "./pages/Settings/Settings"; import NotFound from "./pages/NotFound"; @@ -314,6 +315,16 @@ const App = () => { } /> + + + + + + } + /> { }, ], }, + { + id: "jobs", + title: "Jobs", + isHeader: true, + items: [ + { + id: "jobs-for-you", + title: "Jobs for You", + path: "/jobs", + icon: BriefcaseBusiness, + }, + ], + }, { id: "project", title: "Project", diff --git a/frontend/src/pages/Jobs/JobsForYou.jsx b/frontend/src/pages/Jobs/JobsForYou.jsx new file mode 100644 index 0000000..ca8621d --- /dev/null +++ b/frontend/src/pages/Jobs/JobsForYou.jsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState, useContext } from "react"; +import { Briefcase, MapPin, DollarSign, ExternalLink, RefreshCw } from "lucide-react"; +import { UserContext } from "../../context/userContext"; +import axiosInstance from "../../utils/axiosinstance"; +import { API_PATHS } from "../../utils/apiPaths"; +import toast from "react-hot-toast"; + +const COUNTRY_OPTIONS = [ + { code: "in", label: "India" }, + { code: "us", label: "United States" }, + { code: "gb", label: "United Kingdom" }, + { code: "au", label: "Australia" }, + { code: "ca", label: "Canada" }, +]; + +const JobCard = ({ job }) => ( +
+
+
+
+

{job.title}

+

{job.company}

+
+ + New + +
+ +
+ + {job.location} + + {(job.salary_min || job.salary_max) && ( + + + {job.salary_min && job.salary_max + ? `${Math.round(job.salary_min / 1000)}k – ${Math.round(job.salary_max / 1000)}k` + : job.salary_min + ? `From ${Math.round(job.salary_min / 1000)}k` + : `Up to ${Math.round(job.salary_max / 1000)}k`} + + )} +
+ +

+ {job.description} +

+
+ + + Apply Now + +
+); + +const JobsForYou = () => { + const { user } = useContext(UserContext); + const [jobs, setJobs] = useState([]); + const [role, setRole] = useState(""); + const [country, setCountry] = useState("in"); + const [loading, setLoading] = useState(true); + const [customRole, setCustomRole] = useState(""); + + const fetchJobs = async (selectedCountry, overrideRole = "") => { + setLoading(true); + try { + const params = { country: selectedCountry }; + if (overrideRole) params.role = overrideRole; + const res = await axiosInstance.get(API_PATHS.JOBS.GET, { params }); + setJobs(res.data.jobs || []); + setRole(res.data.role || ""); + } catch (err) { + toast.error("Failed to load job listings. Please try again."); + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchJobs(country); + }, []); + + const handleCountryChange = (e) => { + setCountry(e.target.value); + fetchJobs(e.target.value, customRole); + }; + + const handleSearch = (e) => { + e.preventDefault(); + fetchJobs(country, customRole); + }; + + return ( +
+
+
+
+

+ + Jobs for You +

+

+ Real job listings matched to{" "} + {role ? ( + + {role} + + ) : ( + "your target role" + )}{" "} + — sourced from Adzuna and refreshed daily. +

+
+ +
+
+ setCustomRole(e.target.value)} + placeholder="Override role…" + className="px-3 py-2 text-sm rounded-xl border border-gray-200 dark:border-white/10 bg-white dark:bg-white/5 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-violet-500 w-40" + /> + + +
+
+
+
+ +
+ {loading ? ( +
+
+
+ ) : jobs.length > 0 ? ( + <> +

+ Showing {jobs.length} listings for{" "} + {role} +

+
+ {jobs.map((job) => ( + + ))} +
+ + ) : ( +
+
+ +
+

No listings found

+

+ No jobs matched your current role. Try a different role or country, or start a new interview session to set your target role. +

+
+ )} +
+
+ ); +}; + +export default JobsForYou; diff --git a/frontend/src/utils/apiPaths.js b/frontend/src/utils/apiPaths.js index 86e788d..e687879 100644 --- a/frontend/src/utils/apiPaths.js +++ b/frontend/src/utils/apiPaths.js @@ -40,5 +40,8 @@ export const API_PATHS = { ANALYZE: "/api/resume/analyze", // AI Resume Analyzer via Gemini SAVE: "/api/resume/save", // Save resume to backend GET_ALL: "/api/resume/my-resumes", // Get all user's saved resumes - } + }, + JOBS: { + GET: "/api/jobs", // GET /api/jobs?role=...&country=... + }, }; \ No newline at end of file From b6cfcafbeb9f322c3fde48b3b563e94c2fd469ad Mon Sep 17 00:00:00 2001 From: anandayush005 Date: Fri, 3 Jul 2026 15:44:35 +0530 Subject: [PATCH 4/6] feat: upgrade user schema to handle password hashing and validation --- backend/controllers/authController.js | 16 ++++++---------- backend/models/User.js | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index b9f6e7c..94188c2 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -46,10 +46,6 @@ const registerUser = async (req, res) => { return res.status(400).json({ success: false, message: "A user with this email already exists." }); } - // hash password - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); - // Split name into first and last names for defaults const nameParts = name.trim().split(/\s+/); const firstName = nameParts[0] || ""; @@ -62,7 +58,7 @@ const registerUser = async (req, res) => { const user = await User.create({ name, email, - password: hashedPassword, + password: password, profileImageUrl, firstName, lastName, @@ -116,7 +112,7 @@ const loginUser = async (req, res) => { } // Verify password against stored hash - const isMatch = await bcrypt.compare(password, user.password); + const isMatch = await user.isValidPassword(password); if (!isMatch) { return res.status(401).json({ success: false, message: "Invalid email or password provided." }); } @@ -146,7 +142,8 @@ const loginUser = async (req, res) => { refreshToken, }); } catch (error) { - res.status(500).json({ success: false, message: "Internal server error occurred", error: error.message }); + console.log(error) + res.status(500).json({ success: false, message: "Internal server error occurred", error }); } }; @@ -457,14 +454,13 @@ const changePassword = async (req, res) => { } // Compare original password - const isMatch = await bcrypt.compare(originalPassword, user.password); + const isMatch = await user.isValidPassword(originalPassword); if (!isMatch) { return res.status(400).json({ success: false, message: "Incorrect original password" }); } // Hash new password - const salt = await bcrypt.genSalt(10); - user.password = await bcrypt.hash(newPassword, salt); + user.password = newPassword; await user.save(); res.json({ success: true, message: "Password updated successfully" }); diff --git a/backend/models/User.js b/backend/models/User.js index eb5315c..c339d46 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -1,4 +1,5 @@ const mongoose = require("mongoose"); +const bcrypt = require("bcryptjs"); const UserSchema = new mongoose.Schema( { @@ -59,4 +60,21 @@ const UserSchema = new mongoose.Schema( { timestamps: true } ); +// Hash password automatically on save, and when there is no update in password then the password remains same and saves precious computation of hasing password + +UserSchema.pre('save', async function() { + if (!this.isModified('password')) { + return; + } + + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); +}); + +UserSchema.methods.isValidPassword = async function(candidatePassword) { + // Compare candidate password with the stored hash + return await bcrypt.compare(candidatePassword, this.password); +}; + + module.exports = mongoose.model("User", UserSchema); From b0aa0b43213e93c3c8ca9573a15c80d49918dc2f Mon Sep 17 00:00:00 2001 From: GitGuru-sudo Date: Fri, 3 Jul 2026 21:58:59 +0530 Subject: [PATCH 5/6] fix: make Adzuna Jobs feature optional and fix failing API request - Remove ADZUNA_APP_ID/ADZUNA_API_KEY from required env vars so the server boots without them (previously process.exit(1) on missing keys took down the entire backend for anyone without Adzuna credentials) - Guard getJobs and refreshJobCache with isAdzunaConfigured() so the feature degrades gracefully instead of returning 500s when unset - Gate boot-time cache cron behind key presence in server.js - Drop invalid content_type query param that caused Adzuna to reject every search request with HTTP 400 --- backend/config/validateEnv.js | 6 ++++-- backend/controllers/jobController.js | 16 +++++++++++++++- backend/server.js | 11 +++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/backend/config/validateEnv.js b/backend/config/validateEnv.js index afeae5d..7e3a77a 100644 --- a/backend/config/validateEnv.js +++ b/backend/config/validateEnv.js @@ -2,10 +2,12 @@ const requiredEnvVars = Object.freeze([ "MONGO_URI", "JWT_SECRET", "GEMINI_API_KEY", - "ADZUNA_APP_ID", - "ADZUNA_API_KEY", ]); +// Optional integrations — the server boots fine without these, but the +// dependent feature is disabled until they are provided. +// ADZUNA_APP_ID / ADZUNA_API_KEY → "Jobs for You" (see controllers/jobController.js) + const validateEnv = () => { const missingVars = []; diff --git a/backend/controllers/jobController.js b/backend/controllers/jobController.js index c19a55d..c03ce68 100644 --- a/backend/controllers/jobController.js +++ b/backend/controllers/jobController.js @@ -7,6 +7,10 @@ const ADZUNA_API_KEY = process.env.ADZUNA_API_KEY; const ADZUNA_COUNTRY = process.env.ADZUNA_COUNTRY || "in"; const CACHE_TTL_MS = 24 * 60 * 60 * 1000; +// The Jobs feature is optional: without Adzuna credentials it stays dormant +// instead of crashing the server or spamming failed API calls. +const isAdzunaConfigured = () => Boolean(ADZUNA_APP_ID && ADZUNA_API_KEY); + async function fetchFromAdzuna(role, country = ADZUNA_COUNTRY) { const url = `https://api.adzuna.com/v1/api/jobs/${country}/search/1`; const { data } = await axios.get(url, { @@ -15,7 +19,6 @@ async function fetchFromAdzuna(role, country = ADZUNA_COUNTRY) { app_key: ADZUNA_API_KEY, what: role, results_per_page: 10, - content_type: "application/json", }, }); return (data.results || []).map((j) => ({ @@ -33,6 +36,16 @@ async function fetchFromAdzuna(role, country = ADZUNA_COUNTRY) { exports.getJobs = async (req, res) => { try { + if (!isAdzunaConfigured()) { + return res.json({ + jobs: [], + role: "", + source: "disabled", + message: + "Job listings are not configured on this server. Set ADZUNA_APP_ID and ADZUNA_API_KEY to enable them.", + }); + } + const userId = req.user._id; const latestSession = await Session.findOne({ user: userId }) @@ -64,6 +77,7 @@ exports.getJobs = async (req, res) => { }; exports.refreshJobCache = async () => { + if (!isAdzunaConfigured()) return; try { const roles = await Session.distinct("role", { createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }, diff --git a/backend/server.js b/backend/server.js index 18a5dc6..d29eb43 100644 --- a/backend/server.js +++ b/backend/server.js @@ -142,10 +142,13 @@ app.get("/api/test", (req, res) => { // Remove duplicate CORS middleware (already set above) -// Daily job cache refresh — warm on boot, then every 24 hours -const { refreshJobCache } = require("./controllers/jobController"); -refreshJobCache(); -setInterval(refreshJobCache, 24 * 60 * 60 * 1000); +// Daily job cache refresh — warm on boot, then every 24 hours. +// Only runs when Adzuna is configured; otherwise refreshJobCache() no-ops. +if (process.env.ADZUNA_APP_ID && process.env.ADZUNA_API_KEY) { + const { refreshJobCache } = require("./controllers/jobController"); + refreshJobCache(); + setInterval(refreshJobCache, 24 * 60 * 60 * 1000); +} // Start Server const PORT = process.env.PORT || 5000; From cf34f4e6e1b093faddd7b001add5cedeb188766c Mon Sep 17 00:00:00 2001 From: Revati Kadam Date: Sun, 5 Jul 2026 13:29:57 +0530 Subject: [PATCH 6/6] feat: add interactive custom cursor and layout animations --- frontend/src/App.jsx | 3 +- frontend/src/components/InteractiveCursor.jsx | 86 ++++++++++++++ frontend/src/index.css | 108 ++++++++++++++++++ 3 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/InteractiveCursor.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6be6be4..32e010c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,7 +10,7 @@ import { Toaster } from "react-hot-toast"; import { AnimatePresence } from "framer-motion"; import PageTransition from "./components/animations/PageTransition"; import ErrorBoundary from "./components/ErrorBoundary"; - +import InteractiveCursor from './components/InteractiveCursor'; import Login from "./pages/Auth/Login"; import SignUp from "./pages/Auth/SignUp"; import VerifyEmail from "./pages/Auth/verifyEmail"; @@ -57,6 +57,7 @@ const App = () => {
+ diff --git a/frontend/src/components/InteractiveCursor.jsx b/frontend/src/components/InteractiveCursor.jsx new file mode 100644 index 0000000..62b5f8f --- /dev/null +++ b/frontend/src/components/InteractiveCursor.jsx @@ -0,0 +1,86 @@ +import React, { useEffect, useState } from 'react'; + +const InteractiveCursor = () => { + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [trailingPosition, setTrailingPosition] = useState({ x: 0, y: 0 }); + const [isHovered, setIsHovered] = useState(false); + const [isHidden, setIsHidden] = useState(true); + + useEffect(() => { + // Respect user preference for reduced motion + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (prefersReducedMotion) return; + + const handleMouseMove = (e) => { + setPosition({ x: e.clientX, y: e.clientY }); + setIsHidden(false); + }; + + const handleMouseLeave = () => setIsHidden(true); + const handleMouseEnter = () => setIsHidden(false); + + // Watch for hovers on interactive items + const handleMouseOver = (e) => { + if (e.target.closest('button, a, [role="button"], .topic-card, .summary-card, input, select')) { + setIsHovered(true); + } else { + setIsHovered(false); + } + }; + + window.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseleave', handleMouseLeave); + document.addEventListener('mouseenter', handleMouseEnter); + window.addEventListener('mouseover', handleMouseOver); + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseleave', handleMouseLeave); + document.removeEventListener('mouseenter', handleMouseEnter); + window.removeEventListener('mouseover', handleMouseOver); + }; + }, []); + + // Smooth trailing effect calculation + useEffect(() => { + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (prefersReducedMotion) return; + + let animationFrameId; + + const updateTrailingPosition = () => { + setTrailingPosition((prev) => { + // Linear interpolation for smooth tracking lag + const dx = position.x - prev.x; + const dy = position.y - prev.y; + return { + x: prev.x + dx * 0.15, + y: prev.y + dy * 0.15, + }; + }); + animationFrameId = requestAnimationFrame(updateTrailingPosition); + }; + + animationFrameId = requestAnimationFrame(updateTrailingPosition); + return () => cancelAnimationFrame(animationFrameId); + }, [position]); + + if (isHidden) return null; + + return ( + <> + {/* Main Sharp Dot Pointer */} +
+ {/* Smooth Trailing Glow Ring */} +
+ + ); +}; + +export default InteractiveCursor; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 2c35d10..b821c48 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -385,3 +385,111 @@ html.dark { } } +/* ========================================================================== + ✨ MODERN PREMIUM APPLICATION UI & EFFECTS UPGRADES + ========================================================================== */ + +/* 1. Global Interactive Layout Adjustments */ +@media (min-width: 769px) { + body, a, button, input, select, textarea, [role="button"] { + cursor: none !important; /* Conceals classic ugly OS cursor for desktops */ + } +} + +/* 2. Precision Custom Dot Core Pointer */ +.custom-dot-pointer { + width: 6px; + height: 6px; + background-color: var(--primary, #6b21a8); /* Tailored signature purple look */ + position: fixed; + transform: translate(-50%, -50%); + border-radius: 50%; + pointer-events: none; + z-index: 99999; + transition: width 0.2s ease, height 0.2s ease, background-color 0.2s ease; +} + +.custom-dot-pointer.hovered { + width: 4px; + height: 4px; + background-color: var(--accent, #a855f7); +} + +/* 3. Fluid Trailing Light Follower Ring */ +.custom-trailing-ring { + width: 32px; + height: 32px; + border: 1.5px solid var(--primary, #6b21a8); + background-color: transparent; + position: fixed; + transform: translate(-50%, -50%); + border-radius: 50%; + pointer-events: none; + z-index: 99998; + opacity: 0.75; + transition: width 0.3s cubic-bezier(0.25, 1, 0.5, 1), + height 0.3s cubic-bezier(0.25, 1, 0.5, 1), + background-color 0.3s cubic-bezier(0.25, 1, 0.5, 1), + border-color 0.3s cubic-bezier(0.25, 1, 0.5, 1); +} + +/* Smooth expansion ring visual feedback on interactive items */ +.custom-trailing-ring.hovered { + width: 50px; + height: 50px; + border-color: var(--accent, #a855f7); + background-color: rgba(168, 85, 247, 0.08); /* Sophisticated subtle inner color fill */ +} + +/* 4. Elegant Page Card Entrance Animation Frames */ +@keyframes smoothLayoutEntrance { + from { + opacity: 0; + transform: translateY(16px) scale(0.99); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* Apply graceful page loading reveal mechanics to key interface zones */ +.main-layout, .dashboard-layout, main, .landing-page, .auth-container { + animation: smoothLayoutEntrance 0.65s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +/* 5. Fluid Micro-Interaction Feedback Rules */ +button, a, .topic-card, .summary-card, .profile-info-card, .badge-item { + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.25s ease, + background-color 0.2s ease, + border-color 0.2s ease !important; +} + +/* Refined lifting scale action on high emphasis components */ +button:hover, a:hover, .topic-card:hover, .summary-card:hover { + transform: translateY(-3px) scale(1.02) !important; + box-shadow: 0 12px 20px -4px rgba(0, 0, 0, 0.12), 0 4px 8px -4px rgba(0, 0, 0, 0.06) !important; +} + +/* Gentle tactile compression effect on mouse clicks */ +button:active, a:active, .topic-card:active { + transform: translateY(-1px) scale(0.99) !important; +} + +/* 6. Accessibility Safeguard Guardrail (Honoring Prefer-Reduced-Motion Settings) */ +@media (prefers-reduced-motion: reduce) { + .custom-dot-pointer, .custom-trailing-ring { + display: none !important; + } + body, a, button, input, select, textarea, [role="button"] { + cursor: auto !important; + } + .main-layout, .dashboard-layout, main, .landing-page, .auth-container { + animation: none !important; + } + button:hover, a:hover, .topic-card:hover, .summary-card:hover { + transform: none !important; + box-shadow: none !important; + } +} \ No newline at end of file