Files
T120B165/Client/app/routes/posts.tsx

156 lines
5.5 KiB
TypeScript

import type { Route } from "../+types/root";
import { useState, useEffect, type FormEventHandler } from "react";
import RootLayout from "~/layout/RootLayout";
import { getTagColor } from "../utils/tags";
import type { Post } from "../types/post";
import Icon, { ICONS } from "../components/Icon";
import { API } from "../api/api";
import { useNavigate } from "react-router";
import type { PaginatedResponse } from "~/types/PaginatedResponse";
import Loading from "~/components/Loading";
interface PostCardProps {
post: Post;
onClick: (id: number) => void;
}
export function meta({}: Route.MetaArgs) {
return [
{ title: "ImgBoard - Posts" },
];
}
export default function Posts() {
const PostCard = ({ post, onClick }: PostCardProps) => (
<div className="bg-white rounded-lg shadow-md overflow-hidden cursor-pointer transform hover:-translate-y-1 transition-transform duration-300" onClick={() => onClick(post.id)}>
<img src={post.fileUrl} alt={post.title} className="w-full h-48 object-cover" />
<div className="p-4">
<h3 className="font-bold text-lg truncate">{post.title}</h3>
<div className="flex flex-wrap gap-1 mt-2">
{post.tags.slice(0, 3).map(tag => (
<span key={tag.name} className={`text-xs font-semibold px-2 py-1 rounded-full ${getTagColor(tag)}`}>
{tag.name}
</span>
))}
{post.tags.length > 3 && <span className="text-gray-500 text-xs font-semibold py-1">...</span>}
</div>
</div>
</div>
);
const SearchBar = ({ currentQuery, onSearch }: { currentQuery: string, onSearch: (query: string) => void }) => {
const [query, setQuery] = useState(currentQuery);
const handleSearch: FormEventHandler = (e) => {
e.preventDefault();
onSearch(query);
};
return (
<div className="p-4 md:px-6 md:pt-6">
<form onSubmit={handleSearch} className="max-w-xl mx-auto">
<div className="relative">
<input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search posts by tags (e.g., +high-res -theotown)" className="w-full px-5 py-3 text-gray-700 bg-white border-2 border-gray-200 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"/>
<button type="submit" className="absolute top-0 right-0 mt-3 mr-4 text-gray-400 hover:text-gray-600">
<Icon path={ICONS.search}/>
</button>
</div>
</form>
</div>
);
};
const GridView = ({ posts, onPostSelect }: { posts: Post[]; onPostSelect: (id: number) => void }) => (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 p-4 md:p-6">
{posts.map(post => <PostCard key={post.id} post={post} onClick={onPostSelect} />)}
</div>
);
const Pagination = ({
postsPerPage,
totalPosts,
paginate,
currentPage,
}: {
postsPerPage: number;
totalPosts: number;
paginate: (pageNumber: number) => void;
currentPage: number;
}) => {
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) { pageNumbers.push(i); }
if(pageNumbers.length <= 1) return null;
return (
<nav className="mt-8 flex justify-center">
<ul className="flex items-center space-x-2">
{pageNumbers.map(number => (
<li key={number}>
<button onClick={() => paginate(number)} className={`px-4 py-2 rounded-md transition-colors ${ currentPage === number ? 'bg-blue-500 text-white' : 'bg-white text-gray-700 hover:bg-gray-100'}`}>
{number}
</button>
</li>
))}
</ul>
</nav>
);
};
const [paginatedData, setPaginatedData] = useState<PaginatedResponse<Post> | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const navigate = useNavigate();
useEffect(() => {
setCurrentPage(1);
}, [searchQuery])
useEffect(() => {
const fetchData = async () => {
const data = await API.fetchPosts(currentPage, searchQuery);
setPaginatedData(data);
}
fetchData();
}, [currentPage, searchQuery]);
const handleSearch = (query: string) => { setSearchQuery(query); setCurrentPage(1); };
const handlePostSelect = (id: number) => { navigate(`/posts/${id}`) };
const currentPosts = paginatedData?.items ?? [];
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
if (!paginatedData) {
return <RootLayout>
<Loading />
</RootLayout>;
}
return (
<RootLayout>
{paginatedData.items.length > 0 ?
<>
<SearchBar currentQuery={searchQuery} onSearch={handleSearch} />
<GridView posts={currentPosts} onPostSelect={handlePostSelect} />
<Pagination postsPerPage={paginatedData.pageSize} totalPosts={paginatedData.totalCount} paginate={paginate} currentPage={currentPage} />
</>
:
<div className="p-4 md:p-6 max-w-4xl mx-auto">
<div className="bg-white rounded-xl shadow-lg p-6 text-center">
<h2 className="text-2xl font-bold mb-4 text-gray-800">No Posts Found</h2>
<p className="text-gray-600 mb-6">Try adjusting your search to find what you're looking for.</p>
<button onClick={() => { setSearchQuery(''); setCurrentPage(1); }} className="bg-blue-500 text-white font-semibold px-4 py-2 rounded-md hover:bg-blue-600 transition-colors">
Clear Search
</button>
</div>
</div>
}
</RootLayout>
);
}