diff --git a/client/netlify/edge-functions/og-metadata.js b/client/netlify/edge-functions/og-metadata.js index 85d45e02..530b7e89 100644 --- a/client/netlify/edge-functions/og-metadata.js +++ b/client/netlify/edge-functions/og-metadata.js @@ -166,59 +166,59 @@ export default async (request, context) => { // Replace page title modifiedHtml = modifiedHtml.replace( - /[^<]*<\/title>/, + /<title>[^<]*<\/title>/i, `<title>${escapeHtml(ogTitle)}` ); // Replace meta description modifiedHtml = modifiedHtml.replace( - //, + /]*name=["']?description["']?[^>]*>/i, `` ); // Replace og:title modifiedHtml = modifiedHtml.replace( - //, + /]*property=["']?og:title["']?[^>]*>/i, `` ); // Replace og:description modifiedHtml = modifiedHtml.replace( - //, + /]*property=["']?og:description["']?[^>]*>/i, `` ); // Replace og:image modifiedHtml = modifiedHtml.replace( - //, + /]*property=["']?og:image["']?[^>]*>/i, `` ); // Replace og:url modifiedHtml = modifiedHtml.replace( - //, + /]*property=["']?og:url["']?[^>]*>/i, `` ); // Replace og:type to "article" for quote pages modifiedHtml = modifiedHtml.replace( - //, + /]*property=["']?og:type["']?[^>]*>/i, `` ); // Replace Twitter card metadata modifiedHtml = modifiedHtml.replace( - //, + /]*name=["']?twitter:title["']?[^>]*>/i, `` ); modifiedHtml = modifiedHtml.replace( - //, + /]*name=["']?twitter:description["']?[^>]*>/i, `` ); modifiedHtml = modifiedHtml.replace( - //, + /]*name=["']?twitter:image["']?[^>]*>/i, `` ); diff --git a/client/src/components/CustomButtons/FollowButton.jsx b/client/src/components/CustomButtons/FollowButton.jsx index c518ec95..7a5e8ee4 100644 --- a/client/src/components/CustomButtons/FollowButton.jsx +++ b/client/src/components/CustomButtons/FollowButton.jsx @@ -53,6 +53,7 @@ function FollowButton({ const user = useSelector((state) => state.user) const followingArray = _.get(user, 'data._followingId', []) + const isFollowingUser = isFollowing || followingArray.includes(profileUserId) async function handleClick(action) { if (!ensureAuth()) return @@ -66,8 +67,7 @@ function FollowButton({ await followMutation({ variables: { user_id: profileUserId, action } }) } - // TODO handle data object - if (isFollowing) { + if (isFollowingUser) { const action = 'un-follow' return showIcon ? ( handleClick(action)}> diff --git a/client/src/components/Post/PostCard.jsx b/client/src/components/Post/PostCard.jsx index 39d1aa30..ef873803 100644 --- a/client/src/components/Post/PostCard.jsx +++ b/client/src/components/Post/PostCard.jsx @@ -432,6 +432,29 @@ return { {rejectedBy?.length} + {(upQuote > 0 || downQuote > 0) && ( + <> + | +
+ + + {upQuote} + +
+
+ + + {downQuote} + +
+ + )}
{interactions.length} interactions diff --git a/client/src/components/customExpansionPanel.jsx b/client/src/components/customExpansionPanel.jsx index 8a8a9e39..13c0ae41 100644 --- a/client/src/components/customExpansionPanel.jsx +++ b/client/src/components/customExpansionPanel.jsx @@ -1,4 +1,3 @@ -// TODO: Fix links to have href import React from 'react' import PropTypes from 'prop-types' // @material-ui/core components @@ -63,10 +62,9 @@ export default function CustomAccordion({ collapses, active: activeProp }) { }} >

