Skip to content

Commit

Permalink
Feature/exams feed and navbar (skill-mind#170)
Browse files Browse the repository at this point in the history
* feat: Add exam feed page with comprehensive exam listing and navbar

* chore: Replace exam-paper.png with exam-paper.jpg image

* refactor: Reorganize exam feed components and update import paths
  • Loading branch information
Slenzy2 authored and OSEH-svg committed Feb 28, 2025
1 parent 3f34b72 commit a111fc4
Show file tree
Hide file tree
Showing 10 changed files with 481 additions and 67 deletions.
65 changes: 65 additions & 0 deletions src/app/exam/components/ExamCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ExamProps } from '@/interfaces/exam.interface';
import { Button } from '../../../components/ui/button';

const InfoRow = ({
label,
value,
}: {
label: string;
value: string | number;
}) => (
<div className='flex gap-2 text-sm'>
<span className='text-[#6E6E6E]'>{label}:</span>
<span className='opacity-90'>{value}</span>
</div>
);

export const ExamCard = ({ exam }: { exam: ExamProps }) => {
return (
<div className='bg-[#161716] rounded-lg p-6 text-white'>
<h2 className='text-xl font-bold mb-4'>{exam.title}</h2>
<p className='mb-8 text-sm leading-relaxed'>{exam.description}</p>

<div className='h-[1px] bg-[#313130] mb-8' />

<div className='flex flex-col gap-4 mb-8'>
<InfoRow label='DATE' value={exam.date} />
<InfoRow label='DURATION' value={exam.duration} />
<InfoRow label='REGISTERED CANDIDATES' value={exam.registered} />
<InfoRow label='CERTIFICATION' value={exam.certification} />
<InfoRow label='FORMAT' value={exam.format} />
</div>

<div className='h-[1px] bg-[#313130] mb-8' />

<div className='mb-8'>
<h3 className='text-[#ABABAB] text-sm mb-3'>TOPICS COVERED:</h3>
<ul className='list-disc pl-5 text-sm space-y-2'>
{exam.topics.map((topic, idx) => (
<li key={idx} className='text-[#FCFCFC]'>
{topic}
</li>
))}
</ul>
</div>

<div className='mb-8'>
<h3 className='text-[#ABABAB] text-sm mb-3'>WHY TAKE THIS EXAM?</h3>
<ul className='list-disc pl-5 text-sm space-y-2'>
{exam.advantages.map((advantage, idx) => (
<li key={idx} className='text-[#FCFCFC]'>
{advantage}
</li>
))}
</ul>
</div>

<Button
variant='outline'
className='w-full bg-transparent py-3 border border-[#D0EFB1] rounded-lg hover:bg-[#D0EFB1] hover:text-[#000000] transition-colors uppercase'
>
Register
</Button>
</div>
);
};
179 changes: 179 additions & 0 deletions src/app/exam/components/ExamFeedNavbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
'use client';
import Logo from '@/public/skillnet-white logo.png';
import Avatar from '@/public/img/profile-avatar.png';
import Image from 'next/image';
import Link from 'next/link';
import { Bell, Search, MoreVertical, X, Menu } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { Fragment, useEffect, useRef, useState } from 'react';
import { ExamNavLinkProps } from '@/interfaces/exam.interface';
import { navLinks } from './mockData';

const ExamFeedNavbar = () => {
const pathname = usePathname();
const [showSearch, setShowSearch] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);

const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);

if (showSearch) setShowSearch(false);
};

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsMenuOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

