sendou.ink/app/features/plus-voting/routes/plus.voting.tsx
2026-03-21 15:19:32 +02:00

172 lines
4.5 KiB
TypeScript

import clsx from "clsx";
import { Check } from "lucide-react";
import * as React from "react";
import type { MetaFunction } from "react-router";
import { Form, useLoaderData } from "react-router";
import { Avatar } from "~/components/Avatar";
import { SendouButton } from "~/components/elements/Button";
import { RelativeTime } from "~/components/RelativeTime";
import styles from "~/features/plus-suggestions/plus.module.css";
import { usePlusVoting } from "~/features/plus-voting/core";
import { metaTags } from "~/utils/remix";
import { assertUnreachable } from "~/utils/types";
import { PlusSuggestionComments } from "../../plus-suggestions/routes/plus.suggestions";
import { action } from "../actions/plus.voting.server";
import {
loader,
type PlusVotingLoaderData,
} from "../loaders/plus.voting.server";
export { action, loader };
export const meta: MetaFunction = (args) => {
return metaTags({
title: "Plus Server Voting",
location: args.location,
});
};
export default function PlusVotingPage() {
const data = useLoaderData<PlusVotingLoaderData>();
switch (data.type) {
case "noTimeDefinedInfo": {
return (
<div className="text-center text-lighter text-sm">
Next voting date to be announced
</div>
);
}
case "timeInfo": {
return <VotingTimingInfo {...data} />;
}
case "voting": {
return <Voting {...data} />;
}
default: {
assertUnreachable(data);
}
}
}
function VotingTimingInfo(
data: Extract<PlusVotingLoaderData, { type: "timeInfo" }>,
) {
return (
<div className="stack md">
{data.voted ? (
<div className={styles.votingAlert}>
<Check /> You have voted
</div>
) : null}
<div className="text-sm text-center">
{data.timeInfo.timing === "starts"
? "Next voting starts"
: "Voting is currently happening. Ends"}{" "}
<RelativeTime timestamp={data.timeInfo.timestamp}>
{data.timeInfo.relativeTime}
</RelativeTime>
</div>
</div>
);
}
const tips = [
"Voting progress is saved locally",
"You can use S (-1) and K (+1) keys on desktop to vote",
"You +1 yourself automatically",
];
function Voting(data: Extract<PlusVotingLoaderData, { type: "voting" }>) {
const [randomTip] = React.useState(tips[Math.floor(Math.random() * 3)]);
const { currentUser, previous, votes, addVote, undoLast, isReady, progress } =
usePlusVoting(data.usersForVoting);
if (!isReady) return null;
return (
<div className={clsx(styles.votingContainer, "stack md")}>
<div className="stack xs">
<div className="text-sm text-center">
Voting ends{" "}
<RelativeTime timestamp={data.votingEnds.timestamp}>
{data.votingEnds.relativeTime}
</RelativeTime>
</div>
{progress ? (
<progress
className={styles.votingProgress}
value={progress[0]}
max={progress[1]}
title={`Voting progress ${progress[0]} out of ${progress[1]}`}
/>
) : null}
</div>
{previous ? (
<p className="button-text-paragraph text-sm text-lighter">
Previously{" "}
<span className={previous.score > 0 ? "text-success" : "text-error"}>
{previous.score > 0 ? "+" : ""}
{previous.score}
</span>{" "}
on {previous.user.username}.
<SendouButton
className="ml-auto"
variant="minimal"
onPress={undoLast}
>
Undo?
</SendouButton>
</p>
) : (
<p className="text-sm text-lighter">Tip: {randomTip}</p>
)}
{currentUser ? (
<div className="stack md items-center">
<Avatar user={currentUser.user} size="lg" />
<h2>{currentUser.user.username}</h2>
<div className="stack horizontal lg">
<SendouButton
className={clsx(
styles.votingVoteButton,
styles.votingVoteButtonDownvote,
)}
variant="outlined"
onPress={() => addVote("downvote")}
>
-1
</SendouButton>
<SendouButton
className={styles.votingVoteButton}
variant="outlined"
onPress={() => addVote("upvote")}
>
+1
</SendouButton>
</div>
{currentUser.suggestion ? (
<PlusSuggestionComments
suggestion={currentUser.suggestion}
defaultOpen
/>
) : null}
{currentUser.user.bio ? (
<article className="w-full">
<h2 className={styles.votingBioHeader}>Bio</h2>
{currentUser.user.bio}
</article>
) : null}
</div>
) : (
<Form method="post">
<input type="hidden" name="votes" value={JSON.stringify(votes)} />
<SendouButton className={styles.votingSubmitButton} type="submit">
Submit votes
</SendouButton>
</Form>
)}
</div>
);
}