Votes for post
This commit is contained in:
parent
a08e068030
commit
1f7d95a4ff
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"godotenv"
|
"downvotes",
|
||||||
|
"godotenv",
|
||||||
|
"upvotes"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -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 { useQuery } from "@tanstack/react-query";
|
||||||
import { Interweave } from "interweave";
|
import { Interweave } from "interweave";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
@ -7,6 +7,8 @@ import { axiosLocalhost } from "../../api/axios/axios";
|
|||||||
import { userAtom } from "../../AtomStore/AtomStore";
|
import { userAtom } from "../../AtomStore/AtomStore";
|
||||||
import ChangePostButton from "./ChangePostButton/ChangePostButton";
|
import ChangePostButton from "./ChangePostButton/ChangePostButton";
|
||||||
import SkeletonPostLoader from "./SkeletonLoader/SkeletonLoader";
|
import SkeletonPostLoader from "./SkeletonLoader/SkeletonLoader";
|
||||||
|
import VoteButton, { DOWNVOTE, UPVOTE } from "./VoteButton/VoteButton";
|
||||||
|
import VoteCounter from "./VoteCounter/VoteCounter";
|
||||||
|
|
||||||
type TArticleViewer = {
|
type TArticleViewer = {
|
||||||
htmlToParse?: string;
|
htmlToParse?: string;
|
||||||
@ -38,12 +40,26 @@ export default function ArticleViewer(props: TArticleViewer) {
|
|||||||
<Text className="mb-2" as="div" size={"9"}>
|
<Text className="mb-2" as="div" size={"9"}>
|
||||||
{data.title}
|
{data.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Flex className="mt-4 mb-2">
|
<Flex gap={"3"} className="mt-4 mb-2">
|
||||||
<div hidden={data.user_id != user?.id}>
|
<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
|
<ChangePostButton
|
||||||
postId={queryParams["postId"] || ""}
|
postId={queryParams["postId"] || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Separator size={"4"} className="mb-2" />
|
<Separator size={"4"} className="mb-2" />
|
||||||
|
|||||||
65
enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx
Normal file
65
enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>;
|
||||||
|
}
|
||||||
@ -30,6 +30,8 @@ func PostVotePolicies(c *gin.Context) (bool, []error) {
|
|||||||
case READ_VOTE:
|
case READ_VOTE:
|
||||||
return rules.CheckRule(c, postvoterules.PostVoteReadRule)
|
return rules.CheckRule(c, postvoterules.PostVoteReadRule)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return rules.CheckRule(c, postvoterules.PostVotesReadRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
@ -65,6 +65,25 @@ func (q *Queries) GetPostVote(ctx context.Context, arg GetPostVoteParams) (bool,
|
|||||||
return vote, err
|
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
|
const updateVote = `-- name: UpdateVote :one
|
||||||
UPDATE public.post_votes
|
UPDATE public.post_votes
|
||||||
SET vote=$1
|
SET vote=$1
|
||||||
|
|||||||
@ -21,3 +21,9 @@ RETURNING *;
|
|||||||
SELECT vote
|
SELECT vote
|
||||||
FROM public.post_votes p_v
|
FROM public.post_votes p_v
|
||||||
WHERE p_v.user_id = $1 and p_v.post_id = $2;
|
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;
|
||||||
|
|||||||
@ -3,13 +3,14 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
postvotespolicies "enshi/ABAC/PostVotesPolicies"
|
postvotespolicies "enshi/ABAC/PostVotesPolicies"
|
||||||
"enshi/ABAC/rules"
|
"enshi/ABAC/rules"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PostVotesMiddleware() gin.HandlerFunc {
|
func PostVotesMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
a := strings.Split(c.Request.URL.Path, "/")[1]
|
||||||
switch c.Request.Method {
|
switch c.Request.Method {
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
c.Set("target", postvotespolicies.DELETE_VOTE)
|
c.Set("target", postvotespolicies.DELETE_VOTE)
|
||||||
@ -18,7 +19,11 @@ func PostVotesMiddleware() gin.HandlerFunc {
|
|||||||
c.Set("target", postvotespolicies.CREATE_VOTE)
|
c.Set("target", postvotespolicies.CREATE_VOTE)
|
||||||
|
|
||||||
case "GET":
|
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)
|
isAllowed, errors := postvotespolicies.PostVotePolicies(c)
|
||||||
|
|||||||
@ -155,10 +155,15 @@ func SetupRotes(g *gin.Engine) error {
|
|||||||
)
|
)
|
||||||
|
|
||||||
postVoteGroup.GET(
|
postVoteGroup.GET(
|
||||||
"post-votes/:post-id",
|
"post-vote/:post-id",
|
||||||
voteroutes.GetVote,
|
voteroutes.GetVote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
postVoteGroup.GET(
|
||||||
|
"post-votes/:post-id",
|
||||||
|
voteroutes.GetVotes,
|
||||||
|
)
|
||||||
|
|
||||||
// Admin group routes
|
// Admin group routes
|
||||||
adminGroup := g.Group("/admin/")
|
adminGroup := g.Group("/admin/")
|
||||||
adminGroup.Use(middleware.AdminMiddleware())
|
adminGroup.Use(middleware.AdminMiddleware())
|
||||||
|
|||||||
@ -25,6 +25,13 @@ func CreateVote(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
postVoteParams.UserID = userId
|
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)
|
query := db_repo.New(db_connection.Dbx)
|
||||||
if _, err := query.CreatePostVote(context.Background(), postVoteParams); err != nil {
|
if _, err := query.CreatePostVote(context.Background(), postVoteParams); err != nil {
|
||||||
rest_api_stuff.InternalErrorAnswer(c, err)
|
rest_api_stuff.InternalErrorAnswer(c, err)
|
||||||
|
|||||||
@ -14,11 +14,6 @@ import (
|
|||||||
func GetVote(c *gin.Context) {
|
func GetVote(c *gin.Context) {
|
||||||
var postVoteParams db_repo.GetPostVoteParams
|
var postVoteParams db_repo.GetPostVoteParams
|
||||||
|
|
||||||
if err := c.BindJSON(&postVoteParams); err != nil {
|
|
||||||
rest_api_stuff.BadRequestAnswer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userId, err := getters.GetUserIdFromContext(c)
|
userId, err := getters.GetUserIdFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rest_api_stuff.BadRequestAnswer(c, err)
|
rest_api_stuff.BadRequestAnswer(c, err)
|
||||||
@ -26,6 +21,13 @@ func GetVote(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
postVoteParams.UserID = userId
|
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)
|
query := db_repo.New(db_connection.Dbx)
|
||||||
if voteData, err := query.GetPostVote(context.Background(), postVoteParams); err != nil {
|
if voteData, err := query.GetPostVote(context.Background(), postVoteParams); err != nil {
|
||||||
rest_api_stuff.InternalErrorAnswer(c, err)
|
rest_api_stuff.InternalErrorAnswer(c, err)
|
||||||
|
|||||||
29
enshi_back/routes/voteRoutes/getVotes.go
Normal file
29
enshi_back/routes/voteRoutes/getVotes.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user