return (
<>
<nav className='w-full flex justify-between items-center px-4 sm:px-8 lg:px-16 py-[22px] bg-[#101110] text-sm leading-6 text-[#FCFCFC] fixed top-0 left-0 z-50'>
{/* Logo */}
<Link href='/' className='flex items-center cursor-pointer'>
<Image
className='w-[50px] h-[30px] mobile:w-[80px] mobile:h-[40px]'
src={Logo}
alt='Logo'
/>
</Link>

<div className='hidden md:flex flex-1 items-center w-full justify-between'>
{/* Navigation Links */}
<ul className='flex justify-center items-center gap-4 mx-auto'>
{navLinks.map((link: ExamNavLinkProps, index) => {
const isActive = pathname === link.href;
return (
<Fragment key={link.href}>
<Link href={link.href}>
<span
className={`relative cursor-pointer pb-1 group ${
isActive ? 'text-white' : 'text-[#ABABAB]'
}`}
>
{link.label}
</span>
</Link>
{index < navLinks.length - 1 && (
<li className='bg-[#1D1D1C] w-[3px] h-4 rounded-lg' />
)}
</Fragment>
);
})}
</ul>

<div className='flex items-center gap-2 lgTablet:mx-auto'>
<button className='p-2 hover:bg-[#313130] rounded-full transition-colors'>
<Bell size={20} />
</button>

{/* Search Bar - Show full bar on large screens, icon on smaller */}
<div className='relative'>
<div className='hidden lgTablet:block'>
<Search
className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400'
size={16}
/>
<input
type='text'
placeholder='Search...'
className='pl-9 pr-3 py-1.5 w-[280px] rounded-lg bg-transparent border border-[#696969] focus:outline-none focus:ring-1 focus:ring-[#313130]'
/>
</div>
<button
className='lgTablet:hidden p-2 hover:bg-[#313130] rounded-full transition-colors'
onClick={() => setShowSearch(!showSearch)}
>
<Search size={20} />
</button>
</div>
</div>
</div>

<div className='flex items-center gap-2'>
{/* User Profile */}

<div className='flex items-center'>
<div className='flex items-center gap-1 mobile:gap-2 bg-[#161716] py-1 mobile:py-2 px-3 rounded-lg'>
<Image
className='w-8 h-8 rounded-full'
src={Avatar}
alt='User Avatar'
width={32}
height={32}
/>
<span className='hidden mobile:block text-white'>
osatuyipikin.brawos.eth
</span>
<button className='p-1 hover:bg-[#313130] rounded-full transition-colors'>
<MoreVertical size={16} />
</button>
</div>
</div>

<button
onClick={toggleMenu}
className='md:hidden flex p-2 rounded-lg hover:bg-[#313130] transition-colors'
aria-label='Toggle menu'
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>

{isMenuOpen && (
<div
ref={menuRef}
className='absolute top-full right-4 mt-1 w-64 z-50 rounded-lg bg-[#101110] border border-[#313130] shadow-lg md:hidden'
>
<div className='px-6 py-4 border-b border-[#313130]'>
<h3 className='text-sm font-semibold text-white'>SELECT PAGE</h3>
</div>
<div className='py-2'>
{navLinks.map((link) => {
const isActive = pathname === link.href;
return (
<Link
key={link.href}
href={link.href}
className={`flex items-center px-6 py-3 hover:bg-[#313130] transition-colors text-sm ${
isActive ? 'text-white' : 'text-[#ABABAB]'
}`}
onClick={() => setIsMenuOpen(false)}
>
{link.label}
</Link>
);
})}
</div>
</div>
)}
</nav>

{/* Floating Search Bar */}
{showSearch && (
<div className='fixed top-[80px] left-0 w-full px-4 sm:px-8 lg:px-16 z-50 lgTablet:hidden'>
<div className='relative w-full bg-[#101110] rounded-lg shadow-lg p-4'>
<Search
className='absolute left-7 top-1/2 transform -translate-y-1/2 text-gray-400'
size={16}
/>
<input
type='text'
placeholder='Search...'
className='pl-9 pr-3 py-1.5 w-full rounded-lg bg-transparent border border-[#696969] focus:outline-none focus:ring-1 focus:ring-[#313130]'
autoFocus
/>
</div>
</div>
)}
</>
);
};

export default ExamFeedNavbar;
69 changes: 69 additions & 0 deletions src/app/exam/components/ExamFeedPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use client';

import Image from 'next/image';
import examImage from '@/public/img/exam-paper.jpg';
import { examCategories, examMockData } from './mockData';
import { Button } from '../../../components/ui/button';
import { ArrowLeft, X } from 'lucide-react';
import { ExamCard } from './ExamCard';

