gatsby-core-theme 44.4.44 → 44.4.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [44.4.46](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.4.45...v44.4.46) (2025-09-01)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * comment vote logic ([a09a3bd](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/a09a3bd5ef047976362b12099fb37e84c682cb69))
7
+
8
+ ## [44.4.45](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.4.44...v44.4.45) (2025-08-29)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * voting logic ([15a7af0](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/commit/15a7af00ac73f71837d083c913d83fb9d6c95ca9))
14
+
1
15
  ## [44.4.44](https://gitlab.com/g2m-gentoo/team-floyd/themes/gatsby-themes/compare/v44.4.43...v44.4.44) (2025-08-29)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gatsby-core-theme",
3
- "version": "44.4.44",
3
+ "version": "44.4.46",
4
4
  "description": "Gatsby Theme NPM Package",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -0,0 +1,34 @@
1
+ .buttonGroup{
2
+ background: #EAE5E0;
3
+ font-size: 1.2rem;
4
+ padding: .5rem .8rem;
5
+ height: 2.8rem;
6
+ gap: .4rem;
7
+ color: #0F172A;
8
+
9
+ @include flex-align(center, center);
10
+
11
+ &.left{
12
+ border-bottom-left-radius: 5rem;
13
+ border-top-left-radius: 5rem;
14
+ padding-right: 0;
15
+ }
16
+
17
+ &.right{
18
+ border-bottom-right-radius: 5rem;
19
+ border-top-right-radius: 5rem;
20
+ }
21
+ }
22
+
23
+ .buttonGroupIcon{
24
+ width: 1.6rem;
25
+ height: 1.6rem;
26
+ }
27
+
28
+ .buttonGroup[disabled]{
29
+ cursor: default !important;
30
+ }
31
+
32
+ .disabled > *{
33
+ opacity: .3;
34
+ }
@@ -0,0 +1,92 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ /* eslint-disable no-console */
3
+ /* eslint-disable react-hooks/exhaustive-deps */
4
+ /* eslint-disable jsx-a11y/click-events-have-key-events */
5
+ /* eslint-disable import/no-extraneous-dependencies */
6
+ import React, { useContext } from 'react';
7
+ import axios from 'axios';
8
+ import PropTypes from 'prop-types';
9
+ import LazyImage from '~hooks/lazy-image';
10
+ import useTranslate from '~hooks/useTranslate/useTranslate';
11
+ import { useVote, VotesContext } from '../../../context/VotesProvider';
12
+ import styles from './comment-votes.module.scss';
13
+
14
+ const CommentVotes = ({
15
+ comment,
16
+ voteUpAria = 'Like Button',
17
+ voteDownAria = 'Dislike Button'
18
+ }) => {
19
+ const { updateVote } = useContext(VotesContext);
20
+
21
+ const userVote = useVote(comment.comment_id);
22
+
23
+ const handleVote = async (like = false, commentId) => {
24
+ const data = like
25
+ ? {
26
+ "vote_up": "increase",
27
+ "commentID": commentId
28
+ }
29
+ : {
30
+ "vote_down": "increase",
31
+ "commentID": commentId
32
+ };
33
+
34
+ const headers = {
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ Accept: "application/json",
38
+ },
39
+ };
40
+
41
+ return new Promise((resolve, reject) => {
42
+ axios
43
+ .put('/api/put-vote', data, headers)
44
+ .then((response) => {
45
+ response.ok = response.status === 200;
46
+ resolve(response);
47
+ })
48
+ .catch((error) => reject(error?.response?.data?.errors?.join() || error.message));
49
+ });
50
+ }
51
+
52
+ return <>
53
+ <button
54
+ onClick={async () => {
55
+ updateVote(comment.comment_id, 'vote_up');
56
+ await handleVote(true, comment.comment_id)
57
+ }}
58
+ aria-label={useTranslate('vote_up_aria_label', voteUpAria)}
59
+ type='button'
60
+ className={`${styles.buttonGroup} ${styles.left} ${userVote === 'vote_down' && styles.disabled || ''}`}
61
+ disabled={Boolean(userVote)}
62
+ >
63
+ <LazyImage className={styles.buttonGroupIcon} src='/images/like.svg' />
64
+ <span>{userVote === 'vote_up' ? comment.votes_up + 1 : comment.votes_up}</span>
65
+ </button>
66
+ <button
67
+ onClick={async () => {
68
+ updateVote(comment.comment_id, 'vote_down');
69
+ await handleVote(false, comment.comment_id)
70
+ }}
71
+ aria-label={useTranslate('vote_down_aria_label', voteDownAria)}
72
+ type='button'
73
+ className={`${styles.buttonGroup} ${styles.right} ${userVote === 'vote_up' && styles.disabled || ''}`}
74
+ disabled={Boolean(userVote)}
75
+ >
76
+ <LazyImage className={styles.buttonGroupIcon} src='/images/dislike.svg' />
77
+ <span>{userVote === 'vote_down' ? comment.votes_down + 1 : comment.votes_down}</span>
78
+ </button>
79
+ </>
80
+ };
81
+
82
+ CommentVotes.propTypes = {
83
+ voteUpAria: PropTypes.string,
84
+ voteDownAria: PropTypes.string,
85
+ comment: PropTypes.shape({
86
+ comment_id: PropTypes.string,
87
+ votes_up: PropTypes.number,
88
+ votes_down: PropTypes.number
89
+ })
90
+ };
91
+
92
+ export default CommentVotes;
@@ -59,33 +59,6 @@
59
59
  @include flex-direction(row);
