-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/exams feed and navbar (#170)
* 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
Showing
10 changed files
with
481 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }, | ||
]; |
Oops, something went wrong.