- {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {title} @@ -89,8 +87,13 @@ export default function CustomAccordion({ collapses, active: activeProp }) { display: 'flex', alignItems: 'center', flexDirection: 'row', marginRight: '2%', }} > - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - handleCopy(DOMAIN + url, key)}> + { + e.preventDefault(); + handleCopy(DOMAIN + url, key); + }} + > {activeKey === key ? ( { + const { redirectToIfValid, redirectToIfInvalid } = options; + const dispatch = useDispatch(); + const history = useHistory(); + + useEffect(() => { + const isValid = tokenValidator(dispatch); + + if (isValid && redirectToIfValid) { + history.push(redirectToIfValid); + } else if (!isValid && redirectToIfInvalid) { + history.push(redirectToIfInvalid); + } + }, [dispatch, history, redirectToIfValid, redirectToIfInvalid]); + + // Return the current validation state + return tokenValidator(dispatch); +}; + +export default useTokenValidation; diff --git a/client/src/layouts/Scoreboard.jsx b/client/src/layouts/Scoreboard.jsx index 5de35f4e..70e711cc 100644 --- a/client/src/layouts/Scoreboard.jsx +++ b/client/src/layouts/Scoreboard.jsx @@ -14,7 +14,7 @@ import CssBaseline from '@material-ui/core/CssBaseline' import appRoutes from '../routes' import styles from 'assets/jss/material-dashboard-pro-react/layouts/adminStyle' -import { tokenValidator } from 'store/user' +import useTokenValidation from '../hooks/useTokenValidation' import { useDispatch, useSelector } from 'react-redux' import { SET_SNACKBAR } from 'store/ui' import Snackbar from 'mui-pro/Snackbar/Snackbar' @@ -65,11 +65,8 @@ function Scoreboard(props) { const routes = getRoutes(appRoutes) - // TODO: Abstract validation into custom hook - useEffect(() => { - tokenValidator(dispatch) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + useTokenValidation() + useEffect(() => { const { diff --git a/client/src/views/PasswordResetPage/PasswordResetPage.jsx b/client/src/views/PasswordResetPage/PasswordResetPage.jsx index a14495e2..83acace7 100644 --- a/client/src/views/PasswordResetPage/PasswordResetPage.jsx +++ b/client/src/views/PasswordResetPage/PasswordResetPage.jsx @@ -1,7 +1,7 @@ import React from 'react' import { makeStyles } from '@material-ui/core/styles' import GridContainer from 'mui-pro/Grid/GridContainer' -import { tokenValidator } from 'store/user' +import useTokenValidation from '../../hooks/useTokenValidation' import { useDispatch } from 'react-redux' import { useHistory, useLocation } from 'react-router-dom' import styles from 'assets/jss/material-dashboard-pro-react/views/loginPageStyle' @@ -26,11 +26,8 @@ export default function PasswordResetPage() { }) const [updateUserPassword] = useMutation(UPDATE_USER_PASSWORD) const isValidToken = data && data.verifyUserPasswordResetToken - // TODO: Abstract validation into custom hook - React.useEffect(() => { - if (tokenValidator(dispatch)) history.push('/search') - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + useTokenValidation({ redirectToIfValid: '/search' }) + const classes = useStyles() const handleSubmit = async (values) => { diff --git a/server/app/data/resolvers/mutations/userInvite/sendUserInviteApproval.js b/server/app/data/resolvers/mutations/userInvite/sendUserInviteApproval.js index d1240581..64d34e95 100644 --- a/server/app/data/resolvers/mutations/userInvite/sendUserInviteApproval.js +++ b/server/app/data/resolvers/mutations/userInvite/sendUserInviteApproval.js @@ -87,8 +87,7 @@ export const sendUserInviteApproval = (pubsub) => { } const email = await sendGridEmail(mailOptions); - // *** TODO test to see if email was sent *** - if (email) { + if (email && email.success) { return { code: 'SUCCESS', message: `User sign-up invite sent successfully to ${mailTo}.`, diff --git a/server/app/data/resolvers/queries/post/getPost.js b/server/app/data/resolvers/queries/post/getPost.js index 718224b6..5f1f1867 100644 --- a/server/app/data/resolvers/queries/post/getPost.js +++ b/server/app/data/resolvers/queries/post/getPost.js @@ -1,8 +1,18 @@ +import mongoose from 'mongoose'; import PostModel from '../../models/PostModel'; export const getPost = (pubsub) => { return async (_, args, context) => { - const post = await PostModel.findById(args.postId); + let post; + + // Check if the provided postId is a valid MongoDB ObjectId + if (mongoose.Types.ObjectId.isValid(args.postId)) { + post = await PostModel.findById(args.postId); + } else { + // If it's not a valid ObjectId, assume it's a short URL ID (urlId) + post = await PostModel.findOne({ urlId: args.postId }); + } + if (!post || post.deleted) { if (context && context.res) { context.res.status(404); diff --git a/server/app/data/resolvers/queries/user/findUserById.js b/server/app/data/resolvers/queries/user/findUserById.js index 9cb9fb4c..78a17359 100644 --- a/server/app/data/resolvers/queries/user/findUserById.js +++ b/server/app/data/resolvers/queries/user/findUserById.js @@ -1,6 +1,8 @@ import { isUndefined } from 'lodash'; import UserModel from '../../models/UserModel'; import VotesModel from '../../models/VoteModel'; +import PostModel from '../../models/PostModel'; +import QuoteModel from '../../models/QuoteModel'; import * as utils from '../../utils'; export const findUserById = () => { @@ -31,15 +33,20 @@ export const findUserById = () => { userId = user._id; // get user total votes - const userVotes = await VotesModel.find({ _userId: userId }); + const userVotes = await VotesModel.find({ userId, deleted: false }); user.vote_cast = isUndefined(userVotes) ? 0 : userVotes.length; user._followingId = utils.uniqueArrayObjects(user._followingId); user._followersId = utils.uniqueArrayObjects(user._followersId); - // TODO: calculate user points - user.points = 0; + // Calculate user points + // 10 pts per post, 5 pts per quote, 1 pt per vote cast + const postCount = await PostModel.countDocuments({ userId, deleted: false }); + const quoteCount = await QuoteModel.countDocuments({ quoter: userId, deleted: false }); + const voteCount = user.vote_cast; + + user.points = (postCount * 10) + (quoteCount * 5) + voteCount; return user; } catch (err) { diff --git a/test_fetch.js b/test_fetch.js new file mode 100644 index 00000000..08a882aa --- /dev/null +++ b/test_fetch.js @@ -0,0 +1,26 @@ +const postId = "e_6A4M"; +const graphqlUrl = 'https://api.quote.vote/graphql'; +const graphqlQuery = { + query: ` + query post($postId: String!) { + post(postId: $postId) { + _id + title + text + url + creator { + name + avatar + } + } + } + `, + variables: { postId }, +}; +fetch(graphqlUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(graphqlQuery), +}).then(res => res.json()).then(data => console.log(JSON.stringify(data, null, 2))).catch(err => console.error(err)); diff --git a/test_get_posts.js b/test_get_posts.js new file mode 100644 index 00000000..d39b5327 --- /dev/null +++ b/test_get_posts.js @@ -0,0 +1,9 @@ +const graphqlUrl = 'https://api.quote.vote/graphql'; +fetch(graphqlUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: '{ posts(limit: 5, offset: 0, searchKey: "", sortOrder: "newest") { entities { _id title url } } }' }) +}) +.then(r => r.json()) +.then(d => console.log(JSON.stringify(d, null, 2))) +.catch(console.error); diff --git a/test_regex.js b/test_regex.js new file mode 100644 index 00000000..bd52eb7e --- /dev/null +++ b/test_regex.js @@ -0,0 +1,5 @@ +const fs = require('fs'); +let html = fs.readFileSync('client/index.html', 'utf8'); +const ogTitle = 'Test Title'; +html = html.replace(//, ``); +console.log("Matched:", html.includes(ogTitle)); diff --git a/test_regex2.js b/test_regex2.js new file mode 100644 index 00000000..7c604f82 --- /dev/null +++ b/test_regex2.js @@ -0,0 +1,16 @@ +const ogTitle = 'Replaced Title'; +const testCases = [ + '', + '', + '', + '', +]; + +const regex = /]*property=["']?og:title["']?[^>]*>/i; + +testCases.forEach(html => { + const result = html.replace(regex, ``); + console.log("Original:", html); + console.log("Replaced:", result); + console.log("---"); +});