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 && (