Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
66dd9dd
feat(post.dto): add optional imageUrl to CreatePostDTO
nuuxcode Apr 4, 2024
6c26c18
feat(post.service): handle imageUrl in post creation
nuuxcode Apr 4, 2024
47f0123
fix(side-bar): update image src to prevent caching
nuuxcode Apr 4, 2024
bcac607
feat(forum.dto): add optional logo and banner to CreateForumDTO
nuuxcode Apr 4, 2024
6d0481c
feat(forum.service): handle logo and banner in forum creation
nuuxcode Apr 4, 2024
f1baf53
feat(post.service): modify findAll to fetch all posts by default
nuuxcode Apr 4, 2024
295e959
feat(post.controller): add order query parameter to getAllPosts method
nuuxcode Apr 4, 2024
8ad9162
feat(post): add CommentInput component to post page
nuuxcode Apr 4, 2024
5fde9cb
feat(fetcher): add getUser function and update postComment function
nuuxcode Apr 4, 2024
377ee04
refactor(comment-service): update comment creation logic
nuuxcode Apr 4, 2024
890a6e4
feat(post-controller): add order query parameter to getCommentsByPost…
nuuxcode Apr 4, 2024
5f1a8f0
feat(post-service): add order parameter to findCommentsByPostId function
nuuxcode Apr 4, 2024
0ac9d57
feat(comment-input): add new CommentInput component
nuuxcode Apr 4, 2024
1b34e8f
feat: update application metadata
nuuxcode Apr 4, 2024
fb48fe1
feat: add search functionality
nuuxcode Apr 4, 2024
66c28e3
feat: enable post search on server side
nuuxcode Apr 4, 2024
7a33de3
feat: remove HighlightedPost component
nuuxcode Apr 4, 2024
9d4ab2f
fix(fetcher): add getUser and getMe functions
nuuxcode Apr 5, 2024
730e2d0
style(search): add styling changes to SearchBox component
nuuxcode Apr 5, 2024
75b8d77
feat(user): implement user-related functions in UserController and Us…
nuuxcode Apr 5, 2024
cec1041
feat(forum): implement forum subscription-related functions in ForumC…
nuuxcode Apr 5, 2024
217f835
feat(vote): implement votePost function in VoteController and VoteSer…
nuuxcode Apr 5, 2024
393cec1
fix(commentInput): fix typo in useFetcher function name
nuuxcode Apr 5, 2024
e28821b
feat(profile): add new directory for user profile
nuuxcode Apr 5, 2024
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
3 changes: 2 additions & 1 deletion client/src/app/(post)/post/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Button } from "~/components/ui/button";
import { FaPlus } from "react-icons/fa6";
import FullPost from "~/components/posts/fullpost";
import Comment from "~/components/comment/comment";