60
60
  }
61
61
 
62
- .buttonGroup{
63
- background: #EAE5E0;
64
- font-size: 1.2rem;
65
- padding: .5rem .8rem;
66
- height: 2.8rem;
67
- gap: .4rem;
68
- color: #0F172A;
69
-
70
- @include flex-align(center, center);
71
-
72
- &.left{
73
- border-bottom-left-radius: 5rem;
74
- border-top-left-radius: 5rem;
75
- padding-right: 0;
76
- }
77
-
78
- &.right{
79
- border-bottom-right-radius: 5rem;
80
- border-top-right-radius: 5rem;
81
- }
82
- }
83
-
84
- .buttonGroupIcon{
85
- width: 1.6rem;
86
- height: 1.6rem;
87
- }
88
-
89
62
  .replyButton{
90
63
  border-radius: 10rem;
91
64
  border: 1.5px solid #161128;
@@ -101,10 +74,6 @@
101
74
  display: grid;
102
75
  }
103
76
 
104
- .disabled > *{
105
- opacity: .3;
106
- }
107
-
108
77
  .buttonGroup[disabled]{
109
78
  cursor: default;
110
79
  }
@@ -3,12 +3,12 @@
3
3
  /* eslint-disable react-hooks/exhaustive-deps */
4
4
  /* eslint-disable jsx-a11y/click-events-have-key-events */
5
5
  /* eslint-disable import/no-extraneous-dependencies */
6
- import React, { useRef, useState } from 'react';
7
- import axios from 'axios';
6
+ import React, { useState } from 'react';
8
7
  import PropTypes from 'prop-types';
9
8
  import LazyImage from '~hooks/lazy-image';
10
9
  import LeaveCommentForm from '../leave-comment-form';
11
10
  import useTranslate from '~hooks/useTranslate/useTranslate';
11
+ import CommentVotes from '../../atoms/comment-votes';
12
12
  import styles from './comment.module.scss';
13
13
 
