mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-01 11:16:32 +02:00
♻️ Update how we store poll status (#957)
This commit is contained in:
parent
7670db6778
commit
04211ac168
10 changed files with 108 additions and 186 deletions
|
@ -75,12 +75,7 @@ export function PollsPage() {
|
|||
data.length > 0 ? (
|
||||
<div className="mx-auto grid max-w-3xl gap-3 sm:gap-4">
|
||||
{data.map((poll) => {
|
||||
const { title, id: pollId, createdAt, closed: paused } = poll;
|
||||
const status = poll.event
|
||||
? "closed"
|
||||
: paused
|
||||
? "paused"
|
||||
: "live";
|
||||
const { title, id: pollId, createdAt, status } = poll;
|
||||
return (
|
||||
<div
|
||||
key={poll.id}
|
||||
|
|
|
@ -34,8 +34,6 @@ export const EventCard = () => {
|
|||
),
|
||||
);
|
||||
|
||||
const status = poll?.event ? "closed" : poll?.closed ? "paused" : "live";
|
||||
|
||||
if (!poll) {
|
||||
return null;
|
||||
}
|
||||
|
@ -49,7 +47,7 @@ export const EventCard = () => {
|
|||
/>
|
||||
<div className="bg-pattern p-4 sm:flex sm:flex-row-reverse sm:justify-between sm:px-6">
|
||||
<div className="mb-2">
|
||||
<PollStatusBadge status={status} />
|
||||
<PollStatusBadge status={poll.status} />
|
||||
</div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4 sm:gap-6">
|
||||
|
|
|
@ -42,7 +42,7 @@ import {
|
|||
import ManagePoll from "@/components/poll/manage-poll";
|
||||
import NotificationsToggle from "@/components/poll/notifications-toggle";
|
||||
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
|
||||
import { PollStatus } from "@/components/poll-status";
|
||||
import { PollStatusLabel } from "@/components/poll-status";
|
||||
import { Skeleton } from "@/components/skeleton";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
|
@ -53,7 +53,6 @@ import { NextPageWithLayout } from "../../types";
|
|||
|
||||
const StatusControl = () => {
|
||||
const poll = usePoll();
|
||||
const state = poll.event ? "closed" : poll.closed ? "paused" : "live";
|
||||
const queryClient = trpc.useUtils();
|
||||
const reopen = trpc.polls.reopen.useMutation({
|
||||
onMutate: () => {
|
||||
|
@ -110,7 +109,7 @@ const StatusControl = () => {
|
|||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
<PollStatus status={state} />
|
||||
<PollStatusLabel status={poll.status} />
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { PollStatus } from "@rallly/database";
|
||||
import { cn } from "@rallly/ui";
|
||||
import { CheckCircleIcon, PauseCircleIcon, RadioIcon } from "lucide-react";
|
||||
|
||||
import { Trans } from "@/components/trans";
|
||||
import { IconComponent } from "@/types";
|
||||
|
||||
export type PollState = "live" | "paused" | "closed";
|
||||
|
||||
const LabelWithIcon = ({
|
||||
icon: Icon,
|
||||
children,
|
||||
|
@ -23,11 +22,11 @@ const LabelWithIcon = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const PollStatus = ({
|
||||
export const PollStatusLabel = ({
|
||||
status,
|
||||
className,
|
||||
}: {
|
||||
status: PollState;
|
||||
status: PollStatus;
|
||||
className?: string;
|
||||
}) => {
|
||||
switch (status) {
|
||||
|
@ -43,7 +42,7 @@ export const PollStatus = ({
|
|||
<Trans i18nKey="pollStatusPaused" defaults="Paused" />
|
||||
</LabelWithIcon>
|
||||
);
|
||||
case "closed":
|
||||
case "finalized":
|
||||
return (
|
||||
<LabelWithIcon icon={CheckCircleIcon} className={className}>
|
||||
<Trans i18nKey="pollStatusClosed" defaults="Finalized" />
|
||||
|
@ -52,13 +51,13 @@ export const PollStatus = ({
|
|||
}
|
||||
};
|
||||
|
||||
export const PollStatusBadge = ({ status }: { status: PollState }) => {
|
||||
export const PollStatusBadge = ({ status }: { status: PollStatus }) => {
|
||||
return (
|
||||
<PollStatus
|
||||
<PollStatusLabel
|
||||
className={cn("rounded-full border py-0.5 pl-1.5 pr-3 text-sm", {
|
||||
"border-blue-500 text-blue-500": status === "live",
|
||||
"border-gray-500 text-gray-500": status === "paused",
|
||||
"border-green-500 text-green-500": status === "closed",
|
||||
"border-green-500 text-green-500": status === "finalized",
|
||||
})}
|
||||
status={status}
|
||||
/>
|
||||
|
|
|
@ -65,7 +65,7 @@ const NotificationsToggle: React.FunctionComponent = () => {
|
|||
loading={watch.isLoading || unwatch.isLoading}
|
||||
icon={isWatching ? BellRingIcon : BellOffIcon}
|
||||
data-testid="notifications-toggle"
|
||||
disabled={poll.demo || user.isGuest}
|
||||
disabled={user.isGuest}
|
||||
className="flex items-center gap-2 px-2.5"
|
||||
onClick={async () => {
|
||||
if (user.isGuest) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { User, VoteType } from "@rallly/database";
|
||||
import { PollStatus, User, VoteType } from "@rallly/database";
|
||||
|
||||
export type GetPollApiResponse = {
|
||||
id: string;
|
||||
|
@ -9,10 +9,9 @@ export type GetPollApiResponse = {
|
|||
user: User | null;
|
||||
timeZone: string | null;
|
||||
adminUrlId: string;
|
||||
status: PollStatus;
|
||||
participantUrlId: string;
|
||||
closed: boolean;
|
||||
legacy: boolean;
|
||||
demo: boolean;
|
||||
createdAt: Date;
|
||||
deleted: boolean;
|
||||
};
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
import { prisma, VoteType } from "@rallly/database";
|
||||
import dayjs from "dayjs";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const participantData: Array<{ name: string; votes: VoteType[] }> = [
|
||||
{
|
||||
name: "Reed",
|
||||
votes: ["yes", "no", "yes", "no"],
|
||||
},
|
||||
{
|
||||
name: "Susan",
|
||||
votes: ["yes", "yes", "yes", "no"],
|
||||
},
|
||||
{
|
||||
name: "Johnny",
|
||||
votes: ["no", "no", "yes", "yes"],
|
||||
},
|
||||
{
|
||||
name: "Ben",
|
||||
votes: ["yes", "yes", "yes", "yes"],
|
||||
},
|
||||
];
|
||||
|
||||
const optionValues = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"];
|
||||
|
||||
export const createPoll = async () => {
|
||||
const pollId = nanoid();
|
||||
|
||||
const adminUrlId = nanoid();
|
||||
|
||||
const options: Array<{ start: Date; id: string }> = [];
|
||||
|
||||
for (let i = 0; i < optionValues.length; i++) {
|
||||
options.push({ id: nanoid(), start: new Date(optionValues[i]) });
|
||||
}
|
||||
|
||||
const participants: Array<{
|
||||
name: string;
|
||||
id: string;
|
||||
userId: string;
|
||||
createdAt: Date;
|
||||
}> = [];
|
||||
|
||||
const votes: Array<{
|
||||
optionId: string;
|
||||
participantId: string;
|
||||
type: VoteType;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < participantData.length; i++) {
|
||||
const { name, votes: participantVotes } = participantData[i];
|
||||
const participantId = nanoid();
|
||||
participants.push({
|
||||
id: participantId,
|
||||
name,
|
||||
userId: "user-demo",
|
||||
createdAt: dayjs()
|
||||
.add(i * -1, "minutes")
|
||||
.toDate(),
|
||||
});
|
||||
|
||||
options.forEach((option, index) => {
|
||||
votes.push({
|
||||
optionId: option.id,
|
||||
participantId,
|
||||
type: participantVotes[index],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.poll.create({
|
||||
data: {
|
||||
id: pollId,
|
||||
title: "Lunch Meeting",
|
||||
location: "Starbucks, 901 New York Avenue",
|
||||
description: `Hey everyone, please choose the dates when you are available to meet for our monthly get together. Looking forward to see you all!`,
|
||||
demo: true,
|
||||
adminUrlId,
|
||||
participantUrlId: nanoid(),
|
||||
userId: "guest-user",
|
||||
options: {
|
||||
createMany: {
|
||||
data: options,
|
||||
},
|
||||
},
|
||||
participants: {
|
||||
createMany: {
|
||||
data: participants,
|
||||
},
|
||||
},
|
||||
votes: {
|
||||
createMany: {
|
||||
data: votes,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return pollId;
|
||||
};
|
||||
|
||||
export const deletePoll = async (pollId: string) => {
|
||||
await prisma.$transaction([
|
||||
prisma.vote.deleteMany({
|
||||
where: {
|
||||
pollId,
|
||||
},
|
||||
}),
|
||||
prisma.option.deleteMany({
|
||||
where: {
|
||||
pollId,
|
||||
},
|
||||
}),
|
||||
prisma.participant.deleteMany({
|
||||
where: {
|
||||
pollId,
|
||||
},
|
||||
}),
|
||||
prisma.comment.deleteMany({
|
||||
where: {
|
||||
pollId,
|
||||
},
|
||||
}),
|
||||
prisma.poll.deleteMany({
|
||||
where: {
|
||||
id: pollId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
};
|
|
@ -87,7 +87,6 @@ export const polls = router({
|
|||
timeZone: input.timeZone,
|
||||
location: input.location,
|
||||
description: input.description,
|
||||
demo: input.demo,
|
||||
adminUrlId: adminToken,
|
||||
participantUrlId,
|
||||
userId: ctx.user.id,
|
||||
|
@ -355,12 +354,11 @@ export const polls = router({
|
|||
adminUrlId: true,
|
||||
participantUrlId: true,
|
||||
closed: true,
|
||||
legacy: true,
|
||||
status: true,
|
||||
hideParticipants: true,
|
||||
disableComments: true,
|
||||
hideScores: true,
|
||||
requireParticipantEmail: true,
|
||||
demo: true,
|
||||
options: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -436,6 +434,7 @@ export const polls = router({
|
|||
timeZone: true,
|
||||
adminUrlId: true,
|
||||
participantUrlId: true,
|
||||
status: true,
|
||||
event: {
|
||||
select: {
|
||||
start: true,
|
||||
|
@ -551,14 +550,20 @@ export const polls = router({
|
|||
eventStart = eventStart.tz(poll.timeZone, true);
|
||||
}
|
||||
|
||||
await prisma.event.create({
|
||||
await prisma.poll.update({
|
||||
where: {
|
||||
id: input.pollId,
|
||||
},
|
||||
data: {
|
||||
pollId: poll.id,
|
||||
optionId: input.optionId,
|
||||
start: eventStart.toDate(),
|
||||
duration: option.duration,
|
||||
title: poll.title,
|
||||
userId: ctx.user.id,
|
||||
event: {
|
||||
create: {
|
||||
optionId: input.optionId,
|
||||
start: eventStart.toDate(),
|
||||
duration: option.duration,
|
||||
title: poll.title,
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -721,18 +726,16 @@ export const polls = router({
|
|||
)
|
||||
.mutation(async ({ input }) => {
|
||||
await prisma.$transaction([
|
||||
prisma.event.delete({
|
||||
where: {
|
||||
pollId: input.pollId,
|
||||
},
|
||||
}),
|
||||
prisma.poll.update({
|
||||
where: {
|
||||
id: input.pollId,
|
||||
},
|
||||
data: {
|
||||
eventId: null,
|
||||
closed: false,
|
||||
event: {
|
||||
delete: true,
|
||||
},
|
||||
status: "live",
|
||||
closed: false, // @deprecated
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
@ -749,7 +752,8 @@ export const polls = router({
|
|||
id: input.pollId,
|
||||
},
|
||||
data: {
|
||||
closed: true,
|
||||
closed: true, // TODO (Luke Vella) [2023-12-05]: Remove this
|
||||
status: "paused",
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
@ -827,6 +831,7 @@ export const polls = router({
|
|||
},
|
||||
data: {
|
||||
closed: false,
|
||||
status: "live",
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `demo` on the `polls` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `legacy` on the `polls` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[event_id]` on the table `polls` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "poll_status" AS ENUM ('live', 'paused', 'finalized');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "polls" DROP COLUMN "demo",
|
||||
DROP COLUMN "legacy",
|
||||
ADD COLUMN "status" "poll_status";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "polls_event_id_key" ON "polls"("event_id");
|
||||
|
||||
-- Fix an issue where the "event_id" column was not being set
|
||||
UPDATE "polls"
|
||||
SET "event_id" = "events"."id"
|
||||
FROM "events"
|
||||
WHERE "events"."poll_id" = "polls"."id";
|
||||
|
||||
-- Set the "status" column to corressponding enum value
|
||||
-- If "closed" is true, set to "paused"
|
||||
-- If a poll has an "event_id", set to "finalized"
|
||||
-- If a poll has a "deletedAt" date, set to "deleted"
|
||||
-- Otherwise set to "live"
|
||||
UPDATE "polls"
|
||||
SET "status" = CASE
|
||||
WHEN "closed" = true THEN 'paused'::poll_status
|
||||
WHEN "event_id" IS NOT NULL THEN 'finalized'::poll_status
|
||||
ELSE 'live'::poll_status
|
||||
END;
|
||||
|
||||
-- Make the "status" column non-nullable and default to "live"
|
||||
ALTER TABLE "polls"
|
||||
ALTER COLUMN "status" SET NOT NULL,
|
||||
ALTER COLUMN "status" SET DEFAULT 'live';
|
||||
|
||||
|
||||
DROP INDEX "events_poll_id_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "events_poll_id_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "events" DROP COLUMN "poll_id";
|
|
@ -117,6 +117,14 @@ enum ParticipantVisibility {
|
|||
@@map("participant_visibility")
|
||||
}
|
||||
|
||||
enum PollStatus {
|
||||
live
|
||||
paused
|
||||
finalized
|
||||
|
||||
@@map("poll_status")
|
||||
}
|
||||
|
||||
model Poll {
|
||||
id String @id @unique @map("id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
@ -125,46 +133,45 @@ model Poll {
|
|||
title String
|
||||
description String?
|
||||
location String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
userId String @map("user_id")
|
||||
votes Vote[]
|
||||
timeZone String? @map("time_zone")
|
||||
options Option[]
|
||||
participants Participant[]
|
||||
watchers Watcher[]
|
||||
demo Boolean @default(false)
|
||||
comments Comment[]
|
||||
legacy Boolean @default(false) // @deprecated
|
||||
closed Boolean @default(false) // we use this to indicate whether a poll is paused
|
||||
closed Boolean @default(false) // @deprecated
|
||||
status PollStatus @default(live)
|
||||
deleted Boolean @default(false)
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
touchedAt DateTime @default(now()) @map("touched_at")
|
||||
participantUrlId String @unique @map("participant_url_id")
|
||||
adminUrlId String @unique @map("admin_url_id")
|
||||
eventId String? @map("event_id")
|
||||
event Event?
|
||||
eventId String? @unique @map("event_id")
|
||||
hideParticipants Boolean @default(false) @map("hide_participants")
|
||||
hideScores Boolean @default(false) @map("hide_scores")
|
||||
disableComments Boolean @default(false) @map("disable_comments")
|
||||
requireParticipantEmail Boolean @default(false) @map("require_participant_email")
|
||||
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
event Event? @relation(fields: [eventId], references: [id])
|
||||
options Option[]
|
||||
participants Participant[]
|
||||
watchers Watcher[]
|
||||
comments Comment[]
|
||||
votes Vote[]
|
||||
|
||||
@@index([userId], type: Hash)
|
||||
@@map("polls")
|
||||
}
|
||||
|
||||
model Event {
|
||||
id String @id @default(cuid())
|
||||
pollId String @unique @map("poll_id")
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
poll Poll @relation(fields: [pollId], references: [id])
|
||||
optionId String @map("option_id")
|
||||
title String
|
||||
start DateTime @db.Timestamp(0)
|
||||
duration Int @default(0) @map("duration_minutes")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([pollId], type: Hash)
|
||||
Poll Poll?
|
||||
|
||||
@@index([userId], type: Hash)
|
||||
@@map("events")
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue