Votes for post

This commit is contained in:
Max 2024-12-05 23:11:44 +03:00
parent a08e068030
commit 1f7d95a4ff
13 changed files with 222 additions and 14 deletions

View File

@ -1,5 +1,7 @@
{
"cSpell.words": [
"godotenv"
"downvotes",
"godotenv",
"upvotes"
]
}

View File

@ -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) {
<Text className="mb-2" as="div" size={"9"}>
{data.title}
</Text>
<Flex className="mt-4 mb-2">
<div hidden={data.user_id != user?.id}>
<Flex gap={"3"} className="mt-4 mb-2">
<Flex gap={"1"}>
<VoteButton
vote={UPVOTE}
postId={queryParams["postId"] || ""}
/>
<VoteCounter postId={queryParams["postId"] || ""} />
<VoteButton
vote={DOWNVOTE}
postId={queryParams["postId"] || ""}
/>
</Flex>
<Box hidden={data.user_id != user?.id}>
<ChangePostButton
postId={queryParams["postId"] || ""}
/>
</div>
</Box>
</Flex>
</Flex>
<Separator size={"4"} className="mb-2" />

View File

@ -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 (
<IconButton
variant={data ? "solid" : "outline"}
size={"1"}
onClick={() => voteMutation.mutate()}
>
{props.vote ? <DoubleArrowUpIcon /> : <DoubleArrowDownIcon />}
</IconButton>
);
}

View File

@ -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 <Skeleton>
{calculateRating(0, 0)}
</Skeleton>
}
return <Box>
{calculateRating(data?.upvotes || 0, data?.downvotes || 0)}
</Box>;
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -21,3 +21,9 @@ RETURNING *;
SELECT vote
FROM public.post_votes p_v
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;

View File

@ -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)

View File

@ -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())

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}