Skip to content
20 changes: 10 additions & 10 deletions client/netlify/edge-functions/og-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,59 +166,59 @@ export default async (request, context) => {

// Replace page title
modifiedHtml = modifiedHtml.replace(
/<title>[^<]*<\/title>/,
/<title>[^<]*<\/title>/i,
`<title>${escapeHtml(ogTitle)}</title>`
);

// Replace meta description
modifiedHtml = modifiedHtml.replace(
/<meta name="description" content="[^"]*" \/>/,
/<meta[^>]*name=["']?description["']?[^>]*>/i,
`<meta name="description" content="${escapeHtml(ogDescription)}" />`
);

// Replace og:title
modifiedHtml = modifiedHtml.replace(
/<meta property="og:title" content="[^"]*" \/>/,
/<meta[^>]*property=["']?og:title["']?[^>]*>/i,
`<meta property="og:title" content="${escapeHtml(ogTitle)}" />`
);

// Replace og:description
modifiedHtml = modifiedHtml.replace(
/<meta property="og:description" content="[^"]*" \/>/,
/<meta[^>]*property=["']?og:description["']?[^>]*>/i,
`<meta property="og:description" content="${escapeHtml(ogDescription)}" />`
);

// Replace og:image
modifiedHtml = modifiedHtml.replace(
/<meta property="og:image" content="[^"]*" \/>/,
/<meta[^>]*property=["']?og:image["']?[^>]*>/i,
`<meta property="og:image" content="${escapeHtml(ogImage)}" />`
);

// Replace og:url
modifiedHtml = modifiedHtml.replace(
/<meta property="og:url" content="[^"]*" \/>/,
/<meta[^>]*property=["']?og:url["']?[^>]*>/i,
`<meta property="og:url" content="${escapeHtml(ogUrl)}" />`
);

// Replace og:type to "article" for quote pages
modifiedHtml = modifiedHtml.replace(
/<meta property="og:type" content="[^"]*" \/>/,
/<meta[^>]*property=["']?og:type["']?[^>]*>/i,
`<meta property="og:type" content="article" />`
);

// Replace Twitter card metadata
modifiedHtml = modifiedHtml.replace(
/<meta name="twitter:title" content="[^"]*" \/>/,
/<meta[^>]*name=["']?twitter:title["']?[^>]*>/i,
`<meta name="twitter:title" content="${escapeHtml(ogTitle)}" />`
);

modifiedHtml = modifiedHtml.replace(
/<meta name="twitter:description" content="[^"]*" \/>/,
/<meta[^>]*name=["']?twitter:description["']?[^>]*>/i,
`<meta name="twitter:description" content="${escapeHtml(ogDescription)}" />`
);

modifiedHtml = modifiedHtml.replace(
/<meta name="twitter:image" content="[^"]*" \/>/,
/<meta[^>]*name=["']?twitter:image["']?[^>]*>/i,
`<meta name="twitter:image" content="${escapeHtml(ogImage)}" />`
);

Expand Down
4 changes: 2 additions & 2 deletions client/src/components/CustomButtons/FollowButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ? (
<IconButton onClick={() => handleClick(action)}>
Expand Down
23 changes: 23 additions & 0 deletions client/src/components/Post/PostCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,29 @@ return {
{rejectedBy?.length}
</Typography>
</div>
{(upQuote > 0 || downQuote > 0) && (
<>
<Typography className={classes.divider}>|</Typography>
<div className={classes.voteItem} title="Quote Upvotes">
<ArrowUpwardIcon
className={classNames(classes.voteIcon, classes.upvoteIcon)}
style={{ fontSize: '14px' }}
/>
<Typography className={classes.voteNumber} style={{ fontSize: '12px' }}>
{upQuote}
</Typography>
</div>
<div className={classes.voteItem} title="Quote Downvotes">
<ArrowDownwardIcon
className={classNames(classes.voteIcon, classes.downvoteIcon)}
style={{ fontSize: '14px' }}
/>
<Typography className={classes.voteNumber} style={{ fontSize: '12px' }}>
{downQuote}
</Typography>
</div>
</>
)}
</div>
<div className={classes.interactions}>
<Typography>{interactions.length} interactions</Typography>
Expand Down
13 changes: 8 additions & 5 deletions client/src/components/customExpansionPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// TODO: Fix links to have href
import React from 'react'
import PropTypes from 'prop-types'
// @material-ui/core components
Expand Down Expand Up @@ -63,10 +62,9 @@ export default function CustomAccordion({ collapses, active: activeProp }) {
}}
>
<h4 className={classes.title} style={{ width: '10%' }}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<Link
className={classes.title}
tp
href={url}
>
{title}
</Link>
Expand All @@ -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 */}
<Link onClick={() => handleCopy(DOMAIN + url, key)}>
<Link
href={url}
onClick={(e) => {
e.preventDefault();
handleCopy(DOMAIN + url, key);
}}
>
{activeKey === key ? (
<Tooltip
placement="top"
Expand Down
33 changes: 33 additions & 0 deletions client/src/hooks/useTokenValidation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { tokenValidator } from 'store/user';

/**
* Custom hook to handle token validation and optional redirection.
*
* @param {Object} options - Configuration options
* @param {string} options.redirectToIfValid - Path to redirect to if the token is valid (e.g., /search)
* @param {string} options.redirectToIfInvalid - Path to redirect to if the token is invalid (e.g., /login)
* @returns {boolean} Whether the token is currently valid
*/
export const useTokenValidation = (options = {}) => {
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;
9 changes: 3 additions & 6 deletions client/src/layouts/Scoreboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 3 additions & 6 deletions client/src/views/PasswordResetPage/PasswordResetPage.jsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.`,
Expand Down
12 changes: 11 additions & 1 deletion server/app/data/resolvers/queries/post/getPost.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
13 changes: 10 additions & 3 deletions server/app/data/resolvers/queries/user/findUserById.js
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down Expand Up @@ -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) {
Expand Down
26 changes: 26 additions & 0 deletions test_fetch.js
Original file line number Diff line number Diff line change
@@ -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));
9 changes: 9 additions & 0 deletions test_get_posts.js
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 5 additions & 0 deletions test_regex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const fs = require('fs');
let html = fs.readFileSync('client/index.html', 'utf8');
const ogTitle = 'Test Title';
html = html.replace(/<meta property="og:title" content="[^"]*" \/>/, `<meta property="og:title" content="${ogTitle}" />`);
console.log("Matched:", html.includes(ogTitle));
16 changes: 16 additions & 0 deletions test_regex2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const ogTitle = 'Replaced Title';
const testCases = [
'<meta property="og:title" content="Quote.Vote – The Internet\'s Quote Board" />',
'<meta property="og:title" content="Quote.Vote – The Internet\'s Quote Board">',
'<meta property=og:title content="Quote.Vote – The Internet\'s Quote Board">',
'<meta content="Quote.Vote – The Internet\'s Quote Board" property="og:title" />',
];

const regex = /<meta[^>]*property=["']?og:title["']?[^>]*>/i;

testCases.forEach(html => {
const result = html.replace(regex, `<meta property="og:title" content="${ogTitle}" />`);
console.log("Original:", html);
console.log("Replaced:", result);
console.log("---");
});