Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@ import Header from "./components/Header";
import Footer from "./components/Footer";
import ScrollToTop from "./components/ScrollToTop";


function App() {
return (
<div className="min-h-screen bg-white flex flex-col">
{/* Skip to content link for accessibility */}
<a
href="#main-content"
className="sr-only focus:not-sr-only absolute left-2 top-2 z-50 bg-blue-700 text-white px-4 py-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
tabIndex={0}
>
Skip to main content
</a>
<ScrollToTop />
<Header />
<main className="flex-grow">
<main id="main-content" className="flex-grow focus:outline-none">
<Outlet /> {/* This will render the current page */}
</main>
Comment on lines +34 to 36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid nested

landmarks across pages.

Some pages (e.g., Home.jsx) already render a top-level <main>. Wrapping the <Outlet /> with <main> here risks multiple mains per document, which is an a11y violation. Prefer a single main landmark.

Option A (preferred): Replace page-level outer <main> with <div> on pages that render under App’s <main>.

Option B (stopgap within this file):

-      <main id="main-content" className="flex-grow focus:outline-none">
+      <div id="main-content" role="main" className="flex-grow focus:outline-none">
         <Outlet /> {/* This will render the current page */}
-      </main>
+      </div>

Run to find pages using <main>:


🏁 Script executed:

#!/bin/bash
rg -n -C2 '<main\b' client/src/pages

Length of output: 2336


Avoid nested <main> landmarks by removing page-level <main>

  • rg found page-level <main> in client/src/pages/{Interview.jsx:192, SignUp.jsx:241, Report.jsx:45, Dashboard.jsx:108, Login.jsx:196, Home.jsx:11}
  • Option A (preferred): replace each page-level <main> with a <div> under App’s single <main>
  • Option B (stopgap): in App.jsx change <main id="main-content"> to <div id="main-content" role="main">
🤖 Prompt for AI Agents
In client/src/App.jsx around lines 34 to 36, the app currently renders a
top-level <main> and Outlet pages also contain their own <main> elements causing
nested landmark conflict; to fix, follow the preferred Option A: update each
page file (client/src/pages/Interview.jsx, SignUp.jsx, Report.jsx,
Dashboard.jsx, Login.jsx, Home.jsx) to replace their page-level <main> elements
with <div> (keeping classes/ids/aria attributes intact), or if you need a
stopgap, change the App.jsx <main id="main-content"> to <div id="main-content"
role="main"> so there is only a single main landmark. Ensure IDs, focus
handling, and existing accessibility attributes are preserved after the change.

<Footer />
Expand Down
14 changes: 14 additions & 0 deletions client/src/components/Badges.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";

export default function Badges({ badges = [] }) {
if (!badges.length) return null;
return (
<div className="flex flex-wrap gap-2 my-2" aria-label="User Badges">
{badges.map((badge, idx) => (
<span key={idx} className="bg-yellow-200 text-yellow-800 px-3 py-1 rounded-full text-xs font-semibold" role="img" aria-label={badge}>
{badge}
</span>
))}
</div>
);
}
14 changes: 14 additions & 0 deletions client/src/components/BookmarkButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";

export default function BookmarkButton({ bookmarked, onClick }) {
return (
<button
onClick={onClick}
aria-pressed={bookmarked}
aria-label={bookmarked ? "Remove Bookmark" : "Add Bookmark"}
className={`p-2 rounded-full border transition-colors ${bookmarked ? "bg-yellow-300 text-yellow-900" : "bg-gray-200 text-gray-600 hover:bg-yellow-100"}`}
>
{bookmarked ? "★" : "☆"}
</button>
);
}
2 changes: 1 addition & 1 deletion client/src/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FaGithub, FaTwitter, FaLinkedin, FaEnvelope } from "react-icons/fa";

export default function Footer() {
return (
<footer className="bg-[#fafaf9] text-gray-900 border-t border-gray-100">
<footer className="bg-[#fafaf9] text-gray-900 border-t border-gray-100" role="contentinfo" aria-label="Footer">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Brand Section */}
Expand Down
62 changes: 47 additions & 15 deletions client/src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useState, useRef, useEffect } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { FaUser, FaSignOutAlt, FaChevronDown, FaBars, FaTimes } from "react-icons/fa"
import { useAuth } from "../context/AuthContext"
import React from "react"

export default function Header() {
const { t, i18n } = useTranslation();
const { user, isLoggedIn, logout } = useAuth()
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
Expand Down Expand Up @@ -58,21 +60,30 @@ export default function Header() {

const NavLinks = () => (
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-8">
<Link to="/" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">Home</Link>
<Link to="/" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">{t('welcome', 'Home')}</Link>
<Link to="/about" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">About</Link>
{isLoggedIn && (
<Link to="/interview/setup" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">Practice</Link>
)}
{isLoggedIn && (
<Link to="/dashboard" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">Dashboard</Link>
<Link to="/dashboard" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">{t('dashboard')}</Link>
)}
<Link to="/resources" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">{t('resources')}</Link>
{isLoggedIn && (
<Link to="/mock-interview" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">{t('mockInterview')}</Link>
)}
{isLoggedIn && (
<Link to="/community-qa" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">{t('communityQA')}</Link>
)}
{isLoggedIn && (
<Link to="/resume-review" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">{t('resumeReview')}</Link>
)}
<Link to="/resources" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">Resources</Link>
</div>
)

return (
<>
<header className="sticky top-0 z-50 backdrop-blur-md bg-white/60 border-b border-white/30 shadow-sm transition-colors duration-300">
<header className="sticky top-0 z-50 backdrop-blur-md bg-white/60 border-b border-white/30 shadow-sm transition-colors duration-300" role="banner">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
Expand All @@ -82,30 +93,44 @@ export default function Header() {
</Link>
</div>

<nav className="hidden md:flex">
<nav className="hidden md:flex" aria-label="Main Navigation">
<NavLinks />
</nav>

<div className="md:hidden">
<button
onClick={toggleMobileMenu}
className="text-gray-700 hover:text-gray-900 focus:outline-none"
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
aria-expanded={isMobileMenuOpen}
aria-controls="mobile-menu"
>
{isMobileMenuOpen ? <FaTimes size={20} /> : <FaBars size={20} />}
</button>
</div>

<div className="hidden md:flex items-center space-x-4">
<button
onClick={() => i18n.changeLanguage(i18n.language === 'en' ? 'hi' : 'en')}
className="px-3 py-1 rounded bg-gray-200 text-gray-700 hover:bg-gray-300 mr-2"
aria-label="Switch language"
>
{i18n.language === 'en' ? 'हिंदी' : 'EN'}
</button>
{!isLoggedIn ? (
<>
<Link to="/login" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">Log in</Link>
<Link to="/signup" className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md font-medium transition-colors">Sign up</Link>
<Link to="/login" className="px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">{t('login')}</Link>
<Link to="/signup" className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md font-medium transition-colors">{t('signup')}</Link>
</>
) : (
<div className="relative" ref={dropdownRef}>
<button
onClick={toggleDropdown}
className="flex items-center space-x-2 p-2 rounded-lg hover:bg-white/40 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-haspopup="true"
aria-expanded={isDropdownOpen}
aria-controls="user-menu"
aria-label="User menu"
>
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center overflow-hidden">
{user?.avatar ? (
Expand All @@ -118,18 +143,18 @@ export default function Header() {
</button>

{isDropdownOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
<div id="user-menu" role="menu" aria-label="User menu" className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
<div className="px-4 py-2 border-b border-gray-100">
<p className="text-sm font-medium text-gray-900">{user?.name}</p>
<p className="text-xs text-gray-500">{user?.email}</p>
</div>

<div className="py-1">
<Link to="/profile" className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-white/40 transition-colors duration-300" onClick={() => setIsDropdownOpen(false)}>
<Link to="/profile" className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-white/40 transition-colors duration-300" onClick={() => setIsDropdownOpen(false)} role="menuitem">
<FaUser className="w-4 h-4 mr-3 text-gray-400" />
Profile
</Link>
<button onClick={handleLogout} className="w-full flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-white/40 transition-colors duration-300 text-left">
<button onClick={handleLogout} className="w-full flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-white/40 transition-colors duration-300 text-left" role="menuitem">
<FaSignOutAlt className="w-4 h-4 mr-3 text-gray-400" />
Logout
</button>
Expand All @@ -142,17 +167,24 @@ export default function Header() {
</div>

{isMobileMenuOpen && (
<div className="md:hidden mt-2 space-y-2 pb-4">
<div id="mobile-menu" className="md:hidden mt-2 space-y-2 pb-4" role="menu" aria-label="Mobile Navigation">
<button
onClick={() => i18n.changeLanguage(i18n.language === 'en' ? 'hi' : 'en')}
className="px-3 py-1 rounded bg-gray-200 text-gray-700 hover:bg-gray-300 mb-2"
aria-label="Switch language"
>
{i18n.language === 'en' ? 'हिंदी' : 'EN'}
</button>
<NavLinks />
{!isLoggedIn ? (
<div className="space-y-2 pt-2">
<Link to="/login" className="block px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">Log in</Link>
<Link to="/signup" className="block px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Sign up</Link>
<Link to="/login" className="block px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105" role="menuitem">{t('login')}</Link>
<Link to="/signup" className="block px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600" role="menuitem">{t('signup')}</Link>
</div>
) : (
<div className="space-y-2 pt-2">
<Link to="/profile" className="block px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105">Profile</Link>
<button onClick={handleLogout} className="w-full text-left px-4 py-2 rounded transition-colors duration-300 text-gray-700 hover:text-cyan-500 hover:bg-white/40">Logout</button>
<Link to="/profile" className="block px-4 py-2 rounded transition-all duration-200 text-gray-700 hover:text-cyan-500 hover:bg-white/40 hover:scale-105" role="menuitem">Profile</Link>
<button onClick={handleLogout} className="w-full text-left px-4 py-2 rounded transition-colors duration-300 text-gray-700 hover:text-cyan-500 hover:bg-white/40" role="menuitem">Logout</button>
</div>
)}
</div>
Expand Down
18 changes: 18 additions & 0 deletions client/src/components/Leaderboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";

export default function Leaderboard({ users = [] }) {
if (!users.length) return null;
return (
<div className="my-4" aria-label="Leaderboard">
<h3 className="font-bold text-lg mb-2">🏆 Leaderboard</h3>
<ol className="list-decimal pl-5">
{users.map((user, idx) => (
<li key={user.id || idx} className="mb-1 flex items-center gap-2">
<span className="font-semibold">{user.name}</span>
<span className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs">{user.leaderboardPoints} pts</span>
</li>
))}
</ol>
</div>
);
}
115 changes: 40 additions & 75 deletions client/src/components/ScoreTrendChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,82 +26,47 @@ const CustomTooltip = ({ active, payload }) => {
};

export default function ScoreTrendChart({
chartData
chartData
}) {

return (
<div className="bg-white rounded-lg border border-gray-200 p-6 mb-8">
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Score Trend
</h3>
return (
<section className="bg-white rounded-lg border border-gray-200 p-6 mb-8" aria-labelledby="score-trend-heading" role="region">
<div className="mb-6">
<h3 id="score-trend-heading" className="text-lg font-semibold text-gray-900 mb-4">
Score Trend
</h3>
</div>

{/* <div className="flex space-x-1 bg-gray-100 rounded-lg p-1 w-fit">
{periods.map((period) => (
<button
key={period}
onClick={() => setSelectedPeriod(period)}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
selectedPeriod === period
? "bg-white text-gray-900 shadow-sm"
: "text-gray-600 hover:text-gray-900"
}`}
>
{period}
</button>
))}
</div> */}
</div>

{/* <div className="mb-6">
<div className="text-3xl font-bold text-gray-900 mb-1">
{currentScore}
</div>
<div className="text-sm text-gray-600">
{timeframe}{" "}
<span
className={`font-medium ${
percentageChange >= 0
? "text-green-600"
: "text-red-600"
}`}
>
{percentageChange >= 0 ? "+" : ""}
{percentageChange}%
</span>
</div>
</div> */}

<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={chartData}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
<YAxis
domain={["dataMin - 5", "dataMax + 7"]}
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: "#6b7280" }}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="score"
stroke="#6366f1"
strokeWidth={3}
dot={{ fill: "#6366f1", strokeWidth: 2, r: 4 }}
activeDot={{
r: 6,
stroke: "#6366f1",
strokeWidth: 2,
fill: "#ffffff",
}}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
<div className="h-64" role="img" aria-label="Line chart showing interview score trends over time">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={chartData}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
<YAxis
domain={["dataMin - 5", "dataMax + 7"]}
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: "#6b7280" }}
/>
<Tooltip content={<CustomTooltip />} wrapperStyle={{ outline: 'none' }} />
<Line
type="monotone"
dataKey="score"
stroke="#6366f1"
strokeWidth={3}
dot={{ fill: "#6366f1", strokeWidth: 2, r: 4 }}
activeDot={{
r: 6,
stroke: "#6366f1",
strokeWidth: 2,
fill: "#ffffff",
}}
/>
</LineChart>
</ResponsiveContainer>
</div>
</section>
);
}
10 changes: 10 additions & 0 deletions client/src/components/StreakCounter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from "react";

export default function StreakCounter({ streak = 0 }) {
return (
<div className="flex items-center space-x-2" aria-label="Practice Streak">
<span className="text-2xl" role="img" aria-label="fire">🔥</span>
<span className="font-bold text-lg">{streak} day streak</span>
</div>
);
}
18 changes: 18 additions & 0 deletions client/src/components/TagFilter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";

export default function TagFilter({ tags = [], selected = [], onChange }) {
return (
<div className="flex flex-wrap gap-2 my-2" aria-label="Tag Filter">
{tags.map((tag) => (
<button
key={tag}
className={`px-3 py-1 rounded-full border text-xs font-medium transition-colors ${selected.includes(tag) ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-700 hover:bg-blue-100"}`}
onClick={() => onChange(tag)}
aria-pressed={selected.includes(tag)}
>
{tag}
</button>
))}
</div>
);
}
Loading