14
14
  const Comment = ({
@@ -16,71 +16,21 @@ const Comment = ({
16
16
  comment,
17
17
  authors,
18
18
  isReply,
19
- userInteractions,
20
19
  showVotes = true
21
20
  }) => {
22
21
  const commentName = comment?.author_id ? authors?.[comment?.author_id]?.name : comment.author_name;
23
22
  const commentJobTitle = comment?.author_id ? authors?.[comment?.author_id]?.author_title : undefined;
23
+
24
24
  const date = new Date(comment.updated_at);
25
25
  const day = String(date.getUTCDate()).padStart(2, '0');
26
26
  const month = String(date.getUTCMonth() + 1).padStart(2, '0');
27
27
  const year = date.getUTCFullYear();
28
- const [reply, setReply] = useState(false);
29
- const [likes, setLikes] = useState(comment.votes_up);
30
- const [dislikes, setDislikes] = useState(comment.votes_down);
31
- const currentUserInteractions = useRef(userInteractions);
32
28
 
33
29
  const hours = String(date.getUTCHours()).padStart(2, '0');
34
30
  const minutes = String(date.getUTCMinutes()).padStart(2, '0');
35
31
  const commentDate = `${day}/${month}/${year} | ${hours}:${minutes}`;
36
-
37
- const handleVote = async (like = false, commentId) => {
38
- const data = like
39
- ? {
40
- "vote_up": "increase",
41
- "commentID": commentId
42
- }
43
- : {
44
- "vote_down": "increase",
45
- "commentID": commentId
46
- };
47
-
48
- const headers = {
49
- headers: {
50
- "Content-Type": "application/json",
51
- Accept: "application/json",
52
- },
53
- };
54
-
55
- const existingCookie = document.cookie
56
- .split('; ')
57
- .find((row) => row.startsWith('comments='))
58
- ?.split('=')[1] || '';
59
-
60
- const ids = existingCookie.split(',').filter(Boolean);
61
-
62
- const alreadyVoted = ids.some((entry) => entry.startsWith(`${commentId}:`));
63
- if (!alreadyVoted) {
64
- ids.push(`${commentId}: ${like ? 'vote_up' : 'vote_down'}`);
65
- document.cookie = `comments=${ids.join(',')}; path=/;`;
66
- currentUserInteractions.current = ids;
67
- }
68
-
69
- return new Promise((resolve, reject) => {
70
- axios
71
- .put('/api/put-vote', data, headers)
72
- .then((response) => {
73
- response.ok = response.status === 200;
74
- resolve(response);
75
- })
76
- .catch((error) => reject(error?.response?.data?.errors?.join() || error.message));
77
- });
78
- }
79
- const userVote = currentUserInteractions.current?.find(v => v.startsWith(`${comment.comment_id}:`))?.split(': ')[1];
80
-
81
- const displayedLikes = userVote === 'vote_down' ? likes + 1 : likes;
82
- const displayedDislikes = userVote === 'vote_down' ? dislikes + 1 : dislikes;
83
32
 
33
+ const [reply, setReply] = useState(false);
84
34
 
85
35
  return <div className={`${styles.commentContainer} ${isReply ? styles.isReply : ''}`}>
86
36
  <div className={styles.commentTopArea}>
@@ -94,34 +44,7 @@ const Comment = ({
94
44
  </div>
95
45
  <div className={styles.commentActions}>
96
46
  {showVotes && (
97
- <>
98
- <button
99
- onClick={async () => {
100
- await handleVote(true, comment.comment_id)
101
- setLikes((prev) => prev + 1)
102
- }}
103
- aria-label='Like Button'
104
- type='button'
105
- className={`${styles.buttonGroup} ${styles.left} ${currentUserInteractions.current?.find(v => v.startsWith(`${comment.comment_id}:`))?.split(': ')[1] === 'vote_up' && styles.disabled || ''}`}
106
- disabled={currentUserInteractions.current?.find(v => v.startsWith(`${comment.comment_id}:`))}
107
- >
108
- <LazyImage className={styles.buttonGroupIcon} src='/images/like.svg' />
109
- <span>{displayedLikes}</span>
110
- </button>
111
- <button
112
- onClick={async () => {
113
- await handleVote(false, comment.comment_id)
114
- setDislikes((prev) => prev + 1)
115
- }}
116
- aria-label='Dislike Button'
117
- type='button'
118
- className={`${styles.buttonGroup} ${styles.right} ${currentUserInteractions.current?.find(v => v.startsWith(`${comment.comment_id}:`))?.split(': ')[1] === 'vote_down' && styles.disabled || ''}`}
119
- disabled={currentUserInteractions.current?.find(v => v.startsWith(`${comment.comment_id}:`))}
120
- >
121
- <LazyImage className={styles.buttonGroupIcon} src='/images/dislike.svg' />
122
- <span>{displayedDislikes}</span>
123
- </button>
124
- </>
47
+ <CommentVotes comment={comment} />
125
48
  )}
126
49
  {!isReply && (
127
50
  <button aria-label='Reply Button' onClick={() => setReply(!reply)} type='button' className={styles.replyButton}>
@@ -149,7 +72,6 @@ Comment.propTypes = {
149
72
  }),
150
73
  authors: PropTypes.shape({}),
151
74
  isReply: PropTypes.bool,
152
- userInteractions: PropTypes.arrayOf(PropTypes.string),
153
75
  showVotes: PropTypes.bool
154
76
  };
155
77
 
@@ -1,17 +1,16 @@
1
1
  import React from "react";
2
2
 
3
3
  import PropTypes, { arrayOf } from 'prop-types';
4
- import keygen from '~helpers/keygen';
5
4
  import Comment from "../../../molecules/comment";
6
5
 
7
6
  import styles from './comment-tree.module.scss';
8
7
 
9
- const CommentTree = React.memo(({ pageContext, comment, authors, depth = 0, cookieComments }) =>
10
- <div key={keygen()} className={`${styles?.[`comment_${depth}`]} ${depth > 0 && styles.isReply}`}>
11
- <Comment pageContext={pageContext} comment={comment} authors={authors} isReply={depth > 0} userInteractions={cookieComments} />
8
+ const CommentTree = React.memo(({ pageContext, comment, authors, depth = 0 }) =>
9
+ <div key={comment.comment_id} className={`${styles?.[`comment_${depth}`]} ${depth > 0 && styles.isReply}`}>
10
+ <Comment pageContext={pageContext} comment={comment} authors={authors} isReply={depth > 0} />
12
11
  {comment.replies?.map(reply => (
13
12
  <CommentTree
14
- key={keygen()}
13
+ key={reply.comment_id}
15
14
  comment={reply}
16
15
  authors={authors}
17
16
  depth={depth + 1}
@@ -24,11 +23,11 @@ const CommentTree = React.memo(({ pageContext, comment, authors, depth = 0, cook
24
23
  CommentTree.propTypes = {
25
24
  pageContext: PropTypes.shape({}),
26
25
  comment: PropTypes.shape({
27
- replies: arrayOf(PropTypes.shape({}))
26
+ replies: arrayOf(PropTypes.shape({})),
27
+ comment_id: PropTypes.number
28
28
  }),
29
29
  authors: PropTypes.shape({}),
30
30
  depth: PropTypes.number,
31
- cookieComments: PropTypes.arrayOf(PropTypes.string)
32
31
  };
33
32
 
34
33
  export default CommentTree;
@@ -1,39 +1,28 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import CommentTree from './comment-tree';
4
4
  import LeaveCommentForm from '../../molecules/leave-comment-form';
5
+ import { VotesProvider } from '../../../context/VotesProvider';
5
6
 
6
7
  const Comments = ({ pageContext }) => {
7
8
  const { comments, authors } = pageContext;
8
- const [cookieComments, setCookieComments] = useState([]);
9
-
10
- useEffect(() => {
11
- const storedVotes =
12
- document.cookie
13
- .split('; ')
14
- .find((row) => row.startsWith('comments='))
15
- ?.split('=')[1]
16
- ?.split(',')
17
- .filter(Boolean) || [];
18
-
19
- setCookieComments(storedVotes);
20
- }, []);
21
9
 
22
10
  if (!comments) return null;
23
11
 
24
12
  return (
25
13
  <div>
26
14
  <LeaveCommentForm page={pageContext.page} />
27
- {comments.map((comment) => (
28
- <CommentTree
29
- key={comment.comment_id}
30
- pageContext={pageContext}
31
- comment={comment}
32
- authors={authors?.authors}
33
- depth={0}
34
- cookieComments={cookieComments}
35
- />
36
- ))}
15
+ <VotesProvider>
16
+ {comments.map((comment) => (
17
+ <CommentTree
18
+ key={comment.comment_id}
19
+ pageContext={pageContext}
20
+ comment={comment}
21
+ authors={authors?.authors}
22
+ depth={0}
23
+ />
24
+ ))}
25
+ </VotesProvider>
37
26
  </div>
38
27
  );
39
28
  };
@@ -0,0 +1,49 @@
1
+ import React, {createContext, useContext, useEffect, useState} from "react";
2
+
3
+ export const VotesContext = createContext({});
4
+
5
+ // eslint-disable-next-line react/prop-types
6
+ export const VotesProvider = ({ children }) => {
7
+ const [votes, setVotes] = useState({});
8
+
9
+ useEffect(() => {
10
+ const storedVotes =
11
+ document.cookie
12
+ .split("; ")
13
+ .find((row) => row.startsWith("comments="))
14
+ ?.split("=")[1]
15
+ ?.split(",")
16
+ .filter(Boolean) || [];
17
+ const votesMap = storedVotes.reduce((acc, entry) => {
18
+ const [id, vote] = entry.split(":");
19
+ if (id && vote) acc[id] = vote;
20
+ return acc;
21
+ }, {});
22
+ setVotes(votesMap);
23
+ }, []);
24
+
25
+ const updateVote = (commentId, voteValue) => {
26
+ setVotes((prevVotes) => {
27
+ const newVotes = { ...prevVotes, [commentId]: voteValue };
28
+
29
+ // Convert votes object to cookie string
30
+ const cookieValue = Object.entries(newVotes)
31
+ .map(([id, vote]) => `${id}:${vote}`)
32
+ .join(",");
33
+
34
+ // Set the cookie (expires in 1 year)
35
+ document.cookie = `comments=${cookieValue}; path=/;`;
36
+
37
+ return newVotes;
38
+ });
39
+ };
40
+
41
+ return (
42
+ <VotesContext.Provider value={{votes, updateVote}}>{children}</VotesContext.Provider>
43
+ );
44
+ };
45
+
46
+ export const useVote = (commentId) => {
47
+ const { votes } = useContext(VotesContext);
48
+ return votes?.[commentId];
49
+ };