From 1f7d95a4ff5b97119d03912e560f6785f277d236 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 5 Dec 2024 23:11:44 +0300 Subject: [PATCH] Votes for post --- .vscode/settings.json | 4 +- .../ArticleViewer/ArticleViewer.tsx | 24 +++++-- .../ArticleViewer/VoteButton/VoteButton.tsx | 65 +++++++++++++++++++ .../ArticleViewer/VoteCounter/VoteCounter.tsx | 33 ++++++++++ .../PostVotesPolicies/PostVotePolicies.go | 2 + .../PostVoteRules/readVotesRule.go | 17 +++++ .../db/go_queries/post_votes_queries.sql.go | 19 ++++++ enshi_back/db/queries/post_votes_queries.sql | 8 ++- enshi_back/middleware/postVotesMiddleware.go | 9 ++- enshi_back/routes/routesSetup.go | 7 +- enshi_back/routes/voteRoutes/createVote.go | 7 ++ enshi_back/routes/voteRoutes/getVote.go | 12 ++-- enshi_back/routes/voteRoutes/getVotes.go | 29 +++++++++ 13 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx create mode 100644 enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx create mode 100644 enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readVotesRule.go create mode 100644 enshi_back/routes/voteRoutes/getVotes.go diff --git a/.vscode/settings.json b/.vscode/settings.json index ce758c7..fd2040b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "cSpell.words": [ - "godotenv" + "downvotes", + "godotenv", + "upvotes" ] } \ No newline at end of file diff --git a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx index 15f8a4a..892920a 100644 --- a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx +++ b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx @@ -1,4 +1,4 @@ -import { Container, Flex, Separator, Text } from "@radix-ui/themes"; +import { Box, Container, Flex, Separator, Text } from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; import { Interweave } from "interweave"; import { useAtomValue } from "jotai"; @@ -7,6 +7,8 @@ import { axiosLocalhost } from "../../api/axios/axios"; import { userAtom } from "../../AtomStore/AtomStore"; import ChangePostButton from "./ChangePostButton/ChangePostButton"; import SkeletonPostLoader from "./SkeletonLoader/SkeletonLoader"; +import VoteButton, { DOWNVOTE, UPVOTE } from "./VoteButton/VoteButton"; +import VoteCounter from "./VoteCounter/VoteCounter"; type TArticleViewer = { htmlToParse?: string; @@ -38,12 +40,26 @@ export default function ArticleViewer(props: TArticleViewer) { {data.title} - - + diff --git a/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx b/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx new file mode 100644 index 0000000..46a880b --- /dev/null +++ b/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx @@ -0,0 +1,65 @@ +import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons"; +import { IconButton } from "@radix-ui/themes"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { axiosLocalhost } from "../../../api/axios/axios"; + +export const UPVOTE = true; +export const DOWNVOTE = false; + +type TVoteButton = { + postId: string; + vote: boolean; +}; + +export default function VoteButton(props: TVoteButton) { + const queryClient = useQueryClient(); + + const { data } = useQuery({ + queryKey: [props.vote + "voteCheck"], + queryFn: async () => { + const response = await axiosLocalhost.get( + `post-vote/${props.postId}` + ); + + return (response.data?.vote as boolean) === props.vote || false; + }, + gcTime: 0, + }); + + const voteMutation = useMutation({ + mutationKey: [`voteMutation${props.vote}`], + onMutate: async () => { + queryClient.cancelQueries({ queryKey: [props.vote + "voteCheck"] }); + + queryClient.setQueryData([props.vote + "voteCheck"], true); + queryClient.setQueryData([!props.vote + "voteCheck"], false); + }, + mutationFn: async () => { + await axiosLocalhost.post(`post-votes/${props.postId}`, { + vote: props.vote, + }); + }, + onSuccess: () => {}, + onError: () => { + queryClient.setQueryData([props.vote + "voteCheck"], false); + }, + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: [props.vote + "voteCheck"], + }); + queryClient.invalidateQueries({ + queryKey: ["post_vote_counter"], + }); + }, + }); + + return ( + voteMutation.mutate()} + > + {props.vote ? : } + + ); +} diff --git a/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx b/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx new file mode 100644 index 0000000..4d54842 --- /dev/null +++ b/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx @@ -0,0 +1,33 @@ +import { Box, Skeleton } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { axiosLocalhost } from "../../../api/axios/axios"; + +type TVoteCounter = { + postId: string; +}; + +export default function VoteCounter(props: TVoteCounter) { + const { data, isLoading } = useQuery({ + queryKey: ["post_vote_counter"], + queryFn: async () => { + const response = await axiosLocalhost.get( + `post-votes/${props.postId}` + ); + return response.data as { upvotes: number; downvotes: number }; + }, + }); + + const calculateRating = (upvotes: number, downvotes: number) => { + return upvotes + (-downvotes) + } + + if (isLoading) { + return + {calculateRating(0, 0)} + + } + + return + {calculateRating(data?.upvotes || 0, data?.downvotes || 0)} + ; +} diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go b/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go index 045a327..9e597bf 100644 --- a/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go +++ b/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go @@ -30,6 +30,8 @@ func PostVotePolicies(c *gin.Context) (bool, []error) { case READ_VOTE: return rules.CheckRule(c, postvoterules.PostVoteReadRule) + default: + return rules.CheckRule(c, postvoterules.PostVotesReadRule) } return false, nil diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readVotesRule.go b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readVotesRule.go new file mode 100644 index 0000000..736684e --- /dev/null +++ b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readVotesRule.go @@ -0,0 +1,17 @@ +package postvoterules + +import ( + "github.com/gin-gonic/gin" +) + +func PostVotesReadRule(c *gin.Context) (bool, []error) { + // rulesToCheck := []rules.RuleFunction{} + + // isAllowed, errors := rules.CheckRules( + // c, + // rulesToCheck, + // rules.ALL_RULES_MUST_BE_COMPLETED, + // ) + + return true, nil +} diff --git a/enshi_back/db/go_queries/post_votes_queries.sql.go b/enshi_back/db/go_queries/post_votes_queries.sql.go index 688d23e..05e957c 100644 --- a/enshi_back/db/go_queries/post_votes_queries.sql.go +++ b/enshi_back/db/go_queries/post_votes_queries.sql.go @@ -65,6 +65,25 @@ func (q *Queries) GetPostVote(ctx context.Context, arg GetPostVoteParams) (bool, return vote, err } +const getPostVotes = `-- name: GetPostVotes :one +SELECT count (*) FILTER (WHERE vote = TRUE) as upvotes, +count (*) FILTER (WHERE vote = FALSE) as downvotes +FROM public.post_votes +WHERE post_id = $1 +` + +type GetPostVotesRow struct { + Upvotes int64 `json:"upvotes"` + Downvotes int64 `json:"downvotes"` +} + +func (q *Queries) GetPostVotes(ctx context.Context, postID int64) (GetPostVotesRow, error) { + row := q.db.QueryRow(ctx, getPostVotes, postID) + var i GetPostVotesRow + err := row.Scan(&i.Upvotes, &i.Downvotes) + return i, err +} + const updateVote = `-- name: UpdateVote :one UPDATE public.post_votes SET vote=$1 diff --git a/enshi_back/db/queries/post_votes_queries.sql b/enshi_back/db/queries/post_votes_queries.sql index ee6b651..ea1443a 100644 --- a/enshi_back/db/queries/post_votes_queries.sql +++ b/enshi_back/db/queries/post_votes_queries.sql @@ -20,4 +20,10 @@ RETURNING *; -- name: GetPostVote :one SELECT vote FROM public.post_votes p_v -WHERE p_v.user_id = $1 and p_v.post_id = $2; \ No newline at end of file +WHERE p_v.user_id = $1 and p_v.post_id = $2; + +-- name: GetPostVotes :one +SELECT count (*) FILTER (WHERE vote = TRUE) as upvotes, +count (*) FILTER (WHERE vote = FALSE) as downvotes +FROM public.post_votes +WHERE post_id = $1; diff --git a/enshi_back/middleware/postVotesMiddleware.go b/enshi_back/middleware/postVotesMiddleware.go index f6805b9..0992fab 100644 --- a/enshi_back/middleware/postVotesMiddleware.go +++ b/enshi_back/middleware/postVotesMiddleware.go @@ -3,13 +3,14 @@ package middleware import ( postvotespolicies "enshi/ABAC/PostVotesPolicies" "enshi/ABAC/rules" + "strings" "github.com/gin-gonic/gin" ) func PostVotesMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - + a := strings.Split(c.Request.URL.Path, "/")[1] switch c.Request.Method { case "DELETE": c.Set("target", postvotespolicies.DELETE_VOTE) @@ -18,7 +19,11 @@ func PostVotesMiddleware() gin.HandlerFunc { c.Set("target", postvotespolicies.CREATE_VOTE) case "GET": - c.Set("target", postvotespolicies.READ_VOTE) + if a != "post-votes" { + c.Set("target", postvotespolicies.READ_VOTE) + } else { + c.Set("target", "") + } } isAllowed, errors := postvotespolicies.PostVotePolicies(c) diff --git a/enshi_back/routes/routesSetup.go b/enshi_back/routes/routesSetup.go index 99e4133..2a21d4f 100644 --- a/enshi_back/routes/routesSetup.go +++ b/enshi_back/routes/routesSetup.go @@ -155,10 +155,15 @@ func SetupRotes(g *gin.Engine) error { ) postVoteGroup.GET( - "post-votes/:post-id", + "post-vote/:post-id", voteroutes.GetVote, ) + postVoteGroup.GET( + "post-votes/:post-id", + voteroutes.GetVotes, + ) + // Admin group routes adminGroup := g.Group("/admin/") adminGroup.Use(middleware.AdminMiddleware()) diff --git a/enshi_back/routes/voteRoutes/createVote.go b/enshi_back/routes/voteRoutes/createVote.go index 58946dc..037317c 100644 --- a/enshi_back/routes/voteRoutes/createVote.go +++ b/enshi_back/routes/voteRoutes/createVote.go @@ -25,6 +25,13 @@ func CreateVote(c *gin.Context) { } postVoteParams.UserID = userId + postId, err := getters.GetInt64Param(c, "post-id") + if err != nil { + rest_api_stuff.BadRequestAnswer(c, err) + return + } + postVoteParams.PostID = postId + query := db_repo.New(db_connection.Dbx) if _, err := query.CreatePostVote(context.Background(), postVoteParams); err != nil { rest_api_stuff.InternalErrorAnswer(c, err) diff --git a/enshi_back/routes/voteRoutes/getVote.go b/enshi_back/routes/voteRoutes/getVote.go index 609e74d..a949618 100644 --- a/enshi_back/routes/voteRoutes/getVote.go +++ b/enshi_back/routes/voteRoutes/getVote.go @@ -14,11 +14,6 @@ import ( func GetVote(c *gin.Context) { var postVoteParams db_repo.GetPostVoteParams - if err := c.BindJSON(&postVoteParams); err != nil { - rest_api_stuff.BadRequestAnswer(c, err) - return - } - userId, err := getters.GetUserIdFromContext(c) if err != nil { rest_api_stuff.BadRequestAnswer(c, err) @@ -26,6 +21,13 @@ func GetVote(c *gin.Context) { } postVoteParams.UserID = userId + postId, err := getters.GetInt64Param(c, "post-id") + if err != nil { + rest_api_stuff.BadRequestAnswer(c, err) + return + } + postVoteParams.PostID = postId + query := db_repo.New(db_connection.Dbx) if voteData, err := query.GetPostVote(context.Background(), postVoteParams); err != nil { rest_api_stuff.InternalErrorAnswer(c, err) diff --git a/enshi_back/routes/voteRoutes/getVotes.go b/enshi_back/routes/voteRoutes/getVotes.go new file mode 100644 index 0000000..a259e64 --- /dev/null +++ b/enshi_back/routes/voteRoutes/getVotes.go @@ -0,0 +1,29 @@ +package voteroutes + +import ( + "context" + rest_api_stuff "enshi/REST_API_stuff" + db_repo "enshi/db/go_queries" + "enshi/db_connection" + "enshi/middleware/getters" + "net/http" + + "github.com/gin-gonic/gin" +) + +func GetVotes(c *gin.Context) { + postId, err := getters.GetInt64Param(c, "post-id") + + if err != nil { + rest_api_stuff.BadRequestAnswer(c, err) + return + } + + query := db_repo.New(db_connection.Dbx) + if voteData, err := query.GetPostVotes(context.Background(), postId); err != nil { + rest_api_stuff.InternalErrorAnswer(c, err) + return + } else { + c.IndentedJSON(http.StatusOK, voteData) + } +}