const ExamFeedPage = () => {
return (
<main className='min-h-screen bg-[#101110] pt-24 px-4 sm:px-6 lg:px-[100px]'>
{/* Hero Section */}
<div className='w-full h-[220px] relative rounded-lg overflow-hidden'>
<Image
src={examImage}
alt='Examinations background'
fill
className='object-cover'
priority
/>
<div className='absolute inset-0' />
<div className='relative h-full flex items-end px-2 sm:px-4 lg:px-16 pb-8'>
<h1 className='text-3xl sm:text-4xl font-semibold text-white'>
Ongoing examinations
</h1>
</div>
</div>

<div className='my-14 flex justify-between items-center text-white'>
<Button
variant='ghost'
size='sm'
className='gap-2 hover:bg-transparent hover:text-white hover:brightness-125'
>
<ArrowLeft className='w-4 h-4' />
Back
</Button>

<div className='flex space-x-3 items-center cursor-pointer'>
<div className='font-extralight text-sm'>ChatBox</div>
<X className='w-4 h-4 font-semibold' />
</div>
</div>

{/* Categories Section */}
<div className='flex space-x-4 overflow-x-auto pb-4 scrollbar-hide'>
{examCategories.map((category) => (
<button
key={category}
className='flex-shrink-0 flex flex-col items-start justify-center border border-[#222220] px-6 py-2 bg-[#101110] text-[#FCFCFC] rounded-lg whitespace-nowrap hover:bg-[#313130] transition-colors'
>
{category}
<span className='text-xs text-[#898783]'>6 ongoing exams</span>
</button>
))}
</div>

{/* Exams Section */}
<div className=' py-8 grid grid-cols-1 md:grid-cols-2 gap-6'>
{examMockData.map((exam, index) => (
<ExamCard key={index} exam={exam} />
))}
</div>
</main>
);
};

export default ExamFeedPage;
67 changes: 67 additions & 0 deletions src/app/exam/components/mockData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ExamNavLinkProps, ExamProps } from '@/interfaces/exam.interface';

export const examMockData: ExamProps[] = [
{
title: 'WEB3 FUNDAMENTALS CERTIFICATION EXAM',
description:
"The Web3 Test Exam is a blockchain-powered assessment designed to evaluate a candidate's understanding of decentralized technologies, smart contracts, and blockchain security. This exam is hosted on StarkNet, ensuring that all results are securely stored on-chain for authenticity and tamper-proof verification.",
date: '26th Feb, 2025',
duration: '2h',
registered: 110,
certification: 'Yes on completion',
format: 'Multiple choice',
topics: [
'Blockchain Basics (consensus mechanisms, Layer 1 vs. Layer 2, decentralization)',
'Smart Contracts (solidity fundamentals, contract security, gas optimization)',
'DeFi & NFTs (decentralized finance protocols, NFT standards, use cases)',
],
advantages: [
'Enhance Your Web3 Credentials With A Blockchain-Verified Certificate',
'Prove Your Skills To Potential Employers And Blockchain Projects',
'Gain Credibility In The Decentralized Space With Verifiable Results',
],
},
{
title: 'WEB3 FUNDAMENTALS CERTIFICATION EXAM',
description:
"The Web3 Test Exam is a blockchain-powered assessment designed to evaluate a candidate's understanding of decentralized technologies, smart contracts, and blockchain security. This exam is hosted on StarkNet, ensuring that all results are securely stored on-chain for authenticity and tamper-proof verification.",
date: '26th Feb, 2025',
duration: '2h',
registered: 110,
certification: 'Yes on completion',
format: 'Multiple choice',
topics: [
'Blockchain Basics (consensus mechanisms, Layer 1 vs. Layer 2, decentralization)',
'Smart Contracts (solidity fundamentals, contract security, gas optimization)',
'DeFi & NFTs (decentralized finance protocols, NFT standards, use cases)',
],
advantages: [
'Enhance Your Web3 Credentials With A Blockchain-Verified Certificate',
'Prove Your Skills To Potential Employers And Blockchain Projects',
'Gain Credibility In The Decentralized Space With Verifiable Results',
],
},
];

export const examCategories = [
'JavaScript',
'Data Science',
'AI Development',
'Frontend',
'Cairo',
'Solidity',
'NextJS',
'Blockchain',
'Web3',
'StarkNet',
'Ethereum',
'Polygon',
'Arbitrum',
'Optimism',
];

export const navLinks: ExamNavLinkProps[] = [
{ href: '/exam/feed', label: 'Exams' },
{ href: '/exam/registered', label: 'Registered' },
{ href: '/exam/results', label: 'Results' },
];
Loading

0 comments on commit a111fc4

Please sign in to comment.