mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-14 09:26:49 +02:00
🏗️ Update how we store date values
This also fixes a few bugs: - some values had end times that were before the start times - some values has end times a couple of days in the future It’s not entirely clear how users were able to set these values but this update fixes these values and helps avoid similar issues in the future.
This commit is contained in:
parent
8a9159c322
commit
51c5016656
12 changed files with 203 additions and 158 deletions
|
@ -4,7 +4,6 @@ import React from "react";
|
|||
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
|
||||
import { encodeDateOption } from "../utils/date-time-utils";
|
||||
import { trpc } from "../utils/trpc";
|
||||
import { Button } from "./button";
|
||||
import {
|
||||
|
@ -102,7 +101,6 @@ const Page: React.FunctionComponent = () => {
|
|||
|
||||
await createPoll.mutateAsync({
|
||||
title: title,
|
||||
type: "date",
|
||||
location: formData?.eventDetails?.location,
|
||||
description: formData?.eventDetails?.description,
|
||||
user: session.user.isGuest
|
||||
|
@ -112,7 +110,10 @@ const Page: React.FunctionComponent = () => {
|
|||
}
|
||||
: undefined,
|
||||
timeZone: formData?.options?.timeZone,
|
||||
options: required(formData?.options?.options).map(encodeDateOption),
|
||||
options: required(formData?.options?.options).map((option) => ({
|
||||
startDate: option.type === "date" ? option.date : option.start,
|
||||
endDate: option.type === "timeSlot" ? option.end : undefined,
|
||||
})),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -265,16 +265,11 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
|||
<TimePicker
|
||||
value={startDate}
|
||||
onChange={(newStart) => {
|
||||
let newEnd = dayjs(newStart).add(
|
||||
const newEnd = dayjs(newStart).add(
|
||||
duration,
|
||||
"minutes",
|
||||
);
|
||||
|
||||
if (!newEnd.isSame(newStart, "day")) {
|
||||
newEnd = newEnd
|
||||
.set("hour", 23)
|
||||
.set("minute", 45);
|
||||
}
|
||||
// replace enter with updated start time
|
||||
onChange([
|
||||
...options.slice(0, index),
|
||||
|
@ -293,9 +288,7 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
|||
/>
|
||||
<TimePicker
|
||||
value={new Date(option.end)}
|
||||
startFrom={dayjs(startDate)
|
||||
.add(15, "minutes")
|
||||
.toDate()}
|
||||
after={startDate}
|
||||
onChange={(newEnd) => {
|
||||
onChange([
|
||||
...options.slice(0, index),
|
||||
|
@ -330,7 +323,15 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
|||
const lastOption = expectTimeOption(
|
||||
optionsForDay[optionsForDay.length - 1].option,
|
||||
);
|
||||
const startTime = lastOption.end;
|
||||
|
||||
const startTime = dayjs(lastOption.end).isSame(
|
||||
lastOption.start,
|
||||
"day",
|
||||
)
|
||||
? // if the end time of the previous option is on the same day as the start time, use the end time
|
||||
lastOption.end
|
||||
: // otherwise use the start time
|
||||
lastOption.start;
|
||||
|
||||
onChange([
|
||||
...options,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Listbox } from "@headlessui/react";
|
|||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
|
||||
import { getDuration } from "@/utils/date-time-utils";
|
||||
import { stopPropagation } from "@/utils/stop-propagation";
|
||||
|
||||
import { useDayjs } from "../../../../utils/dayjs";
|
||||
|
@ -17,7 +18,7 @@ import { styleMenuItem } from "../../../menu-styles";
|
|||
|
||||
export interface TimePickerProps {
|
||||
value: Date;
|
||||
startFrom?: Date;
|
||||
after?: Date;
|
||||
className?: string;
|
||||
onChange?: (value: Date) => void;
|
||||
}
|
||||
|
@ -26,10 +27,11 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
|
|||
value,
|
||||
onChange,
|
||||
className,
|
||||
startFrom,
|
||||
after,
|
||||
}) => {
|
||||
const { dayjs } = useDayjs();
|
||||
const { reference, floating, x, y, strategy, refs } = useFloating({
|
||||
placement: "bottom-start",
|
||||
strategy: "fixed",
|
||||
middleware: [
|
||||
offset(5),
|
||||
|
@ -38,7 +40,7 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
|
|||
apply: ({ rects }) => {
|
||||
if (refs.floating.current) {
|
||||
Object.assign(refs.floating.current.style, {
|
||||
width: `${rects.reference.width}px`,
|
||||
minWidth: `${rects.reference.width}px`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -47,15 +49,11 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
|
|||
});
|
||||
|
||||
const renderOptions = () => {
|
||||
const startFromDate = startFrom
|
||||
? dayjs(startFrom)
|
||||
: dayjs(value).startOf("day");
|
||||
const startFromDate = after ? dayjs(after) : dayjs(value).startOf("day");
|
||||
|
||||
const options: React.ReactNode[] = [];
|
||||
const startMinute =
|
||||
startFromDate.get("hour") * 60 + startFromDate.get("minute");
|
||||
const intervals = Math.floor((1440 - startMinute) / 15);
|
||||
for (let i = 0; i < intervals; i++) {
|
||||
|
||||
for (let i = 1; i <= 96; i++) {
|
||||
const optionValue = startFromDate.add(i * 15, "minutes");
|
||||
options.push(
|
||||
<Listbox.Option
|
||||
|
@ -63,7 +61,14 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
|
|||
className={styleMenuItem}
|
||||
value={optionValue.format("YYYY-MM-DDTHH:mm:ss")}
|
||||
>
|
||||
{optionValue.format("LT")}
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{optionValue.format("LT")}</span>
|
||||
{after ? (
|
||||
<span className="text-sm text-slate-500">
|
||||
{getDuration(dayjs(after), optionValue)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</Listbox.Option>,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Placement } from "@floating-ui/react-dom-interactions";
|
||||
import dayjs from "dayjs";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
||||
|
@ -23,6 +24,15 @@ import { useUpdatePollMutation } from "./mutations";
|
|||
|
||||
const PollOptionsForm = React.lazy(() => import("../forms/poll-options-form"));
|
||||
|
||||
const convertOptionToString = (option: { start: Date; duration: number }) => {
|
||||
const start = dayjs(option.start);
|
||||
return option.duration === 0
|
||||
? start.format("YYYY-MM-DD")
|
||||
: `${start.format("YYYY-MM-DDTHH:mm:ss")}/${start
|
||||
.add(option.duration, "minute")
|
||||
.format("YYYY-MM-DDTHH:mm:ss")}`;
|
||||
};
|
||||
|
||||
const ManagePoll: React.FunctionComponent<{
|
||||
placement?: Placement;
|
||||
}> = ({ placement }) => {
|
||||
|
@ -67,18 +77,20 @@ const ManagePoll: React.FunctionComponent<{
|
|||
name="pollOptions"
|
||||
title={poll.title}
|
||||
defaultValues={{
|
||||
navigationDate: poll.options[0].value.split("/")[0],
|
||||
navigationDate: poll.options[0].start.toString(),
|
||||
options: poll.options.map((option) => {
|
||||
const [start, end] = option.value.split("/");
|
||||
return end
|
||||
const start = dayjs(option.start);
|
||||
return option.duration > 0
|
||||
? {
|
||||
type: "timeSlot",
|
||||
start,
|
||||
end,
|
||||
start: start.format("YYYY-MM-DDTHH:mm:ss"),
|
||||
end: start
|
||||
.add(option.duration, "minute")
|
||||
.format("YYYY-MM-DDTHH:mm:ss"),
|
||||
}
|
||||
: {
|
||||
type: "date",
|
||||
date: start,
|
||||
date: start.format("YYYY-MM-DD"),
|
||||
};
|
||||
}),
|
||||
timeZone: poll.timeZone ?? "",
|
||||
|
@ -86,12 +98,14 @@ const ManagePoll: React.FunctionComponent<{
|
|||
onSubmit={(data) => {
|
||||
const encodedOptions = data.options.map(encodeDateOption);
|
||||
const optionsToDelete = poll.options.filter((option) => {
|
||||
return !encodedOptions.includes(option.value);
|
||||
return !encodedOptions.includes(convertOptionToString(option));
|
||||
});
|
||||
|
||||
const optionsToAdd = encodedOptions.filter(
|
||||
(encodedOption) =>
|
||||
!poll.options.find((o) => o.value === encodedOption),
|
||||
!poll.options.find(
|
||||
(o) => convertOptionToString(o) === encodedOption,
|
||||
),
|
||||
);
|
||||
|
||||
const onOk = () => {
|
||||
|
|
|
@ -2,8 +2,6 @@ import { Prisma, prisma } from "@rallly/database";
|
|||
import dayjs from "dayjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { parseValue } from "../../utils/date-time-utils";
|
||||
|
||||
/**
|
||||
* DANGER: This endpoint will permanently delete polls.
|
||||
*/
|
||||
|
@ -25,32 +23,26 @@ export default async function handler(
|
|||
}
|
||||
|
||||
// get polls that have not been accessed for over 30 days
|
||||
const inactivePolls = await prisma.$queryRaw<
|
||||
Array<{ id: string; max: string }>
|
||||
>`
|
||||
SELECT polls.id, MAX(options.value) FROM polls
|
||||
JOIN options ON options.poll_id = polls.id
|
||||
WHERE touched_at <= ${dayjs().add(-30, "days").toDate()} AND deleted = false
|
||||
GROUP BY polls.id;
|
||||
`;
|
||||
const thirtyDaysAgo = dayjs().subtract(30, "days").toDate();
|
||||
|
||||
const pollsToSoftDelete: string[] = [];
|
||||
|
||||
// keep polls that have options that are in the future
|
||||
inactivePolls.forEach(({ id, max: value }) => {
|
||||
const parsedValue = parseValue(value);
|
||||
const date =
|
||||
parsedValue.type === "date" ? parsedValue.date : parsedValue.end;
|
||||
|
||||
if (dayjs(date).isBefore(dayjs())) {
|
||||
pollsToSoftDelete.push(id);
|
||||
}
|
||||
const oldPollsWithAllPastOptions = await prisma.poll.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
touchedAt: { lte: thirtyDaysAgo },
|
||||
options: {
|
||||
every: {
|
||||
start: { lt: new Date() },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const softDeletedPolls = await prisma.poll.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: pollsToSoftDelete,
|
||||
in: oldPollsWithAllPastOptions.map(({ id }) => id),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { sendEmail } from "@rallly/emails";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import dayjs from "dayjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { absoluteUrl } from "../../utils/absolute-url";
|
||||
|
@ -10,46 +11,6 @@ import { comments } from "./polls/comments";
|
|||
import { demo } from "./polls/demo";
|
||||
import { participants } from "./polls/participants";
|
||||
|
||||
const defaultSelectFields: {
|
||||
id: true;
|
||||
timeZone: true;
|
||||
title: true;
|
||||
location: true;
|
||||
description: true;
|
||||
createdAt: true;
|
||||
adminUrlId: true;
|
||||
participantUrlId: true;
|
||||
closed: true;
|
||||
legacy: true;
|
||||
demo: true;
|
||||
options: {
|
||||
orderBy: {
|
||||
value: "asc";
|
||||
};
|
||||
};
|
||||
user: true;
|
||||
deleted: true;
|
||||
} = {
|
||||
id: true,
|
||||
timeZone: true,
|
||||
title: true,
|
||||
location: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
adminUrlId: true,
|
||||
participantUrlId: true,
|
||||
closed: true,
|
||||
legacy: true,
|
||||
demo: true,
|
||||
options: {
|
||||
orderBy: {
|
||||
value: "asc",
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
deleted: true,
|
||||
};
|
||||
|
||||
const getPollIdFromAdminUrlId = async (urlId: string) => {
|
||||
const res = await prisma.poll.findUnique({
|
||||
select: {
|
||||
|
@ -72,7 +33,6 @@ export const polls = router({
|
|||
.input(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
type: z.literal("date"),
|
||||
timeZone: z.string().optional(),
|
||||
location: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
|
@ -82,7 +42,12 @@ export const polls = router({
|
|||
email: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
options: z.string().array(),
|
||||
options: z
|
||||
.object({
|
||||
startDate: z.string(),
|
||||
endDate: z.string().optional(),
|
||||
})
|
||||
.array(),
|
||||
demo: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
|
@ -120,7 +85,6 @@ export const polls = router({
|
|||
data: {
|
||||
id: await nanoid(),
|
||||
title: input.title,
|
||||
type: input.type,
|
||||
timeZone: input.timeZone,
|
||||
location: input.location,
|
||||
description: input.description,
|
||||
|
@ -137,8 +101,14 @@ export const polls = router({
|
|||
: undefined,
|
||||
options: {
|
||||
createMany: {
|
||||
data: input.options.map((value) => ({
|
||||
value,
|
||||
data: input.options.map((option) => ({
|
||||
start: new Date(option.startDate),
|
||||
duration: option.endDate
|
||||
? dayjs(option.endDate).diff(
|
||||
dayjs(option.startDate),
|
||||
"minute",
|
||||
)
|
||||
: 0,
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
@ -193,15 +163,26 @@ export const polls = router({
|
|||
|
||||
if (input.optionsToAdd && input.optionsToAdd.length > 0) {
|
||||
await prisma.option.createMany({
|
||||
data: input.optionsToAdd.map((optionValue) => ({
|
||||
value: optionValue,
|
||||
data: input.optionsToAdd.map((optionValue) => {
|
||||
const [start, end] = optionValue.split("/");
|
||||
if (end) {
|
||||
return {
|
||||
start: new Date(start),
|
||||
duration: dayjs(end).diff(dayjs(start), "minute"),
|
||||
pollId,
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
start: new Date(start.substring(0, 10) + "T00:00:00"),
|
||||
pollId,
|
||||
};
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.poll.update({
|
||||
select: defaultSelectFields,
|
||||
select: { id: true },
|
||||
where: {
|
||||
id: pollId,
|
||||
},
|
||||
|
@ -303,7 +284,24 @@ export const polls = router({
|
|||
.query(async ({ input }) => {
|
||||
const res = await prisma.poll.findUnique({
|
||||
select: {
|
||||
...defaultSelectFields,
|
||||
id: true,
|
||||
timeZone: true,
|
||||
title: true,
|
||||
location: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
adminUrlId: true,
|
||||
participantUrlId: true,
|
||||
closed: true,
|
||||
legacy: true,
|
||||
demo: true,
|
||||
options: {
|
||||
orderBy: {
|
||||
start: "asc",
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
deleted: true,
|
||||
watchers: {
|
||||
select: {
|
||||
userId: true,
|
||||
|
@ -333,7 +331,32 @@ export const polls = router({
|
|||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const res = await prisma.poll.findUnique({
|
||||
select: { userId: true, ...defaultSelectFields },
|
||||
select: {
|
||||
id: true,
|
||||
timeZone: true,
|
||||
title: true,
|
||||
location: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
adminUrlId: true,
|
||||
participantUrlId: true,
|
||||
closed: true,
|
||||
legacy: true,
|
||||
demo: true,
|
||||
options: {
|
||||
orderBy: {
|
||||
start: "asc",
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
userId: true,
|
||||
deleted: true,
|
||||
watchers: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
participantUrlId: input.urlId,
|
||||
},
|
||||
|
|
|
@ -30,10 +30,10 @@ export const demo = router({
|
|||
const adminUrlId = await nanoid();
|
||||
const demoUser = { name: "John Example", email: "noreply@rallly.co" };
|
||||
|
||||
const options: Array<{ value: string; id: string }> = [];
|
||||
const options: Array<{ start: Date; id: string }> = [];
|
||||
|
||||
for (let i = 0; i < optionValues.length; i++) {
|
||||
options.push({ id: await nanoid(), value: optionValues[i] });
|
||||
options.push({ id: await nanoid(), start: new Date(optionValues[i]) });
|
||||
}
|
||||
|
||||
const participants: Array<{
|
||||
|
@ -74,7 +74,6 @@ export const demo = router({
|
|||
data: {
|
||||
id: await nanoid(),
|
||||
title: "Lunch Meeting",
|
||||
type: "date",
|
||||
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,
|
||||
|
|
|
@ -65,7 +65,9 @@ export const decodeOptions = (
|
|||
):
|
||||
| { pollType: "date"; options: ParsedDateOption[] }
|
||||
| { pollType: "timeSlot"; options: ParsedTimeSlotOption[] } => {
|
||||
const pollType = isTimeSlot(options[0].value) ? "timeSlot" : "date";
|
||||
const pollType = options.some(({ duration }) => duration > 0)
|
||||
? "timeSlot"
|
||||
: "date";
|
||||
|
||||
if (pollType === "timeSlot") {
|
||||
return {
|
||||
|
@ -83,12 +85,7 @@ export const decodeOptions = (
|
|||
};
|
||||
|
||||
const parseDateOption = (option: Option): ParsedDateOption => {
|
||||
const dateString =
|
||||
option.value.indexOf("T") === -1
|
||||
? // we add the time because otherwise Date will assume UTC time which might change the day for some time zones
|
||||
option.value + "T00:00:00"
|
||||
: option.value;
|
||||
const date = dayjs(dateString);
|
||||
const date = dayjs(option.start);
|
||||
return {
|
||||
type: "date",
|
||||
optionId: option.id,
|
||||
|
@ -104,16 +101,12 @@ const parseTimeSlotOption = (
|
|||
timeZone: string | null,
|
||||
targetTimeZone: string,
|
||||
): ParsedTimeSlotOption => {
|
||||
const [start, end] = option.value.split("/");
|
||||
|
||||
const startDate =
|
||||
timeZone && targetTimeZone
|
||||
? dayjs(start).tz(timeZone, true).tz(targetTimeZone)
|
||||
: dayjs(start);
|
||||
const endDate =
|
||||
timeZone && targetTimeZone
|
||||
? dayjs(end).tz(timeZone, true).tz(targetTimeZone)
|
||||
: dayjs(end);
|
||||
? dayjs(option.start).tz(timeZone, true).tz(targetTimeZone)
|
||||
: dayjs(option.start);
|
||||
|
||||
const endDate = startDate.add(option.duration, "minute");
|
||||
|
||||
return {
|
||||
type: "timeSlot",
|
||||
|
|
|
@ -176,7 +176,6 @@ const seedData = async () => {
|
|||
{
|
||||
title: "Active Poll",
|
||||
id: "active-poll",
|
||||
type: "date",
|
||||
userId: "user1",
|
||||
participantUrlId: "p1",
|
||||
adminUrlId: "a1",
|
||||
|
@ -185,7 +184,6 @@ const seedData = async () => {
|
|||
{
|
||||
title: "Deleted poll",
|
||||
id: "deleted-poll-6d",
|
||||
type: "date",
|
||||
userId: "user1",
|
||||
deleted: true,
|
||||
deletedAt: dayjs().add(-6, "days").toDate(),
|
||||
|
@ -196,7 +194,6 @@ const seedData = async () => {
|
|||
{
|
||||
title: "Deleted poll 7d",
|
||||
id: "deleted-poll-7d",
|
||||
type: "date",
|
||||
userId: "user1",
|
||||
deleted: true,
|
||||
deletedAt: dayjs().add(-7, "days").toDate(),
|
||||
|
@ -207,7 +204,6 @@ const seedData = async () => {
|
|||
{
|
||||
title: "Still active",
|
||||
id: "still-active-poll",
|
||||
type: "date",
|
||||
userId: "user1",
|
||||
touchedAt: dayjs().add(-29, "days").toDate(),
|
||||
participantUrlId: "p4",
|
||||
|
@ -217,7 +213,6 @@ const seedData = async () => {
|
|||
{
|
||||
title: "Inactive poll",
|
||||
id: "inactive-poll",
|
||||
type: "date",
|
||||
userId: "user1",
|
||||
touchedAt: dayjs().add(-30, "days").toDate(),
|
||||
participantUrlId: "p5",
|
||||
|
@ -228,7 +223,6 @@ const seedData = async () => {
|
|||
demo: true,
|
||||
title: "Demo poll",
|
||||
id: "demo-poll-new",
|
||||
type: "date",
|
||||
userId: "user1",
|
||||
createdAt: new Date(),
|
||||
participantUrlId: "p6",
|
||||
|
@ -238,7 +232,6 @@ const seedData = async () => {
|
|||
demo: true,
|
||||
title: "Old demo poll",
|
||||
id: "demo-poll-old",
|
||||
type: "date",
|
||||
userId: "user1",
|
||||
createdAt: dayjs().add(-2, "days").toDate(),
|
||||
participantUrlId: "p7",
|
||||
|
@ -247,7 +240,6 @@ const seedData = async () => {
|
|||
{
|
||||
title: "Inactive poll with future option",
|
||||
id: "inactive-poll-future-option",
|
||||
type: "date",
|
||||
userId: "user1",
|
||||
touchedAt: dayjs().add(-30, "days").toDate(),
|
||||
participantUrlId: "p8",
|
||||
|
@ -260,32 +252,27 @@ const seedData = async () => {
|
|||
data: [
|
||||
{
|
||||
id: "option-1",
|
||||
value: "2022-02-22",
|
||||
start: new Date("2022-02-22T00:00:00"),
|
||||
pollId: "deleted-poll-7d",
|
||||
},
|
||||
{
|
||||
id: "option-2",
|
||||
value: "2022-02-23",
|
||||
start: new Date("2022-02-23T00:00:00"),
|
||||
pollId: "deleted-poll-7d",
|
||||
},
|
||||
{
|
||||
id: "option-3",
|
||||
value: "2022-02-24",
|
||||
start: new Date("2022-02-24T00:00:00"),
|
||||
pollId: "deleted-poll-7d",
|
||||
},
|
||||
{
|
||||
id: "option-4",
|
||||
value: `${dayjs()
|
||||
.add(10, "days")
|
||||
.format("YYYY-MM-DDTHH:mm:ss")}/${dayjs()
|
||||
.add(10, "days")
|
||||
.add(1, "hour")
|
||||
.format("YYYY-MM-DDTHH:mm:ss")}`,
|
||||
start: dayjs().add(10, "days").toDate(),
|
||||
pollId: "inactive-poll-future-option",
|
||||
},
|
||||
{
|
||||
id: "option-5",
|
||||
value: dayjs().add(-1, "days").format("YYYY-MM-DD"),
|
||||
start: dayjs().add(-1, "days").toDate(),
|
||||
pollId: "inactive-poll",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "options"
|
||||
ADD COLUMN "duration_minutes" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "start" TIMESTAMP(0);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "polls" DROP COLUMN "type";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "poll_type";
|
||||
|
||||
-- Reformat option value into new columns
|
||||
UPDATE "options"
|
||||
SET "start" = CASE
|
||||
WHEN POSITION('/' IN "value") = 0 THEN (value || 'T00:00:00')::TIMESTAMP WITHOUT TIME ZONE
|
||||
ELSE SPLIT_PART("value", '/', 1)::TIMESTAMP WITHOUT TIME ZONE
|
||||
END,
|
||||
"duration_minutes" = CASE
|
||||
WHEN POSITION('/' IN "value") = 0 THEN 0
|
||||
ELSE
|
||||
LEAST(EXTRACT(EPOCH FROM (split_part("value", '/', 2)::timestamp - split_part("value", '/', 1)::timestamp)) / 60, 1440)
|
||||
END;
|
||||
|
||||
-- Fix cases where we have a negative duration due to the end time being in the past
|
||||
-- eg. Some polls have value 2023-03-29T23:00:00/2023-03-29T01:00:00
|
||||
UPDATE "options"
|
||||
SET "duration_minutes" = "duration_minutes" + 1440
|
||||
WHERE "duration_minutes" < 0;
|
||||
|
||||
-- Set start date to be not null now that we have all the data and drop the old value column
|
||||
ALTER TABLE "options"
|
||||
DROP COLUMN "value",
|
||||
DROP COLUMN "updated_at",
|
||||
ALTER COLUMN "start" SET NOT NULL;
|
||||
|
|
@ -22,19 +22,12 @@ model User {
|
|||
@@map("users")
|
||||
}
|
||||
|
||||
enum PollType {
|
||||
date
|
||||
|
||||
@@map("poll_type")
|
||||
}
|
||||
|
||||
model Poll {
|
||||
id String @id @unique @map("id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deadline DateTime?
|
||||
title String
|
||||
type PollType
|
||||
description String?
|
||||
location String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
@ -88,11 +81,11 @@ model Participant {
|
|||
|
||||
model Option {
|
||||
id String @id @default(cuid())
|
||||
value String
|
||||
start DateTime @db.Timestamp(0)
|
||||
duration Int @default(0) @map("duration_minutes")
|
||||
pollId String @map("poll_id")
|
||||
poll Poll @relation(fields: [pollId], references: [id])
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||
|
||||
@@index([pollId], type: Hash)
|
||||
@@map("options")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
import { PrismaClient, VoteType } from "@prisma/client";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
@ -11,14 +11,16 @@ async function main() {
|
|||
// Create some users
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: faker.name.fullName(),
|
||||
email: faker.internet.email(),
|
||||
name: "Dev User",
|
||||
email: "dev@rallly.co",
|
||||
},
|
||||
});
|
||||
|
||||
// Create some polls
|
||||
const polls = await Promise.all(
|
||||
Array.from({ length: 20 }).map(async () => {
|
||||
Array.from({ length: 20 }).map(async (_, i) => {
|
||||
// create some polls with no duration (all day) and some with a random duration.
|
||||
const duration = i % 5 === 0 ? 15 * randInt(8) : 0;
|
||||
const poll = await prisma.poll.create({
|
||||
include: {
|
||||
participants: true,
|
||||
|
@ -30,7 +32,6 @@ async function main() {
|
|||
description: faker.lorem.paragraph(),
|
||||
location: faker.address.streetAddress(),
|
||||
deadline: faker.date.future(),
|
||||
type: "date",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
|
@ -46,7 +47,8 @@ async function main() {
|
|||
) //
|
||||
.map((date) => {
|
||||
return {
|
||||
value: date.toISOString().substring(0, 10),
|
||||
start: date,
|
||||
duration,
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
@ -73,7 +75,7 @@ async function main() {
|
|||
data: poll.options.map((option) => {
|
||||
const randomNumber = randInt(100);
|
||||
const vote =
|
||||
randomNumber > 90 ? "ifNeedBe" : randomNumber > 50 ? "yes" : "no";
|
||||
randomNumber > 95 ? "ifNeedBe" : randomNumber > 50 ? "yes" : "no";
|
||||
return {
|
||||
participantId: participant.id,
|
||||
pollId: poll.id,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue