Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated card styling #41 #72

Closed
wants to merge 1 commit into from
Closed
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
104 changes: 75 additions & 29 deletions frontend/src/pages/Posts.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { useEffect, useState, useRef } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
import { PostData } from '../types';
import Loader from '../components/Loader';
import { useEffect, useState, useRef } from "react";
import axios from "axios";
import { Link } from "react-router-dom";
import { PostData } from "../types";
import Loader from "../components/Loader";

const Posts = () => {
const [posts, setPosts] = useState<PostData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [error, setError] = useState("");
const [showFilterDialog, setShowFilterDialog] = useState(false);
const [tagInput, setTagInput] = useState('');
const [tagInput, setTagInput] = useState("");
const [filterTags, setFilterTags] = useState<string[]>([]);
const filterRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const fetchPosts = async () => {
try {
const response = await axios.get('/api/v1/posts');
const response = await axios.get("/api/v1/posts");
setPosts(response.data.posts);
setLoading(false);
} catch (error) {
setError('Failed to fetch posts');
setError("Failed to fetch posts");
setLoading(false);
}
};
Expand All @@ -30,14 +30,17 @@ const Posts = () => {

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

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

Expand All @@ -48,44 +51,61 @@ const Posts = () => {
const addTag = () => {
if (tagInput && !filterTags.includes(tagInput.toLowerCase())) {
setFilterTags([...filterTags, tagInput.toLowerCase()]);
setTagInput('');
setTagInput("");
}
};

const removeTag = (tagToRemove: string) => {
setFilterTags(filterTags.filter(tag => tag !== tagToRemove));
setFilterTags(filterTags.filter((tag) => tag !== tagToRemove));
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
if (event.key === "Enter") {
event.preventDefault();
addTag();
}
};

const filteredPosts = posts.filter(post =>
filterTags.every(tag => post.tags.map(t => t.toLowerCase()).includes(tag))
const filteredPosts = posts.filter((post) =>
filterTags.every((tag) =>
post.tags.map((t) => t.toLowerCase()).includes(tag)
)
);

if (loading) {
return <Loader/>;
return <Loader />;
}

if (error) {
return <div className='text-red-500 font-semibold text-lg text-center'>{error}</div>;
return (
<div className="text-red-500 font-semibold text-lg text-center">
{error}
</div>
);
}

return (
<div className='max-w-screen-xl flex flex-col items-center justify-center mx-auto p-4'>
<div className="max-w-screen-xl flex flex-col items-center justify-center mx-auto p-4">
<h1 className="text-2xl font-semibold mb-4 text-white">Posts</h1>
<div className="w-full flex justify-between mb-4 relative">
<button
onClick={toggleFilterDialog}
className="flex items-center text-white hover:text-gray-400"
>
Filter
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
<svg
className="w-4 h-4 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
></path>
</svg>
</button>
{showFilterDialog && (
Expand All @@ -111,13 +131,20 @@ const Posts = () => {
</button>
<div className="mt-2 flex flex-wrap">
{filterTags.map((tag, index) => (
<div key={index} className="flex items-center bg-gray-700 text-white px-2 py-1 rounded mr-2 mb-2">
<div
key={index}
className="flex items-center bg-gray-700 text-white px-2 py-1 rounded mr-2 mb-2"
>
<span>{tag}</span>
<button
onClick={() => removeTag(tag)}
className="ml-2 focus:outline-none"
>
<svg className="w-4 h-4 fill-current text-red-500" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<svg
className="w-4 h-4 fill-current text-red-500"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 0c-5.523 0-10 4.477-10 10s4.477 10 10 10 10-4.477 10-10-4.477-10-10-10zm5 13.586-1.414 1.414-3.586-3.586-3.586 3.586-1.414-1.414 3.586-3.586-3.586-3.586 1.414-1.414 3.586 3.586 3.586-3.586 1.414 1.414-3.586 3.586 3.586 3.586z" />
</svg>
</button>
Expand All @@ -129,16 +156,35 @@ const Posts = () => {
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 w-full">
{filteredPosts.map((post) => (
<div key={post.id} className="bg-gray-800 border border-gray-600 p-4 rounded">
<h2 className="text-lg font-semibold mb-2 text-white">{post.title}</h2>
<p className="text-gray-400 mb-2">{post.description.length > 100 ? `${post.description.slice(0, 100)}...` : post.description}</p>
<div
key={post.id}
className="bg-gray-800 border border-gray-600 p-6 rounded-lg shadow-lg hover:shadow-2xl transition-shadow duration-300"
>
<h2 className="text-lg font-semibold mb-2 text-white">
{post.title}
</h2>
<p className="text-gray-400 mb-2">
{post.description.length > 100
? `${post.description.slice(0, 100)}...`
: post.description}
</p>
<p className="text-gray-500">By: {post.author.username}</p>
<div className="mt-2 flex flex-wrap">
{post.tags.map((tag, index) => (
<span key={index} className="text-sm bg-gray-700 text-white px-2 py-1 rounded mr-2 mb-2">{tag}</span>
<span
key={index}
className="text-sm bg-gray-700 text-white px-2 py-1 rounded-full mr-2 mb-2"
>
{tag}
</span>
))}
</div>
<Link to={`/app/posts/${post.id}`} className="text-blue-400 hover:text-blue-300">Read more</Link>
<Link
to={`/app/posts/${post.id}`}
className="text-blue-400 hover:text-blue-300 mt-2 inline-block"
>
Read more
</Link>
</div>
))}
</div>
Expand Down