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 { usePostHog } from "@/utils/posthog";
|
||||||
|
|
||||||
import { encodeDateOption } from "../utils/date-time-utils";
|
|
||||||
import { trpc } from "../utils/trpc";
|
import { trpc } from "../utils/trpc";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
import {
|
import {
|
||||||
|
@ -102,7 +101,6 @@ const Page: React.FunctionComponent = () => {
|
||||||
|
|
||||||
await createPoll.mutateAsync({
|
await createPoll.mutateAsync({
|
||||||
title: title,
|
title: title,
|
||||||
type: "date",
|
|
||||||
location: formData?.eventDetails?.location,
|
location: formData?.eventDetails?.location,
|
||||||
description: formData?.eventDetails?.description,
|
description: formData?.eventDetails?.description,
|
||||||
user: session.user.isGuest
|
user: session.user.isGuest
|
||||||
|
@ -112,7 +110,10 @@ const Page: React.FunctionComponent = () => {
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
timeZone: formData?.options?.timeZone,
|
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
|
<TimePicker
|
||||||
value={startDate}
|
value={startDate}
|
||||||
onChange={(newStart) => {
|
onChange={(newStart) => {
|
||||||
let newEnd = dayjs(newStart).add(
|
const newEnd = dayjs(newStart).add(
|
||||||
duration,
|
duration,
|
||||||
"minutes",
|
"minutes",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newEnd.isSame(newStart, "day")) {
|
|
||||||
newEnd = newEnd
|
|
||||||
.set("hour", 23)
|
|
||||||
.set("minute", 45);
|
|
||||||
}
|
|
||||||
// replace enter with updated start time
|
// replace enter with updated start time
|
||||||
onChange([
|
onChange([
|
||||||
...options.slice(0, index),
|
...options.slice(0, index),
|
||||||
|
@ -293,9 +288,7 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
||||||
/>
|
/>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
value={new Date(option.end)}
|
value={new Date(option.end)}
|
||||||
startFrom={dayjs(startDate)
|
after={startDate}
|
||||||
.add(15, "minutes")
|
|
||||||
.toDate()}
|
|
||||||
onChange={(newEnd) => {
|
onChange={(newEnd) => {
|
||||||
onChange([
|
onChange([
|
||||||
...options.slice(0, index),
|
...options.slice(0, index),
|
||||||
|
@ -330,7 +323,15 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
|
||||||
const lastOption = expectTimeOption(
|
const lastOption = expectTimeOption(
|
||||||
optionsForDay[optionsForDay.length - 1].option,
|
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([
|
onChange([
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Listbox } from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { getDuration } from "@/utils/date-time-utils";
|
||||||
import { stopPropagation } from "@/utils/stop-propagation";
|
import { stopPropagation } from "@/utils/stop-propagation";
|
||||||
|
|
||||||
import { useDayjs } from "../../../../utils/dayjs";
|
import { useDayjs } from "../../../../utils/dayjs";
|
||||||
|
@ -17,7 +18,7 @@ import { styleMenuItem } from "../../../menu-styles";
|
||||||
|
|
||||||
export interface TimePickerProps {
|
export interface TimePickerProps {
|
||||||
value: Date;
|
value: Date;
|
||||||
startFrom?: Date;
|
after?: Date;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (value: Date) => void;
|
onChange?: (value: Date) => void;
|
||||||
}
|
}
|
||||||
|
@ -26,10 +27,11 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
startFrom,
|
after,
|
||||||
}) => {
|
}) => {
|
||||||
const { dayjs } = useDayjs();
|
const { dayjs } = useDayjs();
|
||||||
const { reference, floating, x, y, strategy, refs } = useFloating({
|
const { reference, floating, x, y, strategy, refs } = useFloating({
|
||||||
|
placement: "bottom-start",
|
||||||
strategy: "fixed",
|
strategy: "fixed",
|
||||||
middleware: [
|
middleware: [
|
||||||
offset(5),
|
offset(5),
|
||||||
|
@ -38,7 +40,7 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
|
||||||
apply: ({ rects }) => {
|
apply: ({ rects }) => {
|
||||||
if (refs.floating.current) {
|
if (refs.floating.current) {
|
||||||
Object.assign(refs.floating.current.style, {
|
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 renderOptions = () => {
|
||||||
const startFromDate = startFrom
|
const startFromDate = after ? dayjs(after) : dayjs(value).startOf("day");
|
||||||
? dayjs(startFrom)
|
|
||||||
: dayjs(value).startOf("day");
|
|
||||||
|
|
||||||
const options: React.ReactNode[] = [];
|
const options: React.ReactNode[] = [];
|
||||||
const startMinute =
|
|
||||||
startFromDate.get("hour") * 60 + startFromDate.get("minute");
|
for (let i = 1; i <= 96; i++) {
|
||||||
const intervals = Math.floor((1440 - startMinute) / 15);
|
|
||||||
for (let i = 0; i < intervals; i++) {
|
|
||||||
const optionValue = startFromDate.add(i * 15, "minutes");
|
const optionValue = startFromDate.add(i * 15, "minutes");
|
||||||
options.push(
|
options.push(
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
|
@ -63,7 +61,14 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
|
||||||
className={styleMenuItem}
|
className={styleMenuItem}
|
||||||
value={optionValue.format("YYYY-MM-DDTHH:mm:ss")}
|
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>,
|
</Listbox.Option>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Placement } from "@floating-ui/react-dom-interactions";
|
import { Placement } from "@floating-ui/react-dom-interactions";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
@ -23,6 +24,15 @@ import { useUpdatePollMutation } from "./mutations";
|
||||||
|
|
||||||
const PollOptionsForm = React.lazy(() => import("../forms/poll-options-form"));
|
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<{
|
const ManagePoll: React.FunctionComponent<{
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
}> = ({ placement }) => {
|
}> = ({ placement }) => {
|
||||||
|
@ -67,18 +77,20 @@ const ManagePoll: React.FunctionComponent<{
|
||||||
name="pollOptions"
|
name="pollOptions"
|
||||||
title={poll.title}
|
title={poll.title}
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
navigationDate: poll.options[0].value.split("/")[0],
|
navigationDate: poll.options[0].start.toString(),
|
||||||
options: poll.options.map((option) => {
|
options: poll.options.map((option) => {
|
||||||
const [start, end] = option.value.split("/");
|
const start = dayjs(option.start);
|
||||||
return end
|
return option.duration > 0
|
||||||
? {
|
? {
|
||||||
type: "timeSlot",
|
type: "timeSlot",
|
||||||
start,
|
start: start.format("YYYY-MM-DDTHH:mm:ss"),
|
||||||
end,
|
end: start
|
||||||
|
.add(option.duration, "minute")
|
||||||
|
.format("YYYY-MM-DDTHH:mm:ss"),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: "date",
|
type: "date",
|
||||||
date: start,
|
date: start.format("YYYY-MM-DD"),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
timeZone: poll.timeZone ?? "",
|
timeZone: poll.timeZone ?? "",
|
||||||
|
@ -86,12 +98,14 @@ const ManagePoll: React.FunctionComponent<{
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
const encodedOptions = data.options.map(encodeDateOption);
|
const encodedOptions = data.options.map(encodeDateOption);
|
||||||
const optionsToDelete = poll.options.filter((option) => {
|
const optionsToDelete = poll.options.filter((option) => {
|
||||||
return !encodedOptions.includes(option.value);
|
return !encodedOptions.includes(convertOptionToString(option));
|
||||||
});
|
});
|
||||||
|
|
||||||
const optionsToAdd = encodedOptions.filter(
|
const optionsToAdd = encodedOptions.filter(
|
||||||
(encodedOption) =>
|
(encodedOption) =>
|
||||||
!poll.options.find((o) => o.value === encodedOption),
|
!poll.options.find(
|
||||||
|
(o) => convertOptionToString(o) === encodedOption,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { Prisma, prisma } from "@rallly/database";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { parseValue } from "../../utils/date-time-utils";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DANGER: This endpoint will permanently delete polls.
|
* 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
|
// get polls that have not been accessed for over 30 days
|
||||||
const inactivePolls = await prisma.$queryRaw<
|
const thirtyDaysAgo = dayjs().subtract(30, "days").toDate();
|
||||||
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 pollsToSoftDelete: string[] = [];
|
const oldPollsWithAllPastOptions = await prisma.poll.findMany({
|
||||||
|
select: {
|
||||||
// keep polls that have options that are in the future
|
id: true,
|
||||||
inactivePolls.forEach(({ id, max: value }) => {
|
},
|
||||||
const parsedValue = parseValue(value);
|
where: {
|
||||||
const date =
|
touchedAt: { lte: thirtyDaysAgo },
|
||||||
parsedValue.type === "date" ? parsedValue.date : parsedValue.end;
|
options: {
|
||||||
|
every: {
|
||||||
if (dayjs(date).isBefore(dayjs())) {
|
start: { lt: new Date() },
|
||||||
pollsToSoftDelete.push(id);
|
},
|
||||||
}
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const softDeletedPolls = await prisma.poll.deleteMany({
|
const softDeletedPolls = await prisma.poll.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
in: pollsToSoftDelete,
|
in: oldPollsWithAllPastOptions.map(({ id }) => id),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
import { sendEmail } from "@rallly/emails";
|
import { sendEmail } from "@rallly/emails";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { absoluteUrl } from "../../utils/absolute-url";
|
import { absoluteUrl } from "../../utils/absolute-url";
|
||||||
|
@ -10,46 +11,6 @@ import { comments } from "./polls/comments";
|
||||||
import { demo } from "./polls/demo";
|
import { demo } from "./polls/demo";
|
||||||
import { participants } from "./polls/participants";
|
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 getPollIdFromAdminUrlId = async (urlId: string) => {
|
||||||
const res = await prisma.poll.findUnique({
|
const res = await prisma.poll.findUnique({
|
||||||
select: {
|
select: {
|
||||||
|
@ -72,7 +33,6 @@ export const polls = router({
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
type: z.literal("date"),
|
|
||||||
timeZone: z.string().optional(),
|
timeZone: z.string().optional(),
|
||||||
location: z.string().optional(),
|
location: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
@ -82,7 +42,12 @@ export const polls = router({
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
options: z.string().array(),
|
options: z
|
||||||
|
.object({
|
||||||
|
startDate: z.string(),
|
||||||
|
endDate: z.string().optional(),
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
demo: z.boolean().optional(),
|
demo: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -120,7 +85,6 @@ export const polls = router({
|
||||||
data: {
|
data: {
|
||||||
id: await nanoid(),
|
id: await nanoid(),
|
||||||
title: input.title,
|
title: input.title,
|
||||||
type: input.type,
|
|
||||||
timeZone: input.timeZone,
|
timeZone: input.timeZone,
|
||||||
location: input.location,
|
location: input.location,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
|
@ -137,8 +101,14 @@ export const polls = router({
|
||||||
: undefined,
|
: undefined,
|
||||||
options: {
|
options: {
|
||||||
createMany: {
|
createMany: {
|
||||||
data: input.options.map((value) => ({
|
data: input.options.map((option) => ({
|
||||||
value,
|
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) {
|
if (input.optionsToAdd && input.optionsToAdd.length > 0) {
|
||||||
await prisma.option.createMany({
|
await prisma.option.createMany({
|
||||||
data: input.optionsToAdd.map((optionValue) => ({
|
data: input.optionsToAdd.map((optionValue) => {
|
||||||
value: optionValue,
|
const [start, end] = optionValue.split("/");
|
||||||
pollId,
|
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({
|
await prisma.poll.update({
|
||||||
select: defaultSelectFields,
|
select: { id: true },
|
||||||
where: {
|
where: {
|
||||||
id: pollId,
|
id: pollId,
|
||||||
},
|
},
|
||||||
|
@ -303,7 +284,24 @@ export const polls = router({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const res = await prisma.poll.findUnique({
|
const res = await prisma.poll.findUnique({
|
||||||
select: {
|
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: {
|
watchers: {
|
||||||
select: {
|
select: {
|
||||||
userId: true,
|
userId: true,
|
||||||
|
@ -333,7 +331,32 @@ export const polls = router({
|
||||||
)
|
)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const res = await prisma.poll.findUnique({
|
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: {
|
where: {
|
||||||
participantUrlId: input.urlId,
|
participantUrlId: input.urlId,
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,10 +30,10 @@ export const demo = router({
|
||||||
const adminUrlId = await nanoid();
|
const adminUrlId = await nanoid();
|
||||||
const demoUser = { name: "John Example", email: "noreply@rallly.co" };
|
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++) {
|
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<{
|
const participants: Array<{
|
||||||
|
@ -74,7 +74,6 @@ export const demo = router({
|
||||||
data: {
|
data: {
|
||||||
id: await nanoid(),
|
id: await nanoid(),
|
||||||
title: "Lunch Meeting",
|
title: "Lunch Meeting",
|
||||||
type: "date",
|
|
||||||
location: "Starbucks, 901 New York Avenue",
|
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!`,
|
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,
|
demo: true,
|
||||||
|
|
|
@ -65,7 +65,9 @@ export const decodeOptions = (
|
||||||
):
|
):
|
||||||
| { pollType: "date"; options: ParsedDateOption[] }
|
| { pollType: "date"; options: ParsedDateOption[] }
|
||||||
| { pollType: "timeSlot"; options: ParsedTimeSlotOption[] } => {
|
| { pollType: "timeSlot"; options: ParsedTimeSlotOption[] } => {
|
||||||
const pollType = isTimeSlot(options[0].value) ? "timeSlot" : "date";
|
const pollType = options.some(({ duration }) => duration > 0)
|
||||||
|
? "timeSlot"
|
||||||
|
: "date";
|
||||||
|
|
||||||
if (pollType === "timeSlot") {
|
if (pollType === "timeSlot") {
|
||||||
return {
|
return {
|
||||||
|
@ -83,12 +85,7 @@ export const decodeOptions = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseDateOption = (option: Option): ParsedDateOption => {
|
const parseDateOption = (option: Option): ParsedDateOption => {
|
||||||
const dateString =
|
const date = dayjs(option.start);
|
||||||
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);
|
|
||||||
return {
|
return {
|
||||||
type: "date",
|
type: "date",
|
||||||
optionId: option.id,
|
optionId: option.id,
|
||||||
|
@ -104,16 +101,12 @@ const parseTimeSlotOption = (
|
||||||
timeZone: string | null,
|
timeZone: string | null,
|
||||||
targetTimeZone: string,
|
targetTimeZone: string,
|
||||||
): ParsedTimeSlotOption => {
|
): ParsedTimeSlotOption => {
|
||||||
const [start, end] = option.value.split("/");
|
|
||||||
|
|
||||||
const startDate =
|
const startDate =
|
||||||
timeZone && targetTimeZone
|
timeZone && targetTimeZone
|
||||||
? dayjs(start).tz(timeZone, true).tz(targetTimeZone)
|
? dayjs(option.start).tz(timeZone, true).tz(targetTimeZone)
|
||||||
: dayjs(start);
|
: dayjs(option.start);
|
||||||
const endDate =
|
|
||||||
timeZone && targetTimeZone
|
const endDate = startDate.add(option.duration, "minute");
|
||||||
? dayjs(end).tz(timeZone, true).tz(targetTimeZone)
|
|
||||||
: dayjs(end);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "timeSlot",
|
type: "timeSlot",
|
||||||
|
|
|
@ -176,7 +176,6 @@ const seedData = async () => {
|
||||||
{
|
{
|
||||||
title: "Active Poll",
|
title: "Active Poll",
|
||||||
id: "active-poll",
|
id: "active-poll",
|
||||||
type: "date",
|
|
||||||
userId: "user1",
|
userId: "user1",
|
||||||
participantUrlId: "p1",
|
participantUrlId: "p1",
|
||||||
adminUrlId: "a1",
|
adminUrlId: "a1",
|
||||||
|
@ -185,7 +184,6 @@ const seedData = async () => {
|
||||||
{
|
{
|
||||||
title: "Deleted poll",
|
title: "Deleted poll",
|
||||||
id: "deleted-poll-6d",
|
id: "deleted-poll-6d",
|
||||||
type: "date",
|
|
||||||
userId: "user1",
|
userId: "user1",
|
||||||
deleted: true,
|
deleted: true,
|
||||||
deletedAt: dayjs().add(-6, "days").toDate(),
|
deletedAt: dayjs().add(-6, "days").toDate(),
|
||||||
|
@ -196,7 +194,6 @@ const seedData = async () => {
|
||||||
{
|
{
|
||||||
title: "Deleted poll 7d",
|
title: "Deleted poll 7d",
|
||||||
id: "deleted-poll-7d",
|
id: "deleted-poll-7d",
|
||||||
type: "date",
|
|
||||||
userId: "user1",
|
userId: "user1",
|
||||||
deleted: true,
|
deleted: true,
|
||||||
deletedAt: dayjs().add(-7, "days").toDate(),
|
deletedAt: dayjs().add(-7, "days").toDate(),
|
||||||
|
@ -207,7 +204,6 @@ const seedData = async () => {
|
||||||
{
|
{
|
||||||
title: "Still active",
|
title: "Still active",
|
||||||
id: "still-active-poll",
|
id: "still-active-poll",
|
||||||
type: "date",
|
|
||||||
userId: "user1",
|
userId: "user1",
|
||||||
touchedAt: dayjs().add(-29, "days").toDate(),
|
touchedAt: dayjs().add(-29, "days").toDate(),
|
||||||
participantUrlId: "p4",
|
participantUrlId: "p4",
|
||||||
|
@ -217,7 +213,6 @@ const seedData = async () => {
|
||||||
{
|
{
|
||||||
title: "Inactive poll",
|
title: "Inactive poll",
|
||||||
id: "inactive-poll",
|
id: "inactive-poll",
|
||||||
type: "date",
|
|
||||||
userId: "user1",
|
userId: "user1",
|
||||||
touchedAt: dayjs().add(-30, "days").toDate(),
|
touchedAt: dayjs().add(-30, "days").toDate(),
|
||||||
participantUrlId: "p5",
|
participantUrlId: "p5",
|
||||||
|
@ -228,7 +223,6 @@ const seedData = async () => {
|
||||||
demo: true,
|
demo: true,
|
||||||
title: "Demo poll",
|
title: "Demo poll",
|
||||||
id: "demo-poll-new",
|
id: "demo-poll-new",
|
||||||
type: "date",
|
|
||||||
userId: "user1",
|
userId: "user1",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
participantUrlId: "p6",
|
participantUrlId: "p6",
|
||||||
|
@ -238,7 +232,6 @@ const seedData = async () => {
|
||||||
demo: true,
|
demo: true,
|
||||||
title: "Old demo poll",
|
title: "Old demo poll",
|
||||||
id: "demo-poll-old",
|
id: "demo-poll-old",
|
||||||
type: "date",
|
|
||||||
userId: "user1",
|
userId: "user1",
|
||||||
createdAt: dayjs().add(-2, "days").toDate(),
|
createdAt: dayjs().add(-2, "days").toDate(),
|
||||||
participantUrlId: "p7",
|
participantUrlId: "p7",
|
||||||
|
@ -247,7 +240,6 @@ const seedData = async () => {
|
||||||
{
|
{
|
||||||
title: "Inactive poll with future option",
|
title: "Inactive poll with future option",
|
||||||
id: "inactive-poll-future-option",
|
id: "inactive-poll-future-option",
|
||||||
type: "date",
|
|
||||||
userId: "user1",
|
userId: "user1",
|
||||||
touchedAt: dayjs().add(-30, "days").toDate(),
|
touchedAt: dayjs().add(-30, "days").toDate(),
|
||||||
participantUrlId: "p8",
|
participantUrlId: "p8",
|
||||||
|
@ -260,32 +252,27 @@ const seedData = async () => {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: "option-1",
|
id: "option-1",
|
||||||
value: "2022-02-22",
|
start: new Date("2022-02-22T00:00:00"),
|
||||||
pollId: "deleted-poll-7d",
|
pollId: "deleted-poll-7d",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "option-2",
|
id: "option-2",
|
||||||
value: "2022-02-23",
|
start: new Date("2022-02-23T00:00:00"),
|
||||||
pollId: "deleted-poll-7d",
|
pollId: "deleted-poll-7d",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "option-3",
|
id: "option-3",
|
||||||
value: "2022-02-24",
|
start: new Date("2022-02-24T00:00:00"),
|
||||||
pollId: "deleted-poll-7d",
|
pollId: "deleted-poll-7d",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "option-4",
|
id: "option-4",
|
||||||
value: `${dayjs()
|
start: dayjs().add(10, "days").toDate(),
|
||||||
.add(10, "days")
|
|
||||||
.format("YYYY-MM-DDTHH:mm:ss")}/${dayjs()
|
|
||||||
.add(10, "days")
|
|
||||||
.add(1, "hour")
|
|
||||||
.format("YYYY-MM-DDTHH:mm:ss")}`,
|
|
||||||
pollId: "inactive-poll-future-option",
|
pollId: "inactive-poll-future-option",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "option-5",
|
id: "option-5",
|
||||||
value: dayjs().add(-1, "days").format("YYYY-MM-DD"),
|
start: dayjs().add(-1, "days").toDate(),
|
||||||
pollId: "inactive-poll",
|
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")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PollType {
|
|
||||||
date
|
|
||||||
|
|
||||||
@@map("poll_type")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Poll {
|
model Poll {
|
||||||
id String @id @unique @map("id")
|
id String @id @unique @map("id")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
deadline DateTime?
|
deadline DateTime?
|
||||||
title String
|
title String
|
||||||
type PollType
|
|
||||||
description String?
|
description String?
|
||||||
location String?
|
location String?
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
@ -88,11 +81,11 @@ model Participant {
|
||||||
|
|
||||||
model Option {
|
model Option {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
value String
|
start DateTime @db.Timestamp(0)
|
||||||
|
duration Int @default(0) @map("duration_minutes")
|
||||||
pollId String @map("poll_id")
|
pollId String @map("poll_id")
|
||||||
poll Poll @relation(fields: [pollId], references: [id])
|
poll Poll @relation(fields: [pollId], references: [id])
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
|
||||||
|
|
||||||
@@index([pollId], type: Hash)
|
@@index([pollId], type: Hash)
|
||||||
@@map("options")
|
@@map("options")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import { PrismaClient, VoteType } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
@ -11,14 +11,16 @@ async function main() {
|
||||||
// Create some users
|
// Create some users
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
name: faker.name.fullName(),
|
name: "Dev User",
|
||||||
email: faker.internet.email(),
|
email: "dev@rallly.co",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create some polls
|
// Create some polls
|
||||||
const polls = await Promise.all(
|
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({
|
const poll = await prisma.poll.create({
|
||||||
include: {
|
include: {
|
||||||
participants: true,
|
participants: true,
|
||||||
|
@ -30,7 +32,6 @@ async function main() {
|
||||||
description: faker.lorem.paragraph(),
|
description: faker.lorem.paragraph(),
|
||||||
location: faker.address.streetAddress(),
|
location: faker.address.streetAddress(),
|
||||||
deadline: faker.date.future(),
|
deadline: faker.date.future(),
|
||||||
type: "date",
|
|
||||||
user: {
|
user: {
|
||||||
connect: {
|
connect: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
@ -46,7 +47,8 @@ async function main() {
|
||||||
) //
|
) //
|
||||||
.map((date) => {
|
.map((date) => {
|
||||||
return {
|
return {
|
||||||
value: date.toISOString().substring(0, 10),
|
start: date,
|
||||||
|
duration,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -73,7 +75,7 @@ async function main() {
|
||||||
data: poll.options.map((option) => {
|
data: poll.options.map((option) => {
|
||||||
const randomNumber = randInt(100);
|
const randomNumber = randInt(100);
|
||||||
const vote =
|
const vote =
|
||||||
randomNumber > 90 ? "ifNeedBe" : randomNumber > 50 ? "yes" : "no";
|
randomNumber > 95 ? "ifNeedBe" : randomNumber > 50 ? "yes" : "no";
|
||||||
return {
|
return {
|
||||||
participantId: participant.id,
|
participantId: participant.id,
|
||||||
pollId: poll.id,
|
pollId: poll.id,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue