diff --git a/client/src/api/posts.js b/client/src/api/posts.js index 7202645..b5a9338 100644 --- a/client/src/api/posts.js +++ b/client/src/api/posts.js @@ -9,9 +9,11 @@ export const getPosts = async (cursor, limit) => { } } -export const searchPosts = async (query) => { +export const searchPosts = async (query, cursor, limit) => { try { - const { data } = await api.get('/posts/search', { params: { query } }) + const { data } = await api.get('/posts/search', { + params: { query, cursor, limit } + }) return data } catch (error) { throw handleApiError(error) diff --git a/client/src/api/tags.js b/client/src/api/tags.js index 06140be..6fb2cc3 100644 --- a/client/src/api/tags.js +++ b/client/src/api/tags.js @@ -17,3 +17,14 @@ export const searchTags = async (q) => { throw handleApiError(error) } } + +export const searchPostsByTags = async (tag, cursor, limit) => { + try { + const { data } = await api.get(`/tags/posts/${tag}`, { + params: { cursor, limit } + }) + return data + } catch (error) { + throw handleApiError(error) + } +} diff --git a/client/src/components/AccountMenu.jsx b/client/src/components/AccountMenu.jsx index e94b2ee..eef406e 100644 --- a/client/src/components/AccountMenu.jsx +++ b/client/src/components/AccountMenu.jsx @@ -20,10 +20,21 @@ import { Link } from 'react-router' import { useTheme } from '@/hooks' import { useState } from 'react' import { UserAvatar } from '.' +import { useStore } from '@/store' const AccountMenuItems = ({ handleClose, handleClick, open }) => { - const { user } = useUser() + const { user, sign } = useUser() const { signOut } = useAuth() + const { openSnackbar } = useStore() + + const logOut = () => { + try { + signOut() + openSnackbar('Logged in successfully') + } catch (error) { + openSnackbar(error.message, 'error') + } + } return ( <> @@ -52,7 +63,7 @@ const AccountMenuItems = ({ handleClose, handleClick, open }) => { - + diff --git a/client/src/components/PostCard.jsx b/client/src/components/PostCard.jsx index f9c0eff..b2204c0 100644 --- a/client/src/components/PostCard.jsx +++ b/client/src/components/PostCard.jsx @@ -177,17 +177,13 @@ const EditCard = ({ post, setEditing }) => { if (!validateInputs()) { return } - try { - updatePost({ - ...editedPost, - media: editedPost.media - ? await convertToBase64(editedPost.media) - : editedPost.imageUrl - }) - setEditing(false) - } catch (error) { - console.error(error) - } + updatePost({ + ...editedPost, + media: editedPost.media + ? await convertToBase64(editedPost.media) + : editedPost.imageUrl + }) + setEditing(false) } return ( @@ -333,15 +329,9 @@ const EditCard = ({ post, setEditing }) => { const StaticCard = ({ post, setEditing }) => { const { user } = useUser() const navigate = useNavigate() - console.log(post) + return ( - + { - const navigate = useNavigate() +const TrendingTags = () => { + const { data: tags, isLoading } = useTrendingTags() return ( Trending Topics - - {tags.map((tag) => ( - - - - ))} - + {isLoading ? ( + + ) : ( + + )} ) } +const TagsList = ({ tags }) => { + return ( + + {tags.map((tag) => ( + + + + ))} + + ) +} export default TrendingTags diff --git a/client/src/hooks/posts.js b/client/src/hooks/posts.js index 3c8a8a2..0c4caa1 100644 --- a/client/src/hooks/posts.js +++ b/client/src/hooks/posts.js @@ -23,6 +23,13 @@ export const useGetPosts = () => getNextPageParam: (lastPage) => lastPage.nextCursor }) +export const useSearchPosts = (query) => + useInfiniteQuery({ + queryKey: ['search-posts', query], + queryFn: ({ pageParam }) => searchPosts(query, pageParam, 6), + getNextPageParam: (lastPage) => lastPage.nextCursor + }) + export const useGetPost = (id) => useQuery({ queryKey: ['post', id], @@ -163,13 +170,6 @@ export const useDeletePost = () => { }) } -export const useSearchPosts = () => { - return useQuery({ - queryKey: ['searchPosts'], - queryFn: () => searchPosts - }) -} - export const useRefresh = () => { const queryClient = useQueryClient() const [refreshing, setRefreshing] = useState(false) diff --git a/client/src/hooks/tags.js b/client/src/hooks/tags.js index 674f0df..f00f749 100644 --- a/client/src/hooks/tags.js +++ b/client/src/hooks/tags.js @@ -1,5 +1,5 @@ -import { useQuery } from '@tanstack/react-query' -import { searchTags } from '@/api' +import { useInfiniteQuery, useQuery } from '@tanstack/react-query' +import { getTrendingTags, searchPostsByTags, searchTags } from '@/api' export const useSearchTags = (input) => useQuery({ @@ -9,3 +9,17 @@ export const useSearchTags = (input) => staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 30 }) + +export const useTrendingTags = () => + useQuery({ + queryKey: ['tags'], + queryFn: getTrendingTags, + staleTime: Infinity + }) + +export const useSearchPostsByTag = (tag) => + useInfiniteQuery({ + queryKey: ['search-tags', tag], + queryFn: ({ pageParam }) => searchPostsByTags(tag, pageParam, 6), + getNextPageParam: (lastPage) => lastPage.nextCursor + }) diff --git a/client/src/pages/LogIn.jsx b/client/src/pages/LogIn.jsx index 673979c..0e2b0a5 100644 --- a/client/src/pages/LogIn.jsx +++ b/client/src/pages/LogIn.jsx @@ -15,6 +15,7 @@ import { LockOutlined } from '@mui/icons-material' import { useSignIn } from '@clerk/clerk-react' import { OAuthButtons } from '@/components' import { useState } from 'react' +import { useStore } from '@/store' const Form = () => { const initialState = { email: '', password: '' } @@ -25,7 +26,7 @@ const Form = () => { const handleChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }) - + const { openSnackbar } = useStore() const handleSubmit = async (event) => { event.preventDefault() setError('') @@ -40,6 +41,7 @@ const Form = () => { if (result.status === 'complete') { await setActive({ session: result.createdSessionId }) + openSnackbar('Logged in successfully') navigate('/') } else { console.error('Sign-in failed', result) diff --git a/client/src/pages/Post.jsx b/client/src/pages/Post.jsx index 69065d8..0538fde 100644 --- a/client/src/pages/Post.jsx +++ b/client/src/pages/Post.jsx @@ -66,6 +66,7 @@ const PostCard = ({ post, isLoading, error }) => { if (error?.message === 'API Error: Post not found') { navigate('/not-found') } + document.title = `Memories | ${post.title}` return ( @@ -149,6 +150,7 @@ const Top3Reactions = ({ post }) => { const Post = () => { const { id } = useParams() const { data: post, isLoading, error } = useGetPost(id) + return ( diff --git a/client/src/pages/Posts.jsx b/client/src/pages/Posts.jsx index 0afa5e1..ad6ac0b 100644 --- a/client/src/pages/Posts.jsx +++ b/client/src/pages/Posts.jsx @@ -60,29 +60,6 @@ const PostGrid = () => { } const Posts = () => { - // const { data: tags } = useGetTrendingTags() - const tags = [ - { - hashtag: 'media', - count: 5 - }, - { - hashtag: 'post', - count: 5 - }, - { - hashtag: 'social', - count: 5 - }, - { - hashtag: 'love', - count: 3 - }, - { - hashtag: 'chocolates', - count: 2 - } - ] return ( @@ -90,7 +67,8 @@ const Posts = () => { container size={{ xs: 12, md: 8, xl: 9 }} overflow="auto" - height={'calc(100vh - 100px)'} + height={'calc(100vh - 170px)'} + minHeight={400} sx={{ '&::-webkit-scrollbar': { display: 'none' }, scrollbarWidth: 'none', @@ -112,8 +90,7 @@ const Posts = () => { - {/* */} - + diff --git a/client/src/pages/Search.jsx b/client/src/pages/Search.jsx index 992babc..14977d8 100644 --- a/client/src/pages/Search.jsx +++ b/client/src/pages/Search.jsx @@ -1,177 +1,89 @@ -import { useState, useEffect } from 'react' -import { useParams, useSearchParams } from 'react-router' -import { - Typography, - Tab, - Tabs, - Card, - CardContent, - List, - ListItem, - ListItemText, - ListItemAvatar, - Avatar, - Box, - Container, - Stack -} from '@mui/material' +import { useEffect } from 'react' +import { useNavigate, useParams, useSearchParams } from 'react-router' +import { Typography, Grid2 as Grid, Box, Container, Stack } from '@mui/material' +import { PostCard, PostCardSkeleton } from '@/components' +import { useSearchPosts, useSearchPostsByTag } from '@/hooks' +import { useInView } from 'react-intersection-observer' +import { useStore } from '@/store' -// Dummy data -const dummyData = { - posts: [ - { - id: 1, - title: 'First post about React', - content: 'React is awesome!' - }, - { - id: 2, - title: 'Learning TypeScript', - content: 'TypeScript makes coding safer.' - }, - { - id: 3, - title: 'Next.js for the win', - content: 'Next.js simplifies React development.' - } - ], - users: [ - { id: 1, name: 'John Doe', username: '@johndoe' }, - { id: 2, name: 'Jane Smith', username: '@janesmith' }, - { id: 3, name: 'Bob Johnson', username: '@bobjohnson' } - ], - comments: [ - { id: 1, user: 'John Doe', content: 'Great post!' }, - { id: 2, user: 'Jane Smith', content: 'I learned a lot from this.' }, - { id: 3, user: 'Bob Johnson', content: 'Thanks for sharing!' } - ] -} - -const Search = ({ hashtag }) => { - const [searchParams] = useSearchParams() - const { tag } = useParams() - const [searchTerm, setSearchTerm] = useState( - hashtag ? tag : searchParams.get('q') || '' - ) - const [activeTab, setActiveTab] = useState(0) - const [filteredData, setFilteredData] = useState(dummyData) - - useEffect(() => { - const filtered = { - posts: dummyData.posts.filter( - (post) => - post.title - .toLowerCase() - .includes(searchTerm.toLowerCase()) || - post.content - .toLowerCase() - .includes(searchTerm.toLowerCase()) - ), - users: dummyData.users.filter( - (user) => - user.name - .toLowerCase() - .includes(searchTerm.toLowerCase()) || - user.username - .toLowerCase() - .includes(searchTerm.toLowerCase()) - ), - comments: dummyData.comments.filter( - (comment) => - comment.user - .toLowerCase() - .includes(searchTerm.toLowerCase()) || - comment.content - .toLowerCase() - .includes(searchTerm.toLowerCase()) - ) - } - setFilteredData(filtered) - }, [searchTerm]) +const SearchGrid = ({ term, queryFn }) => { + const { ref, inView } = useInView() + const navigate = useNavigate() + const { + data: posts, + isPending, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + isError, + error + } = queryFn(term) + const { openSnackbar } = useStore() useEffect(() => { - const query = searchParams.get('q') - if (query) { - setSearchTerm(query) + if (inView && hasNextPage) { + fetchNextPage() } - }, [searchParams]) + }, [inView, hasNextPage, fetchNextPage]) - const handleTabChange = (_, newValue) => setActiveTab(newValue) + if (isPending) { + return Array.from({ length: 6 }).map((_, i) => ( + + + + )) + } - const renderContent = () => { - switch (activeTab) { - case 0: - return ( - - {filteredData.posts.map((post) => ( - - - - ))} - - ) - case 1: - return ( - - {filteredData.users.map((user) => ( - - - {user.name[0]} - - - - ))} - - ) - case 2: - return ( - - {filteredData.comments.map((comment) => ( - - - - ))} - - ) - default: - return null - } + if (isError) { + navigate('/') + openSnackbar(`Error loading posts: ${error.message}`, 'error') } + const allPosts = posts.pages.flatMap((page) => page.posts) + return ( + <> + {allPosts.map((post) => ( + + + + ))} + + {isFetchingNextPage && ( + + + + )} + + + ) +} + +const Search = ({ hashtag }) => { + const [searchParams] = useSearchParams() + const { tag } = useParams() + const searchTerm = hashtag ? tag : searchParams.get('q') + const queryFn = tag ? useSearchPostsByTag : useSearchPosts return ( - - - Search results for - {hashtag ? ` #${searchTerm}` : ` "${searchTerm}"`} - - - - - - - - {renderContent()} - - + + Search results for + {hashtag ? ` #${searchTerm}` : ` "${searchTerm}"`} + + + + )