Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f8db791
feat: add initial layout for topics page with navigation and content …
qaisersofficial Oct 17, 2025
4b015f4
feat: implement TopicsNav component with integrated SearchBar
qaisersofficial Oct 18, 2025
63ea320
feat: add initial FilterSortBar component and integrate it into Topic…
qaisersofficial Oct 18, 2025
9006d47
feat: update FilterSortBar layout and button styles for improved UI
qaisersofficial Oct 18, 2025
fde548e
Complete UI of the FilterSortBar
qaisersofficial Oct 18, 2025
dd969a7
create the topic content for topic card and paste it into a container
qaisersofficial Oct 19, 2025
75d4327
Update the Header.tsx for navigation of Links and change the icons as…
qaisersofficial Oct 19, 2025
a2fada4
feat: add StatItem component and refactor TopicsCard to use it for di…
qaisersofficial Oct 19, 2025
6470664
feat: implement dynamic TopicsCard rendering with topic data in CardC…
qaisersofficial Oct 19, 2025
58cca0d
feat: refactor CardContainer to use centralized TOPICS_DATA and creat…
qaisersofficial Oct 19, 2025
2813949
feat: enhance TopicsCard and CardContainer with improved accessibilit…
qaisersofficial Oct 19, 2025
7f61bf1
feat: implement search functionality in TopicsNav and CardContainer w…
qaisersofficial Oct 19, 2025
612fe58
feat: add filtering and sorting functionality to TopicsNav, CardConta…
qaisersofficial Oct 19, 2025
c7d4998
Fix: Update .gitignore
Oct 24, 2025
0a1b89e
Merge branch 'main' into feature/topics
Oct 24, 2025
601ce3f
Fix: Delete lockfile
Oct 24, 2025
ed2d97b
FIx: Refactor lockfile & gitignore
Oct 24, 2025
744f534
Fix: Reinstalled dependencies
Oct 24, 2025
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
726 changes: 333 additions & 393 deletions package-lock.json

Large diffs are not rendered by default.

Binary file added public/01-Topic-Design-Patterns.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions src/app/topics/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";
import { useState } from "react";
import TopicsNav from "@/components/topicsMain/TopicsNav";
import CardContainer from "@/components/topicsMain/CardContainer";

const Page = () => {
const [searchQuery, setSearchQuery] = useState("");
const [filterOption, setFilterOption] = useState("");
const [sortOption, setSortOption] = useState("");

return (
<section className="w-full flex justify-center items-center">
<div className="max-w-6xl h-full flex-1 items-center">
<TopicsNav
onSearch={setSearchQuery}
onFilterChange={setFilterOption}
onSortChange={setSortOption}
/>
<div className="w-full my-3 py-2 md:my-6 md:py-4">
<CardContainer
searchQuery={searchQuery}
filterOption={filterOption}
sortOption={sortOption}
/>
</div>
</div>
</section>
);
};

export default Page;
96 changes: 96 additions & 0 deletions src/components/common/FilterSortBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use client";

import { useState } from "react";
import { ChevronDown } from "lucide-react";

interface FilterSortBarProps {
onFilterChange: (filter: string) => void;
onSortChange: (sort: string) => void;
}

const FilterSortBar: React.FC<FilterSortBarProps> = ({
onFilterChange,
onSortChange,
}) => {
const [openMenu, setOpenMenu] = useState<"filter" | "sort" | null>(null);

const toggleMenu = (menu: "filter" | "sort") => {
setOpenMenu((prev) => (prev === menu ? null : menu));
};

return (
<div className="w-[52%] md:w-[30%] flex items-end gap-1 text-sm md:gap-2.5 justify-end sm:flex-row flex-col">
{/* Filter Button */}
<div className="relative">
<button
onClick={() => toggleMenu("filter")}
className="flex gap-1 items-center bg-black text-white rounded-full p-1.5 md:text-base md:px-3 transition hover:opacity-90 cursor-pointer hover:font-bold"
>
<ChevronDown
size={20}
strokeWidth={3.5}
className={`transition-transform duration-300 ${
openMenu === "filter" ? "rotate-180" : ""
}`}
/>
<span className="text-sm">Filter</span>
</button>

{/* Filter Dropdown */}
{openMenu === "filter" && (
<div className="absolute right-0 mt-2 w-40 bg-white border border-gray-200 rounded-xl shadow-md p-2 z-10 animate-fadeIn">
{["Newest", "Oldest", "Popular"].map((filter) => (
<p
key={filter}
className="text-sm text-gray-600 hover:text-black cursor-pointer"
onClick={() => {
onFilterChange(filter);
setOpenMenu(null);
}}
>
{filter}
</p>
))}
</div>
)}
</div>

{/* Sort Button */}
<div className="relative">
<button
onClick={() => toggleMenu("sort")}
className="flex gap-1 items-center bg-black text-white rounded-full p-1.5 md:text-base md:px-3 transition hover:opacity-90 cursor-pointer hover:font-bold"
>
<ChevronDown
size={18}
strokeWidth={3.5}
className={`transition-transform duration-300 ${
openMenu === "sort" ? "rotate-180" : ""
}`}
/>
<span className="text-sm">Sort</span>
</button>

{/* Sort Dropdown */}
{openMenu === "sort" && (
<div className="absolute right-0 mt-2 w-40 bg-white border border-gray-200 rounded-xl shadow-md p-2 z-10 animate-fadeIn">
{["A–Z", "Z–A", "By Date"].map((sort) => (
<p
key={sort}
className="text-sm text-gray-600 hover:text-black cursor-pointer"
onClick={() => {
onSortChange(sort);
setOpenMenu(null);
}}
>
{sort}
</p>
))}
</div>
)}
</div>
</div>
);
};

