diff --git a/.env.development b/.env.development index 3aae4b84..70197e9e 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,6 @@ SKIP_PREFLIGHT_CHECK=true REACT_APP_DOMAIN=https://quote.vote -NODE_ENV=production -REACT_APP_SERVER=https://api.quote.vote -REACT_APP_SERVER_WS=ws://api.quote.vote +NODE_ENV=development +REACT_APP_SERVER=https://sandbox.quote.vote +REACT_APP_SERVER_WS=ws://sandbox.quote.vote HUSKY_SKIP_HOOKS=1 git rebase diff --git a/README.md b/README.md index 7229704e..d8d23153 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ and democratic technologists alike. `git clone https://github.com/QuoteVote/quotevote-react.git && cd quotevote-react` 3. Create a `.env` file and add the following: ``` - NODE_ENV=dev - REACT_APP_SERVER=https://api.quote.vote - REACT_APP_SERVER_WS=wss://api.quote.vote + NODE_ENV=development + REACT_APP_SERVER=https://sandbox.quote.vote + REACT_APP_SERVER_WS=wss://sandbox.quote.vote REACT_APP_DOMAIN=https://quote.vote ``` diff --git a/src/graphql/mutations.jsx b/src/graphql/mutations.jsx index c2c89e9d..8420f304 100644 --- a/src/graphql/mutations.jsx +++ b/src/graphql/mutations.jsx @@ -9,7 +9,8 @@ export const CREATE_GROUP = gql` url created } - }` + } +` export const SUBMIT_POST = gql` mutation addPost($post: PostInput!) { @@ -70,7 +71,7 @@ export const ADD_QUOTE = gql` export const UPDATE_POST_BOOKMARK = gql` mutation updatePostBookmark($postId: String!, $userId: String!) { updatePostBookmark(postId: $postId, userId: $userId) { - _id, + _id bookmarkedBy } } @@ -100,12 +101,12 @@ export const FOLLOW_MUTATION = gql` ` export const REQUEST_USER_ACCESS_MUTATION = gql` -mutation requestUserAccess($requestUserAccessInput: RequestUserAccessInput!) { - requestUserAccess(requestUserAccessInput: $requestUserAccessInput) { - _id - email + mutation requestUserAccess($requestUserAccessInput: RequestUserAccessInput!) { + requestUserAccess(requestUserAccessInput: $requestUserAccessInput) { + _id + email + } } -} ` export const SEND_INVESTOR_EMAIL = gql` @@ -143,10 +144,7 @@ export const UPDATE_USER = gql` ` export const UPDATE_USER_AVATAR = gql` - mutation updateUserAvatar( - $user_id: String! - $avatarQualities: JSON - ) { + mutation updateUserAvatar($user_id: String!, $avatarQualities: JSON) { updateUserAvatar(user_id: $user_id, avatarQualities: $avatarQualities) { _id username @@ -158,9 +156,7 @@ export const UPDATE_USER_AVATAR = gql` ` export const CREATE_POST_MESSAGE_ROOM = gql` - mutation createPostMessageRoom( - $postId: String! - ) { + mutation createPostMessageRoom($postId: String!) { createPostMessageRoom(postId: $postId) { _id users @@ -214,7 +210,7 @@ export const ADD_ACTION_REACTION = gql` ` export const UPDATE_MESSAGE_REACTION = gql` - mutation updateReaction($_id: String! $emoji: String!) { + mutation updateReaction($_id: String!, $emoji: String!) { updateReaction(_id: $_id, emoji: $emoji) { _id emoji @@ -223,7 +219,7 @@ export const UPDATE_MESSAGE_REACTION = gql` ` export const UPDATE_ACTION_REACTION = gql` - mutation updateReaction($_id: String! $emoji: String!) { + mutation updateReaction($_id: String!, $emoji: String!) { updateReaction(_id: $_id, emoji: $emoji) { _id emoji @@ -247,3 +243,12 @@ export const DELETE_POST = gql` } } ` + +export const UPDATE_FEATURED_SLOT = gql` + mutation updateFeaturedSlot($postId: String!, $featuredSlot: Int) { + updateFeaturedSlot(postId: $postId, featuredSlot: $featuredSlot) { + _id + featuredSlot + } + } +` diff --git a/src/graphql/query.jsx b/src/graphql/query.jsx index 5de63f46..33db6611 100644 --- a/src/graphql/query.jsx +++ b/src/graphql/query.jsx @@ -1,18 +1,18 @@ import gql from 'graphql-tag' export const GROUPS_QUERY = gql` -query groups($limit: Int!) { - groups(limit: $limit) { - _id - creatorId - adminIds - allowedUserIds - privacy - title - url - description + query groups($limit: Int!) { + groups(limit: $limit) { + _id + creatorId + adminIds + allowedUserIds + privacy + title + url + description + } } -} ` export const USER_INVITE_REQUESTS = gql` @@ -162,7 +162,7 @@ export const GET_CHAT_ROOMS = gql` ` export const GET_ROOM_MESSAGES = gql` - query messages ($messageRoomId: String!){ + query messages($messageRoomId: String!) { messages(messageRoomId: $messageRoomId) { _id messageRoomId @@ -183,7 +183,7 @@ export const GET_ROOM_MESSAGES = gql` ` export const GET_MESSAGE_REACTIONS = gql` - query messageReactions ($messageId: String!){ + query messageReactions($messageId: String!) { messageReactions(messageId: $messageId) { _id emoji @@ -194,7 +194,7 @@ export const GET_MESSAGE_REACTIONS = gql` ` export const GET_ACTION_REACTIONS = gql` - query actionReactions ($actionId: String!){ + query actionReactions($actionId: String!) { actionReactions(actionId: $actionId) { _id emoji @@ -342,101 +342,101 @@ export const GET_USER = gql` ` export const GET_USER_ACTIVITY = gql` -query activities( - $user_id: String! - $limit: Int! - $offset: Int! - $searchKey: String! - $startDateRange: String - $endDateRange: String - $activityEvent: JSON! -) { - activities( - user_id: $user_id - limit: $limit - offset: $offset - searchKey: $searchKey - startDateRange: $startDateRange - endDateRange: $endDateRange - activityEvent: $activityEvent + query activities( + $user_id: String! + $limit: Int! + $offset: Int! + $searchKey: String! + $startDateRange: String + $endDateRange: String + $activityEvent: JSON! ) { - entities { - created - postId - userId - user { - _id - name - username - avatar - } - activityType - content - post { - _id - title - text - url - upvotes - downvotes - votes { + activities( + user_id: $user_id + limit: $limit + offset: $offset + searchKey: $searchKey + startDateRange: $startDateRange + endDateRange: $endDateRange + activityEvent: $activityEvent + ) { + entities { + created + postId + userId + user { _id + name + username + avatar } - quotes { + activityType + content + post { _id + title + text + url + upvotes + downvotes + votes { + _id + } + quotes { + _id + } + comments { + _id + } + messageRoom { + _id + messages { + _id + } + } + bookmarkedBy + created + creator { + _id + name + username + avatar + } } - comments { + voteId + vote { _id + startWordIndex + endWordIndex + created + type + tags } - messageRoom { + commentId + comment { _id - messages { - _id - } + created + userId + content + startWordIndex + endWordIndex } - bookmarkedBy - created - creator { + quoteId + quote { _id - name - username - avatar + startWordIndex + endWordIndex + created + quote } } - voteId - vote { - _id - startWordIndex - endWordIndex - created - type - tags - } - commentId - comment { - _id - created - userId - content - startWordIndex - endWordIndex - } - quoteId - quote { - _id - startWordIndex - endWordIndex - created - quote + pagination { + total_count + limit + offset } } - pagination { - total_count - limit - offset - } } -} ` export const GET_CHECK_DUPLICATE_EMAIL = gql` @@ -451,18 +451,18 @@ export const VERIFY_PASSWORD_RESET_TOKEN = gql` ` export const GET_FOLLOW_INFO = gql` - query getUserFollowInfo($username: String! $filter: String) { + query getUserFollowInfo($username: String!, $filter: String) { getUserFollowInfo(username: $username, filter: $filter) } ` export const GET_NOTIFICATIONS = gql` - query notifications{ - notifications{ + query notifications { + notifications { _id userId userIdBy - userBy{ + userBy { _id name avatar @@ -493,3 +493,42 @@ export const GET_LATEST_QUOTES = gql` } } ` +export const GET_FEATURED_POSTS = gql` + query featuredPosts { + featuredPosts { + _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/views/ControlPanel/ControlPanel.jsx b/src/views/ControlPanel/ControlPanel.jsx index e0ad6410..e6d3c494 100644 --- a/src/views/ControlPanel/ControlPanel.jsx +++ b/src/views/ControlPanel/ControlPanel.jsx @@ -1,3 +1,4 @@ +import React from 'react' import PropTypes from 'prop-types' import cx from 'classnames' import Grid from '@material-ui/core/Grid' @@ -10,13 +11,20 @@ import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' +import Select from '@material-ui/core/Select' +import MenuItem from '@material-ui/core/MenuItem' +import FormControl from '@material-ui/core/FormControl' +import TextField from '@material-ui/core/TextField' import Button from '@material-ui/core/Button' import Skeleton from '@material-ui/lab/Skeleton' import makeStyles from '@material-ui/core/styles/makeStyles' import { useMutation, useQuery } from '@apollo/react-hooks' -import { USER_INVITE_REQUESTS } from '@/graphql/query' -import { UPDATE_USER_INVITE_STATUS } from '@/graphql/mutations' +import { USER_INVITE_REQUESTS, GET_TOP_POSTS } from '@/graphql/query' +import { + UPDATE_USER_INVITE_STATUS, + UPDATE_FEATURED_SLOT, +} from '@/graphql/mutations' // react plugin for creating charts import ChartistGraph from 'react-chartist' @@ -31,16 +39,20 @@ const useStyles = makeStyles(controlPanelStylwa) const ActionButtons = ({ status, id }) => { const classes = useStyles() - const [sendUserInviteApproval, { loading }] = useMutation(UPDATE_USER_INVITE_STATUS) + const [sendUserInviteApproval, { loading }] = useMutation( + UPDATE_USER_INVITE_STATUS, + ) const submitData = async (selectedStatus) => { await sendUserInviteApproval({ variables: { userId: id, inviteStatus: `${selectedStatus}`, }, - refetchQueries: [{ - query: USER_INVITE_REQUESTS, - }], + refetchQueries: [ + { + query: USER_INVITE_REQUESTS, + }, + ], }) } @@ -119,6 +131,142 @@ const ActionButtons = ({ status, id }) => { } } +const FeaturedPostsTable = () => { + const classes = useStyles() + const queryVars = { + limit: 50, + offset: 0, + searchKey: '', + startDateRange: null, + endDateRange: null, + friendsOnly: false, + } + const { data, refetch } = useQuery(GET_TOP_POSTS, { + variables: queryVars, + }) + const [updateSlot, { loading }] = useMutation(UPDATE_FEATURED_SLOT) + const [selection, setSelection] = React.useState({}) + const [filter, setFilter] = React.useState('') + + if (!data) { + return + } + + const posts = data.posts.entities + const usedSlots = {} + posts.forEach((p) => { + if (p.featuredSlot) usedSlots[p.featuredSlot] = p._id + }) + + const filteredPosts = posts.filter((p) => { + const q = filter.toLowerCase() + return ( + p.title.toLowerCase().includes(q) || + (p.text || '').toLowerCase().includes(q) || + p._id.includes(q) + ) + }) + + const handleSelect = (id) => (e) => { + setSelection({ ...selection, [id]: e.target.value }) + } + + const handleSave = async (id) => { + const slot = selection[id] + await updateSlot({ + variables: { postId: id, featuredSlot: slot ? Number(slot) : null }, + }) + refetch() + } + + return ( + + + Featured Posts + setFilter(e.target.value)} + className={classes.filterInput} + /> + + + + + + Post ID + + + Title + + + Summary + + + Featured Slot + + + Action + + + + + {filteredPosts.map((post) => ( + + {post._id} + {post.title} + {(post.text || '').slice(0, 100)} + + + + + + + + + + ))} + +
+
+
+
+ ) +} + const ControlPanelContainer = ({ data }) => { const classes = useStyles() const header = ['Email', 'Status', 'Action'] @@ -141,11 +289,17 @@ const ControlPanelContainer = ({ data }) => { } } // eslint-disable-next-line radix - const inviteRequestCount = data.userInviteRequests.filter((user) => parseInt(user.status) === 1).length + const inviteRequestCount = data.userInviteRequests.filter( + (user) => parseInt(user.status) === 1, + ).length const totalUsers = data.userInviteRequests.length const result = data.userInviteRequests.reduce((_r, { joined }) => { const dateObj = moment(joined).format('yyyy-MM-01') - const objectKey = dateObj.toLocaleString('en-us', { year: 'numeric', month: 'numeric', day: 'numeric' }) + const objectKey = dateObj.toLocaleString('en-us', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + }) const r = { ..._r } // decouple instance if (!r[objectKey]) r[objectKey] = { objectKey, entries: 1 } else r[objectKey].entries++ @@ -155,7 +309,10 @@ const ControlPanelContainer = ({ data }) => { const lineSeries = labels.map((label) => result[label].entries) const formatLabels = labels.map((label) => { const dateObj = new Date(label) - return dateObj.toLocaleString('en-us', { month: 'numeric', year: 'numeric' }) + return dateObj.toLocaleString('en-us', { + month: 'numeric', + year: 'numeric', + }) }) const chartData = { labels: formatLabels, @@ -177,15 +334,23 @@ const ControlPanelContainer = ({ data }) => { return ( - Invite Control Panel + + Invite Control Panel + - User Invitation Requests + + User Invitation Requests + - +
{header.map((name) => ( @@ -231,10 +396,7 @@ const ControlPanelContainer = ({ data }) => { - + User Invitation Statistics { display="inline" style={{ float: 'right' }} > - Invite Requests: - {' '} - {inviteRequestCount || 0} + Invite Requests: {inviteRequestCount || 0} - - Total Users: - {' '} - {totalUsers || 0} + + Total Users: {totalUsers || 0} - Active Users Today: - {' '} - {activeUsersCount} + Active Users Today: {activeUsersCount} + + + ) @@ -284,12 +444,10 @@ const ControlPanel = () => { const classes = useStyles() const { admin } = useSelector((state) => state.user.data) if (!admin) { - return () + return } if (data) { - return ( - - ) + return } return ( diff --git a/src/views/ControlPanel/controlPanelStyles.js b/src/views/ControlPanel/controlPanelStyles.js index 1c1f3488..1baf6341 100644 --- a/src/views/ControlPanel/controlPanelStyles.js +++ b/src/views/ControlPanel/controlPanelStyles.js @@ -87,6 +87,15 @@ const requestAccessStyles = (theme) => ({ lineHeight: 1.39, color: '#333333', }, + featuredRow: { + backgroundColor: '#fff8e1', + }, + slotSelect: { + minWidth: 80, + }, + filterInput: { + marginBottom: 15, + }, }) export default requestAccessStyles diff --git a/src/views/SearchPage/index.jsx b/src/views/SearchPage/index.jsx index 8356cc0d..6561093e 100644 --- a/src/views/SearchPage/index.jsx +++ b/src/views/SearchPage/index.jsx @@ -8,7 +8,7 @@ import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import format from 'date-fns/format'; import { jwtDecode } from 'jwt-decode'; -import { GET_TOP_POSTS } from '../../graphql/query'; +import { GET_TOP_POSTS, GET_FEATURED_POSTS } from '../../graphql/query'; import { serializePost } from '../../utils/objectIdSerializer'; import PostsList from '../../components/Post/PostsList'; import ErrorBoundary from '../../components/ErrorBoundary'; @@ -306,6 +306,8 @@ export default function SearchPage() { pollInterval: 3000, // Poll every 3 seconds }) + const { data: featuredData } = useQuery(GET_FEATURED_POSTS) + // Auto-show results for guest mode useEffect(() => { if (isGuestMode && !showResults) { @@ -467,6 +469,9 @@ export default function SearchPage() { const processedData = processAndSortData(data) + const featuredPosts = (featuredData?.featuredPosts || []) + .map((post) => serializePost(post)) + // Create carousel items from posts for guest mode const createCarouselItems = (posts) => { if (!posts || !posts.length) return [] @@ -564,7 +569,15 @@ export default function SearchPage() { - + + {featuredPosts.length > 0 && ( + + + {createCarouselItems(featuredPosts)} + + + )} + {/* Filter Buttons - Always visible */} + + {featuredPosts.length > 0 && ( + + + {createCarouselItems(featuredPosts)} + + + )} {!showResults && (