Skip to content

Commit

Permalink
feat: Move admin eligibility picker. Add loading states for votes and…
Browse files Browse the repository at this point in the history
… eligibility (#69)
  • Loading branch information
ChangoMan authored Aug 29, 2024
1 parent c89a217 commit bf01433
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 96 deletions.
116 changes: 20 additions & 96 deletions packages/nextjs/app/admin/_components/SubmissionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@

import { useRouter } from "next/navigation";
import { SubmissionComments } from "./SubmissionComments";
import { SubmissionEligible } from "./SubmissionEligible";
import "./submission-rating.css";
import { useMutation } from "@tanstack/react-query";
import clsx from "clsx";
import { useAccount } from "wagmi";
import { Address } from "~~/components/scaffold-eth";
import { Submission } from "~~/services/database/repositories/submissions";
import { getFormattedDateTime } from "~~/utils/date";
import { postMutationFetcher } from "~~/utils/react-query";
import { notification } from "~~/utils/scaffold-eth";

export const SubmissionCard = ({ submission }: { submission: Submission }) => {
const { address: connectedAddress } = useAccount();

const { mutateAsync: postNewVote } = useMutation({
const { mutateAsync: postNewVote, isPending: isVotePending } = useMutation({
mutationFn: (newVote: { score: number }) =>
postMutationFetcher(`/api/submissions/${submission.id}/votes`, { body: newVote }),
});
const { mutateAsync: postNewEligible } = useMutation({
mutationFn: (newEligible: { eligible: boolean; clear: boolean }) =>
postMutationFetcher(`/api/submissions/${submission.id}/eligible`, { body: newEligible }),
});
const { refresh } = useRouter();

const vote = async (newScore: number) => {
Expand All @@ -44,36 +41,6 @@ export const SubmissionCard = ({ submission }: { submission: Submission }) => {
}
};

const setEligible = async (newEligible: boolean) => {
try {
const result = await postNewEligible({ eligible: newEligible, clear: false });

notification.success(result.message);
refresh();
} catch (error: any) {
if (error instanceof Error) {
notification.error(error.message);
return;
}
notification.error("Something went wrong");
}
};

const clearEligible = async () => {
try {
const result = await postNewEligible({ eligible: false, clear: true });

notification.success(result.message);
refresh();
} catch (error: any) {
if (error instanceof Error) {
notification.error(error.message);
return;
}
notification.error("Something went wrong");
}
};

const scoreAvg =
submission.votes.length > 0
? (submission.votes.map(vote => vote.score).reduce((a, b) => a + b, 0) / submission.votes.length).toFixed(2)
Expand All @@ -85,60 +52,10 @@ export const SubmissionCard = ({ submission }: { submission: Submission }) => {

return (
<div key={submission.id} className="card bg-base-200 text-secondary-content border border-gray-300 rounded-none">
<div className="card-body p-4">
<SubmissionEligible submission={submission} />
<div className="card-body p-4 pt-6">
<h2 className="card-title mb-3 xl:text-2xl">{submission.title}</h2>
<div className="flex flex-wrap justify-between items-center gap-4">
<div className="flex items-center mb-4">
<input
type="radio"
id={`eligible_${submission.id}_false`}
name={`eligible_${submission.id}`}
className="radio"
checked={submission.eligible === false}
onChange={() => setEligible(false)}
/>
{submission.eligible === false ? (
<div
className="tooltip"
data-tip={`Set by ${submission.eligibleAdmin} on ${submission.eligibleTimestamp ? getFormattedDateTime(new Date(submission.eligibleTimestamp)) : ""}`}
>
<label className="mr-4 ml-1" htmlFor={`eligible_${submission.id}_false`}>
Not eligible
</label>
</div>
) : (
<label className="mr-4 ml-1" htmlFor={`eligible_${submission.id}_false`}>
Not eligible
</label>
)}
<input
type="radio"
id={`eligible_${submission.id}_true`}
name={`eligible_${submission.id}`}
className="radio"
checked={submission.eligible === true}
onChange={() => setEligible(true)}
/>
{submission.eligible === true ? (
<div
className="tooltip"
data-tip={`Set by ${submission.eligibleAdmin} on ${submission.eligibleTimestamp ? getFormattedDateTime(new Date(submission.eligibleTimestamp)) : ""}`}
>
<label className="mr-4 ml-1" htmlFor={`eligible_${submission.id}_true`}>
Eligible
</label>
</div>
) : (
<label className="mr-4 ml-1" htmlFor={`eligible_${submission.id}_true`}>
Eligible
</label>
)}
{submission.eligible !== undefined && (
<button className="cursor-pointer underline text-sm ml-3" onClick={clearEligible}>
Clear
</button>
)}
</div>
<div className="mt-1 flex shrink-0 gap-3">
{submission.linkToRepository && (
<a href={submission.linkToRepository} className="inline-block" target="_blank">
Expand All @@ -161,7 +78,9 @@ export const SubmissionCard = ({ submission }: { submission: Submission }) => {
</div>

<p>{submission.description}</p>
{submission.feedback && <p>Extensions feedback: {submission.feedback}</p>}
{submission.feedback && <p>Extensions Feedback: {submission.feedback}</p>}

<div className="divider my-0" />

<div className="flex items-center justify-between">
<div className="rating flex items-center">
Expand All @@ -186,16 +105,21 @@ export const SubmissionCard = ({ submission }: { submission: Submission }) => {
))}
</div>
{score > 0 && (
<label
className="cursor-pointer underline text-sm ml-3 hover:no-underline"
htmlFor={`rating_${submission.id}_0`}
>
Clear
</label>
<div className="flex items-center gap-2">
{isVotePending && <span className="loading loading-xs"></span>}
<label
className={clsx("cursor-pointer underline text-sm hover:no-underline", {
"text-gray-400 cursor-not-allowed": isVotePending,
})}
htmlFor={`rating_${submission.id}_0`}
>
Clear
</label>
</div>
)}
</div>

<div className="mt-4 flex items-center justify-between">
<div className="mt-3 flex items-center justify-between">
<div className="badge badge-accent flex flex-col shrink-0 p-8 border border-accent-content">
<div className="text-2xl font-bold">{scoreAvg}</div>
<div>{submission.votes.length} votes</div>
Expand Down
151 changes: 151 additions & 0 deletions packages/nextjs/app/admin/_components/SubmissionEligible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"use client";

import { useRouter } from "next/navigation";
import "./submission-rating.css";
import { useMutation } from "@tanstack/react-query";
import clsx from "clsx";
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
import { Submission } from "~~/services/database/repositories/submissions";
import { getFormattedDateTime } from "~~/utils/date";
import { postMutationFetcher } from "~~/utils/react-query";
import { notification } from "~~/utils/scaffold-eth";

const eligibleLabelStyles = "label cursor-pointer text-sm justify-start gap-2";

// Close the dropdown by blurring the active element
const closeDropdown = () => {
const elem = document.activeElement;
if (elem instanceof HTMLElement) {
elem.blur();
}
};

export const SubmissionEligible = ({ submission }: { submission: Submission }) => {
const { mutateAsync: postNewEligible, isPending } = useMutation({
mutationFn: (newEligible: { eligible: boolean; clear: boolean }) =>
postMutationFetcher(`/api/submissions/${submission.id}/eligible`, { body: newEligible }),
});
const { refresh } = useRouter();

const setEligible = async (newEligible: boolean) => {
try {
const result = await postNewEligible({ eligible: newEligible, clear: false });

closeDropdown();
notification.success(result.message);
refresh();
} catch (error: any) {
if (error instanceof Error) {
notification.error(error.message);
return;
}
notification.error("Something went wrong");
}
};

const clearEligible = async () => {
try {
const result = await postNewEligible({ eligible: false, clear: true });

closeDropdown();
notification.success(result.message);
refresh();
} catch (error: any) {
if (error instanceof Error) {
notification.error(error.message);
return;
}
notification.error("Something went wrong");
}
};

let buttonLabel = "Eligibility";
if (submission.eligible === false) {
buttonLabel = "Not Eligible";
}
if (submission.eligible === true) {
buttonLabel = "Eligible";
}

return (
<div className="dropdown dropdown-end">
<div
tabIndex={0}
role="button"
className={clsx("absolute top-0 right-0 btn btn-xs border-0 font-medium text-gray-200 tracking-tighter", {
"text-gray-600 bg-gray-300 hover:bg-gray-400": submission.eligible === null,
"bg-red-800 hover:bg-red-700": submission.eligible === false,
"bg-green-800 hover:bg-green-700": submission.eligible === true,
})}
>
{buttonLabel}
</div>
<div
tabIndex={0}
className="dropdown-content bg-base-100 z-[1] w-auto py-2 px-3 top-7 border border-gray-200 shadow"
>
<div className="flex flex-col">
<div className="form-control">
<label className={eligibleLabelStyles} htmlFor={`eligible_${submission.id}_false`}>
<input
type="radio"
id={`eligible_${submission.id}_false`}
name={`eligible_${submission.id}`}
className="radio checked:bg-opacity-60"
checked={submission.eligible === false}
onChange={() => setEligible(false)}
/>
<span className="label-text tracking-tight">Not Eligible</span>
</label>
</div>
<div className="form-control">
<label className={eligibleLabelStyles} htmlFor={`eligible_${submission.id}_true`}>
<input
type="radio"
id={`eligible_${submission.id}_true`}
name={`eligible_${submission.id}`}
className="radio checked:bg-opacity-60"
checked={submission.eligible === true}
onChange={() => setEligible(true)}
/>
<span className="label-text tracking-tight">Eligible</span>
</label>
</div>
<div className="flex items-center justify-between gap-3 p-2">
{submission.eligible !== undefined && (
<div className="flex items-center gap-2">
<button
className={clsx("cursor-pointer underline text-sm hover:no-underline", {
"text-gray-400 cursor-not-allowed": isPending,
})}
onClick={clearEligible}
>
Clear
</button>
{isPending && <span className="loading loading-xs"></span>}
</div>
)}

{submission.eligible === false && (
<div
className="tooltip before:w-96"
data-tip={`Set by ${submission.eligibleAdmin} on ${submission.eligibleTimestamp ? getFormattedDateTime(new Date(submission.eligibleTimestamp)) : ""}`}
>
<QuestionMarkCircleIcon className="w-4 h-4" />
</div>
)}

{submission.eligible === true && (
<div
className="tooltip"
data-tip={`Set by ${submission.eligibleAdmin} on ${submission.eligibleTimestamp ? getFormattedDateTime(new Date(submission.eligibleTimestamp)) : ""}`}
>
<QuestionMarkCircleIcon className="w-4 h-4" />
</div>
)}
</div>
</div>
</div>
</div>
);
};
10 changes: 10 additions & 0 deletions packages/nextjs/app/admin/_components/SubmissionTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export const SubmissionTabs = ({ submissions }: { submissions: Submission[] }) =
return <SubmissionCard key={submission.id} submission={submission} />;
})
)}
{notVoted.length === 0 && (
<div role="alert" className="alert col-span-2">
<span>There are no submissions to vote on.</span>
</div>
)}
</div>
</div>

Expand All @@ -67,6 +72,11 @@ export const SubmissionTabs = ({ submissions }: { submissions: Submission[] }) =
/>
<div role="tabpanel" className="tab-content py-6">
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{voted.length === 0 && (
<div role="alert" className="alert col-span-2">
<span>You have not voted on any submissions yet.</span>
</div>
)}
{voted.map(submission => {
return <SubmissionCard key={submission.id} submission={submission} />;
})}
Expand Down

0 comments on commit bf01433

Please sign in to comment.