export default FilterSortBar;
65 changes: 65 additions & 0 deletions src/components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { useState, useRef } from "react";
import { Search } from "lucide-react";

interface SearchBarProps {
onSearch: (query: string) => void;
}

const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
const [expanded, setExpanded] = useState(false);
const [query, setQuery] = useState("");
const inputRef = useRef<HTMLInputElement | null>(null);

const handleExpand = () => {
setExpanded(true);
setTimeout(() => inputRef.current?.focus(), 250);
};

const handleBlur = () => {
if (inputRef.current && inputRef.current.value.trim() === "") {
setExpanded(false);
}
};

const handleSearch = () => {
onSearch(query.trim());
};

return (
<div className="w-full sm:w-[60%] md:w-[40%] lg:w-[30%] flex items-center gap-3">
<div
className={`flex md:items-center transition-all duration-500 ease-in-out ${
expanded
? "w-[60%] border border-gray-200 rounded-full md:px-3 py-1 bg-white text-gray-700"
: "p-[4px] md:p-1 justify-center bg-black text-white rounded-full cursor-pointer"
}`}
onClick={!expanded ? handleExpand : undefined}
>
<Search className="cursor-pointer w-[20px] h-[20px] md:w-[24px] md:h-[24px]" />
{expanded && (
<input
ref={inputRef}
type="text"
placeholder="Search..."
className="ml-2 bg-transparent outline-none text-sm w-full text-gray-700"
value={query}
onChange={(e) => setQuery(e.target.value)}
onBlur={handleBlur}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
/>
)}
</div>

<button
onClick={handleSearch}
className="bg-black text-white rounded-full p-1 text-sm md:text-base md:px-3 md:py-1.5 transition hover:opacity-90 cursor-pointer"
>
Search
</button>
</div>
);
};

export default SearchBar;
20 changes: 20 additions & 0 deletions src/components/common/StatItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";

interface StatItemProps {
icon: React.ReactNode;
label: string;
value: number;
}

const StatItem: React.FC<StatItemProps> = ({ icon, label, value }) => {
return (
<div className="flex items-center gap-1">
{icon}
<span className="text-[10px] font-bold">
{value} {label}
</span>
</div>
);
};

export default StatItem;
71 changes: 71 additions & 0 deletions src/components/common/TopicsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Clock } from "lucide-react";
import StatItem from "./StatItems";

interface Stat {
icon: React.ReactNode;
label: string;
value: number;
}

interface TopicsCardProps {
title: string;
description: string;
duration: string;
imageUrl: string;
stats: Stat[];
}


const TopicsCard: React.FC<TopicsCardProps> = ({
title,
description,
duration,
imageUrl,
stats,
}) => {
return (
<div className="hover:scale-[1.02] duration-300">
{/* Image Section */}
<img
src={imageUrl}
alt={`This image contains the data for ${title}`}
className="object-cover w-full h-2/3 border border-[#D9D9D9] rounded-xl"
/>

{/* Content Section */}
<div className="p-2.5 bg-[#D9D9D9] rounded-xl -mt-6 sm:-mt-14 md:-mt-6 lg:-mt-8 z-5 md:z-10 relative">
<div className="flex justify-between items-center">
{/* Title */}
<a href="#" className="text-sm lg:text-medium font-bold hover:underline">
{title}
</a>

{/* Duration */}
<div className="flex items-center text-sm md:text-medium">
<Clock size={16} className="mr-1 text-[#02542D]" />
<span className="font-bold">{duration}</span>
</div>
</div>

{/* Description */}
<p className="font-extrabold tracking-normal text-sm md:text-wrap text-[#999999]">
{description}
</p>

{/* Stats Row */}
<div className="flex items-center my-4 flex-wrap justify-between space-x-0.5">
{stats.map((item, index) => (
<StatItem key={index} icon={item.icon} label={item.label} value={item.value} />
))}
</div>

{/* Button */}
<button className="w-full bg-black text-white py-2 rounded-full font-medium hover:opacity-80 cursor-pointer transition">
View
</button>
</div>
</div>
);
};

export default TopicsCard;
28 changes: 19 additions & 9 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
"use client"
import { HEADER_ITEMS } from "@/constants";
import { MenuIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";


const Header = () => {
const pathname = usePathname();
return (
<header className="w-full flex justify-center items-center h-16">
<div className="max-w-6xl h-full flex flex-1 items-center justify-between">
<div className="gap-x-[15px] md:flex hidden items-center">
<h1>FA</h1>
{HEADER_ITEMS.map((nav) => (
<Link
className="text-gray-400 text-[15px] font-medium"
key={nav.id}
href={nav.href}
>
{nav.label}
</Link>
))}
{HEADER_ITEMS.map((nav) => {
const isActive = pathname === nav.href;

return (
<Link
key={nav.id}
href={nav.href}
className={`text-[15px] font-medium transition-colors duration-200 text-[#999999]
${isActive ? " border-b-2 border-[#004715]" : " hover:text-[#004715]"}
`}
>
{nav.label}
</Link>
);
})}
</div>
<MenuIcon className="md:hidden block" size={36} />
<button className="rounded-full px-3 py-2 text-background bg-foreground text-[14px] font-semibold">
Expand Down
Loading