first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import {useMutation} from '@tanstack/react-query';
import {apiClient} from '@common/http/query-client';
import {BackendResponse} from '@common/http/backend-response/backend-response';
import {VotableModel} from '@common/votes/votable-model';
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
interface Response extends BackendResponse {
model: VotableModel;
}
interface Payload {
voteType: 'upvote' | 'downvote';
}
export function useStoreVote(model: VotableModel) {
return useMutation({
mutationFn: (payload: Payload) => changeVote(model, payload),
onSuccess: response => {
//
},
onError: err => showHttpErrorToast(err),
});
}
function changeVote(model: VotableModel, payload: Payload) {
return apiClient
.post<Response>('vote', {
vote_type: payload.voteType,
model_id: model.id,
model_type: model.model_type,
})
.then(r => r.data);
}

View File

@@ -0,0 +1,74 @@
import {ThumbUpIcon} from '@common/icons/material/ThumbUp';
import {ThumbDownIcon} from '@common/icons/material/ThumbDown';
import {VotableModel} from '@common/votes/votable-model';
import {Button} from '@common/ui/buttons/button';
import {useStoreVote} from '@common/votes/requests/use-store-vote';
import {useState} from 'react';
import {FormattedNumber} from '@common/i18n/formatted-number';
import clsx from 'clsx';
interface Props {
model: VotableModel;
className?: string;
showUpvotesOnly?: boolean;
}
export function ThumbButtons({model, className, showUpvotesOnly}: Props) {
const changeVote = useStoreVote(model);
const [upvotes, setUpvotes] = useState(model.upvotes || 0);
const [downvotes, setDownvotes] = useState(model.downvotes || 0);
const [currentVote, setCurrentVote] = useState(model.current_vote);
const syncLocalState = (model: VotableModel) => {
setUpvotes(model.upvotes);
setDownvotes(model.downvotes);
setCurrentVote(model.current_vote);
};
return (
<div className={clsx(className, 'whitespace-nowrap')}>
<Button
className="gap-6"
sizeClassName="px-8 py-4"
color={currentVote === 'upvote' ? 'primary' : undefined}
disabled={changeVote.isPending}
aria-label="Upvote"
onClick={() => {
changeVote.mutate(
{voteType: 'upvote'},
{
onSuccess: response => syncLocalState(response.model),
},
);
}}
>
<ThumbUpIcon />
<div>
<FormattedNumber value={upvotes} />
</div>
</Button>
{!showUpvotesOnly && (
<Button
className="gap-6"
sizeClassName="px-8 py-4"
color={currentVote === 'downvote' ? 'primary' : undefined}
disabled={changeVote.isPending}
aria-label="Downvote"
onClick={() => {
changeVote.mutate(
{voteType: 'downvote'},
{
onSuccess: response => syncLocalState(response.model),
},
);
}}
>
<ThumbDownIcon />
<div>
<FormattedNumber value={downvotes} />
</div>
</Button>
)}
</div>
);
}

View File

@@ -0,0 +1,9 @@
export interface VotableModel {
id: number;
upvotes: number;
downvotes: number;
score: number;
model_type: string;
current_vote?: 'upvote' | 'downvote';
reports_count?: number;
}

View File

@@ -0,0 +1,6 @@
export interface Vote {
id: number;
user_id: number;
user_ip: string;
vote_type: 'upvote' | 'downvote';
}