Relative time voting timing info

This commit is contained in:
Kalle 2022-06-11 21:53:34 +03:00
parent 79278c4cbc
commit 07cc6699f2
8 changed files with 126 additions and 18 deletions

View File

@ -0,0 +1,30 @@
import type * as React from "react";
import { useIsMounted } from "~/hooks/useIsMounted";
export function RelativeTime({
children,
timestamp,
}: {
children: React.ReactNode;
timestamp: number;
}) {
const isMounted = useIsMounted();
return (
<abbr
title={
isMounted
? new Date(timestamp).toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
day: "numeric",
month: "long",
timeZoneName: "short",
})
: undefined
}
>
{children}
</abbr>
);
}

View File

@ -1,6 +1,6 @@
import shuffle from "just-shuffle";
import invariant from "tiny-invariant";
import type { MonthYear} from "~/modules/plus-server";
import type { MonthYear } from "~/modules/plus-server";
import { upcomingVoting } from "~/modules/plus-server";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import type { Unpacked } from "~/utils/types";

11
app/hooks/useIsMounted.ts Normal file
View File

@ -0,0 +1,11 @@
import * as React from "react";
export function useIsMounted() {
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
setIsMounted(true);
}, []);
return isMounted;
}

View File

@ -164,7 +164,7 @@ export function canSuggestNewUserBE({
]);
}
function isVotingActive() {
export function isVotingActive() {
const now = new Date();
const { endDate, startDate } = monthsVotingRange({
month: now.getMonth(),

View File

@ -1,36 +1,76 @@
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { formatDistance } from "date-fns";
import { RelativeTime } from "~/components/RelativeTime";
import { db } from "~/db";
import type { UsersForVoting } from "~/db/models/plusVotes.server";
import { getUser } from "~/modules/auth";
import { monthsVotingRange, upcomingVoting } from "~/modules/plus-server";
import { isVotingActive } from "~/permissions";
interface PlusVotingLoaderData {
usersForVoting?: UsersForVoting;
}
type PlusVotingLoaderData =
// voting is not active OR user is not eligible to vote
| {
type: "timeInfo";
relativeTime: string;
timestamp: number;
timing: "starts" | "ends";
}
// user can vote
| {
type: "voting";
usersForVoting?: UsersForVoting;
}
// user already voted
| { type: "votingInfo"; votingInfo: { placeholder: true } };
export const loader: LoaderFunction = async ({ request }) => {
const user = await getUser(request);
const now = new Date();
const { startDate, endDate } = monthsVotingRange(upcomingVoting(now));
if (!isVotingActive()) {
return json<PlusVotingLoaderData>({
type: "timeInfo",
relativeTime: formatDistance(startDate, now, { addSuffix: true }),
timestamp: startDate.getTime(),
timing: "starts",
});
}
const usersForVoting = db.plusVotes.usersForVoting(user);
if (!usersForVoting) {
return json<PlusVotingLoaderData>({
type: "timeInfo",
relativeTime: formatDistance(endDate, now, { addSuffix: true }),
timestamp: endDate.getTime(),
timing: "ends",
});
}
return json<PlusVotingLoaderData>({
usersForVoting: db.plusVotes.usersForVoting(user),
type: "voting",
usersForVoting,
});
};
export default function PlusVotingPage() {
const data = useLoaderData();
const data = useLoaderData<PlusVotingLoaderData>();
return <NextVotingInfo />;
}
if (data.type === "timeInfo") {
return (
<div className="text-sm text-center">
{data.timing === "starts"
? "Next voting starts"
: "Voting is currently happening. Ends"}{" "}
<RelativeTime timestamp={data.timestamp}>
{data.relativeTime}
</RelativeTime>
</div>
);
}
function NextVotingInfo() {
return (
<div className="text-sm text-center">
Next voting starts{" "}
{monthsVotingRange(upcomingVoting(new Date())).startDate.toLocaleString(
"en-US"
)}
</div>
);
return null;
}

View File

@ -334,6 +334,14 @@ hr {
border-color: var(--theme-transparent);
}
abbr:not([title]) {
text-decoration: none;
}
abbr[title] {
cursor: help;
}
dialog {
border: 0;
margin: auto;

18
package-lock.json generated
View File

@ -15,6 +15,7 @@
"better-sqlite3": "^7.5.3",
"clsx": "^1.1.1",
"countries-list": "^2.6.1",
"date-fns": "^2.28.0",
"fuse.js": "^6.6.2",
"just-shuffle": "^4.0.1",
"randomcolor": "^0.6.2",
@ -4284,6 +4285,18 @@
"node": ">= 6"
}
},
"node_modules/date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz",
@ -17365,6 +17378,11 @@
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
"integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og=="
},
"date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
},
"dayjs": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz",

View File

@ -34,6 +34,7 @@
"better-sqlite3": "^7.5.3",
"clsx": "^1.1.1",
"countries-list": "^2.6.1",
"date-fns": "^2.28.0",
"fuse.js": "^6.6.2",
"just-shuffle": "^4.0.1",
"randomcolor": "^0.6.2",