import CommentInput from "~/components/comment/commentInput";
function Page({ params }: { params: { slug: string } }) {
const { getPost, getComments } = useFetcher();
const { data: postData, error: postError } = useSWR(`getPost/${params.slug}`, getPost);
Expand All @@ -21,6 +21,7 @@ function Page({ params }: { params: { slug: string } }) {
{postData && (
<div>
<FullPost post={postData} />
<CommentInput postId={postData.id} />
<Comment comments={commentsData} />
</div>
)}
Expand Down
208 changes: 208 additions & 0 deletions client/src/app/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"use client";

import { useFetcher } from "~/hooks/fetcher";
import React from "react";
import { useState } from 'react';
import useSWR from 'swr';
import Link from 'next/link';

function UserProfile({ params }: { params: { username: string } }) {
const { getUser, getUserPosts, getUserComments, getUserFollowers, getUserFollowing, getUserSubscriptions, getUserPostVotes, getUserCommentVotes } = useFetcher();
const { data: userProfile, error } = useSWR(`getUser/${params.username}`, getUser);

const [selectedTab, setSelectedTab] = useState('posts');
const endpoint = selectedTab === 'posts' ? `getUserPosts/${params.username}` :
selectedTab === 'comments' ? `getUserComments/${params.username}` :
selectedTab === 'followers' ? `getUserFollowers/${params.username}` :
selectedTab === 'following' ? `getUserFollowing/${params.username}` :
selectedTab === 'subscriptions' ? `getUserSubscriptions/${params.username}` :
selectedTab === 'postVotes' ? `getUserPostVotes/${params.username}` :
`getUserCommentVotes/${params.username}`;

const fetcher = selectedTab === 'posts' ? getUserPosts :
selectedTab === 'comments' ? getUserComments :
selectedTab === 'followers' ? getUserFollowers :
selectedTab === 'following' ? getUserFollowing :
selectedTab === 'subscriptions' ? getUserSubscriptions :
selectedTab === 'postVotes' ? getUserPostVotes :
getUserCommentVotes;

const { data: tabData } = useSWR(endpoint, fetcher);
const dataToDisplay = selectedTab === 'followers' ? tabData?.followers :
selectedTab === 'following' ? tabData?.followings :
tabData;
console.log(endpoint, dataToDisplay);
if (error) return <div>Error: {error.message}</div>
if (!userProfile) return <div>Loading...</div>

return (
<div className="flex flex-col justify-between w-full max-w-6xl mx-auto">
<div className="flex flex-col md:flex-row lg:flex-row justify-between w-full">
<div className="w-full md:w-1/3 p-4">
<div className="dark:bg-secondary dark:text-[#d8dce0] bg-white rounded-lg shadow-md p-6">
<img className="w-24 h-24 -mt-12 border-4 border-white rounded-full mx-auto shadow-lg" src={userProfile.avatarUrl} alt={userProfile.username} />
<h1 className="mb-2 text-xl font-bold">{userProfile.username}</h1>
<p className="mb-2 text-sm text-gray-500 dark:text-[#d8dce0]">{userProfile.email}</p>
<p className="mb-2 text-sm dark:text-[#d8dce0]">{userProfile.aboutMe}</p>
<h2 className="text-lg font-semibold">Social Media</h2>
<div className="flex space-x-2 mt-2">
<a href={userProfile.github} target="_blank" rel="noopener noreferrer">GitHub</a>
<a href={userProfile.twitter} target="_blank" rel="noopener noreferrer">Twitter</a>
<a href={userProfile.linkedin} target="_blank" rel="noopener noreferrer">LinkedIn</a>
</div>
</div>
</div>
<div className="w-full md:w-1/3 p-4">
<div className="dark:bg-secondary dark:text-[#d8dce0] bg-white rounded-lg shadow-md p-6">
<h2 className="text-lg font-semibold">Stats</h2>
<div className="flex flex-wrap flex-col md:flex-row lg:flex-row justify-between mt-2">
<div className="text-center mx-2">Reputation: {userProfile.reputation}</div>
<div className="text-center mx-2">Posts: {userProfile.PostCount}</div>
<div className="text-center mx-2">Comments: {userProfile.CommentCount}</div>
<div className="text-center mx-2">Followers: {userProfile.followersCount}</div>
<div className="text-center mx-2">Following: {userProfile.followingCount}</div>
</div>
</div>
</div>
<div className="w-full md:w-1/3 p-4">
<div className="dark:bg-secondary dark:text-[#d8dce0] bg-white rounded-lg shadow-md p-6">
<h2 className="text-lg font-semibold">Basic Information</h2>
<div className="mt-2">
<div>Last Login: {new Date(userProfile.lastLogin).toLocaleString() || 'Not available'}</div>
<div>Country: {userProfile.country || 'Not available'}</div>
<div>City: {userProfile.city || 'Not available'}</div>
<div>Phone: {userProfile.phone || 'Not available'}</div>
<div>Website: {userProfile.website || 'Not available'}</div>
<div>Join Time: {new Date(userProfile.createdAt).toLocaleString() || 'Not available'}</div>
</div>
</div>
</div>
</div>
<div className="w-full p-4">
<div className="dark:bg-secondary dark:text-[#d8dce0] bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between">
<button
className={`p-2 ${selectedTab === 'posts' ? 'text-orange-500' : 'text-gray-500'}`}
onClick={() => setSelectedTab('posts')}
>
Posts
</button>
<button
className={`p-2 ${selectedTab === 'comments' ? 'text-orange-500' : 'text-gray-500'}`}
onClick={() => setSelectedTab('comments')}
>
Comments
</button>
<button
className={`p-2 ${selectedTab === 'followers' ? 'text-orange-500' : 'text-gray-500'}`}
onClick={() => setSelectedTab('followers')}
>
Followers
</button>
<button
className={`p-2 ${selectedTab === 'following' ? 'text-orange-500' : 'text-gray-500'}`}
onClick={() => setSelectedTab('following')}
>
Following
</button>
<button
className={`p-2 ${selectedTab === 'subscriptions' ? 'text-orange-500' : 'text-gray-500'}`}
onClick={() => setSelectedTab('subscriptions')}
>
Subscriptions
</button>
<button
className={`p-2 ${selectedTab === 'postVotes' ? 'text-orange-500' : 'text-gray-500'}`}
onClick={() => setSelectedTab('postVotes')}
>
Post Votes
</button>
<button
className={`p-2 ${selectedTab === 'commentVotes' ? 'text-orange-500' : 'text-gray-500'}`}
onClick={() => setSelectedTab('commentVotes')}
>
Comment Votes
</button>
</div>
<div className="mt-4">
{dataToDisplay ? (
dataToDisplay.length > 0 ? (
dataToDisplay.map(item => {
switch (selectedTab) {
case 'posts':
return (
<Link href={`/post/${item.id}`} key={item.id}>
<div className="bg-white shadow rounded-lg p-4 mb-4">
<h2 className="text-gray-700">{item.title}</h2>
<p className="text-gray-700">{item.content}</p>
</div>
</Link>
);
case 'comments':
return (
<Link href={`/post/${item.postId}`} key={item.id}>
<div className="bg-white shadow rounded-lg p-4 mb-4">
<p className="text-gray-700">{item.content}</p>
<p>Upvotes: {item.upvotesCount}, Downvotes: {item.downvotesCount}</p>
</div>
</Link>
);
case 'followers':
case 'following':
return (
<Link href={`/profile/${item.username}`} key={item.id}>
<div className="bg-white shadow rounded-lg p-4 mb-4">
<img src={item.avatarUrl} alt={item.username} />
<p className="text-gray-700">{item.username}</p>
<p>Reputation: {item.reputation}</p>
</div>
</Link>
);
case 'subscriptions':
return (
<Link href={`/forum/${item.id}`} key={item.id}>
<div className="bg-white shadow rounded-lg p-4 mb-4">
<img src={item.logo} alt={item.name} />
<h2 className="text-gray-700">{item.name}</h2>
<p className="text-gray-700">{item.description}</p>
</div>
</Link>
);
case 'postVotes':
return (
<Link href={`/post/${item.postId}`} key={item.id}>
<div className="bg-white shadow rounded-lg p-4 mb-4">
<p className="text-gray-700">{item.content}</p>
<p>Vote status: {item.voteStatus}, Upvotes: {item.upvotesCount}, Downvotes: {item.downvotesCount}</p>
</div>
</Link>
);
case 'commentVotes':
return (
<Link href={`/post/${item.postId}`} key={item.id}>
<div className="bg-white shadow rounded-lg p-4 mb-4">
<p className="text-gray-700">{item.content}</p>
<p>Vote status: {item.voteStatus}, Upvotes: {item.upvotesCount}, Downvotes: {item.downvotesCount}</p>
</div>
</Link>
);
default:
return null;
}
})
) : (
<p>No data available.</p>
)
) : (
<div className="flex justify-center items-center h-12">
<p className="text-gray-500">Loading...</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};

export default UserProfile;
4 changes: 2 additions & 2 deletions client/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { dark } from "@clerk/themes";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "WeGotThis Community",
description: "You are not alone. WeGotThis Community is here to help you.",
};

export default function RootLayout({
Expand Down
29 changes: 29 additions & 0 deletions client/src/app/search/[query]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import React from "react";
import { useFetcher } from "~/hooks/fetcher";
import useSWR from "swr";
import Post from "~/components/posts/post";
import { Separator } from "@radix-ui/react-separator";


function SearchPage({ params }: { params: { query: string } }) {
const { searchPosts } = useFetcher();
const { data: results, error } = useSWR(`searchPosts/${params.query}`, searchPosts);

if (error) return <div>Failed to load</div>
if (!results) return <div>Loading...</div>

return (
<div>
{results.map((post: any) => (
<div key={post.id} className="flex flex-col gap-8">
<Post post={post} />
<Separator className="bg-[#64748B] bg-opacity-30 h-[2px]" />
</div>
))}
</div>
);
};

export default SearchPage;
56 changes: 56 additions & 0 deletions client/src/components/comment/commentInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useFetcher } from '~/hooks/fetcher';
import Image from 'next/image';
import Link from 'next/link';
import { useState, useEffect } from 'react';

interface CommentInputProps {
postId: string;
}
interface User {
username: string;
avatarUrl: string;
}
const CommentInput: React.FC<CommentInputProps> = ({ postId }) => {
const [content, setContent] = useState('');
const [user, setUser] = useState<User | null>(null);
const { postComment, getMe } = useFetcher();

useEffect(() => {
const fetchUser = async () => {
const userData = await getMe();
setUser(userData);
};

fetchUser();
}, []);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await postComment(`postComment/${postId}`, content, null);
setContent('');
};

return (
<div className="flex gap-2 items-center py-2">
{user && (
<>
<Link href={`/${user.username}`}>
<Image
className="rounded-full h-8 w-8 md:w-10 md:h-10"
src={user.avatarUrl || `https://eu.ui-avatars.com/api/?name=${encodeURIComponent(user.username)}&size=250`}
alt="logo"
width={40}
height={400}
/>
</Link>
<form onSubmit={handleSubmit} className="flex flex-col justify-center">
<textarea value={content} onChange={e => setContent(e.target.value)} className="dark:text-primary text-sm" />
<button type="submit" className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-2">Post Comment</button>
</form>
</>
)}
</div>
);
};

export default CommentInput;
Loading