diff --git a/src/components/Post/LivePostStream.jsx b/src/components/Post/LivePostStream.jsx new file mode 100644 index 00000000..820acc4b --- /dev/null +++ b/src/components/Post/LivePostStream.jsx @@ -0,0 +1,99 @@ +import { useEffect, useState } from 'react' +import { Grid } from '@material-ui/core' +import { useQuery, useSubscription } from '@apollo/react-hooks' +import PropTypes from 'prop-types' +import PostCard from './PostCard' +import { GET_TOP_POSTS } from '../../graphql/query' +import { NEW_POST_SUBSCRIPTION } from '../../graphql/subscription' + +function LivePostStream({ + paused, + searchKey, + startDateRange, + endDateRange, + friendsOnly, + enabled, +}) { + const { data, loading } = useQuery(GET_TOP_POSTS, { + variables: { + limit: 10, + offset: 0, + searchKey, + startDateRange, + endDateRange, + friendsOnly, + }, + fetchPolicy: 'network-only', + skip: !enabled, + }) + + const [posts, setPosts] = useState([]) + const [queued, setQueued] = useState([]) + + useEffect(() => { + if (data && data.posts) { + setPosts(data.posts.entities) + } + }, [data]) + + useSubscription(NEW_POST_SUBSCRIPTION, { + onSubscriptionData: ({ subscriptionData }) => { + const newPost = subscriptionData.data && subscriptionData.data.post + if (!newPost) return + if (paused) { + setQueued((q) => [newPost, ...q]) + } else { + setPosts((p) => [newPost, ...p]) + } + }, + }) + + useEffect(() => { + if (!paused && queued.length) { + setPosts((p) => [...queued, ...p]) + setQueued([]) + } + }, [paused, queued]) + + if (loading && posts.length === 0) { + return
Loading...
+ } + + return ( + + {posts.map((post) => ( + + + + ))} + + ) +} + +LivePostStream.propTypes = { + paused: PropTypes.bool, + searchKey: PropTypes.string, + startDateRange: PropTypes.string, + endDateRange: PropTypes.string, + friendsOnly: PropTypes.bool, + enabled: PropTypes.bool, +} + +LivePostStream.defaultProps = { + paused: false, + searchKey: '', + startDateRange: '', + endDateRange: '', + friendsOnly: false, + enabled: true, +} + +export default LivePostStream diff --git a/src/graphql/subscription.jsx b/src/graphql/subscription.jsx index eddd6b8b..12165e6a 100644 --- a/src/graphql/subscription.jsx +++ b/src/graphql/subscription.jsx @@ -37,3 +37,37 @@ export const NEW_NOTIFICATION_SUBSCRIPTION = gql` } } ` + +export const NEW_POST_SUBSCRIPTION = gql` + subscription post { + post { + _id + userId + title + text + upvotes + downvotes + bookmarkedBy + created + url + creator { + name + username + avatar + _id + } + votes { + _id + startWordIndex + endWordIndex + type + } + comments { _id } + quotes { _id } + messageRoom { + _id + messages { _id } + } + } + } +` diff --git a/src/mui-pro/Sidebar/Sidebar.jsx b/src/mui-pro/Sidebar/Sidebar.jsx index e788ec00..e0ee554f 100644 --- a/src/mui-pro/Sidebar/Sidebar.jsx +++ b/src/mui-pro/Sidebar/Sidebar.jsx @@ -13,12 +13,14 @@ import AppBar from "@material-ui/core/AppBar"; import Toolbar from "@material-ui/core/Toolbar"; import IconButton from "@material-ui/core/IconButton"; import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; import MenuIcon from "@material-ui/icons/Menu"; import sidebarStyle from "assets/jss/material-dashboard-pro-react/components/sidebarStyle"; import { SET_SELECTED_PAGE } from "../../store/ui"; import ChatMenu from "../../components/Chat/ChatMenu"; import NotificationMenu from "../../components/Notifications/NotificationMenu"; import SettingsMenu from "../../components/Settings/SettingsMenu"; +import SubmitPost from "../../components/SubmitPost/SubmitPost"; // We've created this component so we can have a ref to the wrapper of the links that appears in our sidebar. // This was necessary so that we could initialize PerfectScrollbar on the links. @@ -48,6 +50,7 @@ class MenuSidebar extends React.Component { openAvatar: false, miniActive: true, MessageDisplay: null, + postDialogOpen: false, ...this.getCollapseStates(props.routes) }; } @@ -274,6 +277,14 @@ class MenuSidebar extends React.Component { {loggedIn ? ( <> + @@ -313,6 +324,9 @@ class MenuSidebar extends React.Component { /> + this.setState({ postDialogOpen: false })}> + this.setState({ postDialogOpen: open })} /> + ); } diff --git a/src/routes.jsx b/src/routes.jsx index 89e2b0f4..d2589459 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -7,6 +7,7 @@ import LogoutPage from './components/LogoutPage' import HomeSvg from './assets/svg/Home' import ProfileAvatar from './components/Profile/ProfileAvatar' import NotificationMobileView from './components/Notifications/NotificationMobileView' +import SearchIcon from '@material-ui/icons/Search' import SearchPage from 'views/SearchPage' const routes = [ @@ -22,7 +23,7 @@ const routes = [ path: 'search', name: 'Search', rtlName: 'التقويم', - icon: () => Trending, + icon: SearchIcon, component: SearchPage, layout: '/', }, diff --git a/src/views/SearchView/SearchView.jsx b/src/views/SearchView/SearchView.jsx index f9a33014..3a17c373 100644 --- a/src/views/SearchView/SearchView.jsx +++ b/src/views/SearchView/SearchView.jsx @@ -1,9 +1,233 @@ -import React, { PureComponent } from 'react' +import { useState, useEffect } from 'react' +import { useSelector } from 'react-redux' +import { makeStyles } from '@material-ui/core/styles' +import { + IconButton, + Typography, + Button, + InputBase, + Paper, +} from '@material-ui/core' +import SearchIcon from '@material-ui/icons/Search' +import DatePicker from 'react-datepicker' +import 'react-datepicker/dist/react-datepicker.css' +import format from 'date-fns/format' +import PauseIcon from '@material-ui/icons/Pause' +import PlayArrowIcon from '@material-ui/icons/PlayArrow' +import LivePostStream from '../../components/Post/LivePostStream' -class SearchView extends PureComponent { - render() { - return
Search View Page(todo)
- } +const useStyles = makeStyles((theme) => ({ + root: { + padding: 16, + backgroundColor: '#f0f2f5', + minHeight: '100vh', + }, + controls: { + display: 'flex', + justifyContent: 'center', + marginBottom: 16, + gap: 8, + }, + iconsContainer: { + marginTop: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + gap: 8, + }, + searchBar: { + display: 'flex', + alignItems: 'center', + width: '100%', + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.common.white, + padding: theme.spacing(1, 2), + marginBottom: theme.spacing(2), + border: '1px solid #ddd', + }, + input: { + marginLeft: theme.spacing(1), + flex: 1, + }, + iconButton: { + padding: 10, + color: theme.palette.text.secondary, + }, + icon: { + margin: theme.spacing(0, 1), + color: theme.palette.text.secondary, + fontSize: '1.5rem', + transition: 'all 0.2s ease-in-out', + '&:hover': { + transform: 'scale(1.1)', + backgroundColor: theme.palette.action.hover, + }, + }, + activeFilter: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.dark, + }, + }, + datePickerContainer: { + padding: '16px', + backgroundColor: 'white', + borderRadius: '8px', + boxShadow: '0 4px 20px rgba(0,0,0,0.15)', + maxWidth: 400, + margin: '0 auto', + marginBottom: 16, + }, + datePickerInput: { + '& .react-datepicker-wrapper': { + width: '100%', + }, + '& .react-datepicker__input-container input': { + width: '100%', + padding: '12px 16px', + border: '1px solid #ddd', + borderRadius: '4px', + fontSize: '14px', + fontFamily: theme.typography.fontFamily, + '&:focus': { + outline: 'none', + borderColor: theme.palette.primary.main, + boxShadow: `0 0 0 2px ${theme.palette.primary.main}20`, + }, + }, + }, +})) + +function SearchView() { + const classes = useStyles() + const loggedIn = useSelector((state) => !!state.user.data._id) + const [paused, setPaused] = useState(false) + const [filterMode, setFilterMode] = useState('all') + const [isCalendarVisible, setIsCalendarVisible] = useState(false) + const [dateRangeFilter, setDateRangeFilter] = useState({ startDate: null, endDate: null }) + const [searchKey, setSearchKey] = useState('') + const [streamEnabled, setStreamEnabled] = useState(loggedIn) + + const togglePause = () => setPaused((p) => !p) + const handleFriendsFilter = () => setFilterMode((m) => (m === 'friends' ? 'all' : 'friends')) + const handleInteractionsFilter = () => setFilterMode((m) => (m === 'interactions' ? 'all' : 'interactions')) + const handleCalendarToggle = () => setIsCalendarVisible((v) => !v) + const handleDateChange = ([startDate, endDate]) => setDateRangeFilter({ startDate, endDate }) + const clearDateFilter = () => setDateRangeFilter({ startDate: null, endDate: null }) + + useEffect(() => { + if ( + streamEnabled && + ( + searchKey || + filterMode !== 'all' || + dateRangeFilter.startDate || + dateRangeFilter.endDate + ) + ) { + setStreamEnabled(false) + } + }, [searchKey, filterMode, dateRangeFilter.startDate, dateRangeFilter.endDate, streamEnabled]) + + const showLiveStream = + streamEnabled && + loggedIn && + !searchKey && + filterMode === 'all' && + !dateRangeFilter.startDate && + !dateRangeFilter.endDate + + return ( +
+
+ + {paused ? : } + + {paused ? 'Stream paused' : 'Live stream'} +
+ + e.preventDefault()}> + setSearchKey(e.target.value)} + /> + + + + + +
+ + 👥 + + + 🔥 + + + 📅 + +
+ + {isCalendarVisible && ( +
+
+ handleDateChange([date, dateRangeFilter.endDate])} + selectsStart + startDate={dateRangeFilter.startDate} + endDate={dateRangeFilter.endDate} + maxDate={dateRangeFilter.endDate || new Date()} + dateFormat="MMM d, yyyy" + placeholderText="Select start date" + /> +
+
+ handleDateChange([dateRangeFilter.startDate, date])} + selectsEnd + startDate={dateRangeFilter.startDate} + endDate={dateRangeFilter.endDate} + minDate={dateRangeFilter.startDate} + maxDate={new Date()} + dateFormat="MMM d, yyyy" + placeholderText="Select end date" + /> +
+ +
+ )} + + {showLiveStream && ( + + )} +
+ ) } export default SearchView