Switch to tRPC (#173)
21
README.md
|
@ -74,16 +74,17 @@ yarn start
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Parameter | Default | Description |
|
||||||
| --------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| DATABASE_URL | postgres://postgres:postgres@rallly_db:5432/db | A postgres database URL. Leave out if using the docker-compose file since it will spin up and connect to its own database instance. |
|
| NEXT_PUBLIC_BASE_URL | http://localhost:3000 | The hosting url of the server, used for creating links and making api calls from the client. |
|
||||||
| SECRET_PASSWORD | - | A long string (minimum 32 characters) that is used to encrypt session data. |
|
| DATABASE_URL | postgres://postgres:postgres@rallly_db:5432/db | A postgres database URL. Leave out if using the docker-compose file since it will spin up and connect to its own database instance. |
|
||||||
| SUPPORT_EMAIL | - | An email address that will appear as the FROM email for all emails being sent out. |
|
| SECRET_PASSWORD | - | A long string (minimum 32 characters) that is used to encrypt session data. |
|
||||||
| SMTP_HOST | - | Host name of your SMTP server |
|
| SUPPORT_EMAIL | - | An email address that will appear as the FROM email for all emails being sent out. |
|
||||||
| SMTP_PORT | - | Port of your SMTP server |
|
| SMTP_HOST | - | Host name of your SMTP server |
|
||||||
| SMTP_SECURE | false | Set to "true" if SSL is enabled for your SMTP connection |
|
| SMTP_PORT | - | Port of your SMTP server |
|
||||||
| SMTP_USER | - | Username to use for your SMTP connection |
|
| SMTP_SECURE | false | Set to "true" if SSL is enabled for your SMTP connection |
|
||||||
| SMTP_PWD | - | Password to use for your SMTP connection |
|
| SMTP_USER | - | Username to use for your SMTP connection |
|
||||||
|
| SMTP_PWD | - | Password to use for your SMTP connection |
|
||||||
|
|
||||||
## 👨💻 Contributors
|
## 👨💻 Contributors
|
||||||
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { Participant, Vote, VoteType } from "@prisma/client";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export interface AddParticipantPayload {
|
|
||||||
pollId: string;
|
|
||||||
name: string;
|
|
||||||
votes: Array<{ optionId: string; type: VoteType }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddParticipantResponse = Participant & {
|
|
||||||
votes: Vote[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addParticipant = async (
|
|
||||||
payload: AddParticipantPayload,
|
|
||||||
): Promise<AddParticipantResponse> => {
|
|
||||||
const res = await axios.post<AddParticipantResponse>(
|
|
||||||
`/api/poll/${payload.pollId}/participant`,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
};
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Comment } from "@prisma/client";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export interface CreateCommentPayload {
|
|
||||||
pollId: string;
|
|
||||||
content: string;
|
|
||||||
authorName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createComment = async (
|
|
||||||
payload: CreateCommentPayload,
|
|
||||||
): Promise<Comment> => {
|
|
||||||
const { data } = await axios.post<Comment>(
|
|
||||||
`/api/poll/${payload.pollId}/comments`,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
};
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { Poll } from "@prisma/client";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export const createDemo = async (): Promise<Poll> => {
|
|
||||||
const { data } = await axios.post<Poll>("/api/poll/demo");
|
|
||||||
return data;
|
|
||||||
};
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { Poll } from "@prisma/client";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export interface CreatePollPayload {
|
|
||||||
title: string;
|
|
||||||
type: "date";
|
|
||||||
timeZone?: string;
|
|
||||||
location?: string;
|
|
||||||
description?: string;
|
|
||||||
user: {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
options: string[];
|
|
||||||
demo?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createPoll = async (payload: CreatePollPayload): Promise<Poll> => {
|
|
||||||
const { data } = await axios.post<Poll>("/api/poll", payload);
|
|
||||||
return data;
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export interface DeleteParticipantPayload {
|
|
||||||
pollId: string;
|
|
||||||
participantId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteParticipant = async (
|
|
||||||
payload: DeleteParticipantPayload,
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { pollId, participantId } = payload;
|
|
||||||
await axios.delete(`/api/poll/${pollId}/participant/${participantId}`);
|
|
||||||
} catch (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Link, Option, Participant, Poll, User, Vote } from "@prisma/client";
|
|
||||||
|
|
||||||
export interface GetPollApiResponse extends Omit<Poll, "verificationCode"> {
|
|
||||||
options: Option[];
|
|
||||||
participants: Array<Participant & { votes: Vote[] }>;
|
|
||||||
user: User;
|
|
||||||
role: "admin" | "participant";
|
|
||||||
links: Array<Link>;
|
|
||||||
pollId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetPollResponse extends Omit<GetPollApiResponse, "createdAt"> {
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { Participant, Vote, VoteType } from "@prisma/client";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export interface UpdateParticipantPayload {
|
|
||||||
pollId: string;
|
|
||||||
participantId: string;
|
|
||||||
name: string;
|
|
||||||
votes: Array<{ optionId: string; type: VoteType }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateParticipant = async (
|
|
||||||
payload: UpdateParticipantPayload,
|
|
||||||
): Promise<Participant & { votes: Vote[] }> => {
|
|
||||||
const { pollId, participantId, ...body } = payload;
|
|
||||||
const res = await axios.patch<Participant & { votes: Vote[] }>(
|
|
||||||
`/api/poll/${pollId}/participant/${participantId}`,
|
|
||||||
body,
|
|
||||||
);
|
|
||||||
return res.data;
|
|
||||||
};
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { Poll } from "@prisma/client";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export interface UpdatePollPayload {
|
|
||||||
title?: string;
|
|
||||||
timeZone?: string;
|
|
||||||
location?: string;
|
|
||||||
description?: string;
|
|
||||||
optionsToDelete?: string[];
|
|
||||||
optionsToAdd?: string[];
|
|
||||||
notifications?: boolean;
|
|
||||||
closed?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updatePoll = async (
|
|
||||||
urlId: string,
|
|
||||||
payload: UpdatePollPayload,
|
|
||||||
): Promise<Poll> => {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.patch<Poll>(`/api/poll/${urlId}`, payload);
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,145 +0,0 @@
|
||||||
import { updatePoll, UpdatePollPayload } from "api-client/update-poll";
|
|
||||||
import { usePlausible } from "next-plausible";
|
|
||||||
import { useMutation, useQueryClient } from "react-query";
|
|
||||||
|
|
||||||
import { addParticipant } from "../../api-client/add-participant";
|
|
||||||
import {
|
|
||||||
deleteParticipant,
|
|
||||||
DeleteParticipantPayload,
|
|
||||||
} from "../../api-client/delete-participant";
|
|
||||||
import { GetPollResponse } from "../../api-client/get-poll";
|
|
||||||
import { updateParticipant } from "../../api-client/update-participant";
|
|
||||||
import { usePoll } from "../poll-context";
|
|
||||||
import { useSession } from "../session";
|
|
||||||
import { ParticipantForm } from "./types";
|
|
||||||
|
|
||||||
export const useAddParticipantMutation = (pollId: string) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const session = useSession();
|
|
||||||
const plausible = usePlausible();
|
|
||||||
const { options } = usePoll();
|
|
||||||
|
|
||||||
return useMutation(
|
|
||||||
(payload: ParticipantForm) =>
|
|
||||||
addParticipant({
|
|
||||||
pollId,
|
|
||||||
name: payload.name.trim(),
|
|
||||||
votes: options.map(
|
|
||||||
(option, i) =>
|
|
||||||
payload.votes[i] ?? { optionId: option.optionId, type: "no" },
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
onSuccess: (participant) => {
|
|
||||||
plausible("Add participant");
|
|
||||||
queryClient.setQueryData<GetPollResponse>(
|
|
||||||
["getPoll", pollId],
|
|
||||||
(poll) => {
|
|
||||||
if (!poll) {
|
|
||||||
throw new Error(
|
|
||||||
"Tried to update poll but no result found in query cache",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
poll.participants = [participant, ...poll.participants];
|
|
||||||
return poll;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
session.refresh();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateParticipantMutation = (pollId: string) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const plausible = usePlausible();
|
|
||||||
const { options } = usePoll();
|
|
||||||
|
|
||||||
return useMutation(
|
|
||||||
(payload: ParticipantForm & { participantId: string }) =>
|
|
||||||
updateParticipant({
|
|
||||||
pollId,
|
|
||||||
participantId: payload.participantId,
|
|
||||||
name: payload.name.trim(),
|
|
||||||
votes: options.map(
|
|
||||||
(option, i) =>
|
|
||||||
payload.votes[i] ?? { optionId: option.optionId, type: "no" },
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
onSuccess: (participant) => {
|
|
||||||
plausible("Update participant");
|
|
||||||
queryClient.setQueryData<GetPollResponse>(
|
|
||||||
["getPoll", pollId],
|
|
||||||
(poll) => {
|
|
||||||
if (!poll) {
|
|
||||||
throw new Error(
|
|
||||||
"Tried to update poll but no result found in query cache",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
poll.participants = poll.participants.map((p) =>
|
|
||||||
p.id === participant.id ? participant : p,
|
|
||||||
);
|
|
||||||
|
|
||||||
return poll;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
queryClient.invalidateQueries(["getPoll", pollId]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDeleteParticipantMutation = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const plausible = usePlausible();
|
|
||||||
const { poll } = usePoll();
|
|
||||||
return useMutation(
|
|
||||||
(payload: DeleteParticipantPayload) => deleteParticipant(payload),
|
|
||||||
{
|
|
||||||
onMutate: ({ participantId }) => {
|
|
||||||
queryClient.setQueryData<GetPollResponse>(
|
|
||||||
["getPoll", poll.urlId],
|
|
||||||
(poll) => {
|
|
||||||
if (!poll) {
|
|
||||||
throw new Error(
|
|
||||||
"Tried to update poll but no result found in query cache",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
poll.participants = poll.participants.filter(
|
|
||||||
({ id }) => id !== participantId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return poll;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
plausible("Remove participant");
|
|
||||||
},
|
|
||||||
onSettled: (_data, _error, { pollId }) => {
|
|
||||||
queryClient.invalidateQueries(["getPoll", pollId]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdatePollMutation = () => {
|
|
||||||
const { poll } = usePoll();
|
|
||||||
const plausible = usePlausible();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation(
|
|
||||||
(payload: UpdatePollPayload) => updatePoll(poll.urlId, payload),
|
|
||||||
{
|
|
||||||
onSuccess: (data) => {
|
|
||||||
queryClient.setQueryData(["getPoll", poll.urlId], data);
|
|
||||||
plausible("Updated poll");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,63 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import * as React from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { validEmail } from "utils/form-validation";
|
|
||||||
|
|
||||||
import Button from "@/components/button";
|
|
||||||
|
|
||||||
const GuestSession: React.VoidFunctionComponent = () => {
|
|
||||||
const { handleSubmit, register, formState, getValues } = useForm<{
|
|
||||||
email: string;
|
|
||||||
}>({
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className="card border-amber-500 ring-2 ring-amber-500/20">
|
|
||||||
<h2>Guest session</h2>
|
|
||||||
<p>
|
|
||||||
Guest sessions allow us to remember your device so that you can edit
|
|
||||||
your votes and comments later. However, these sessions are temporary and
|
|
||||||
when they end, cannot be resumed.{" "}
|
|
||||||
<a href="">Read more about guest sessions.</a>
|
|
||||||
</p>
|
|
||||||
<p>Login with your email to make sure you don't lose access:</p>
|
|
||||||
{formState.submitCount > 0 ? (
|
|
||||||
<div>
|
|
||||||
An email has been sent to <strong>{getValues("email")}</strong>.
|
|
||||||
Please check your inbox.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(({ email }) => {
|
|
||||||
axios.post("/api/login", { email });
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
{...register("email", {
|
|
||||||
validate: validEmail,
|
|
||||||
})}
|
|
||||||
className={clsx("input w-full", {
|
|
||||||
"input-error": formState.errors.email,
|
|
||||||
})}
|
|
||||||
placeholder="Email address"
|
|
||||||
/>
|
|
||||||
{formState.errors.email ? (
|
|
||||||
<div className="mt-1 text-sm text-red-500">
|
|
||||||
Please enter a valid email address
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="mt-4 flex space-x-3">
|
|
||||||
<Button htmlType="submit" type="primary">
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GuestSession;
|
|
1
declarations/environment.d.ts
vendored
|
@ -4,6 +4,7 @@ declare global {
|
||||||
DATABASE_URL: string;
|
DATABASE_URL: string;
|
||||||
NODE_ENV: "development" | "production";
|
NODE_ENV: "development" | "production";
|
||||||
SECRET_PASSWORD: string;
|
SECRET_PASSWORD: string;
|
||||||
|
NEXT_PUBLIC_LEGACY_POLLS?: string;
|
||||||
NEXT_PUBLIC_MAINTENANCE_MODE?: string;
|
NEXT_PUBLIC_MAINTENANCE_MODE?: string;
|
||||||
PLAUSIBLE_DOMAIN?: string;
|
PLAUSIBLE_DOMAIN?: string;
|
||||||
NEXT_PUBLIC_CRISP_WEBSITE_ID?: string;
|
NEXT_PUBLIC_CRISP_WEBSITE_ID?: string;
|
||||||
|
|
6
declarations/i18next.d.ts
vendored
|
@ -1,8 +1,8 @@
|
||||||
import "react-i18next";
|
import "react-i18next";
|
||||||
|
|
||||||
import app from "../public/locales/en/app.json";
|
import app from "~/public/locales/en/app.json";
|
||||||
import homepage from "../public/locales/en/homepage.json";
|
import homepage from "~/public/locales/en/homepage.json";
|
||||||
import support from "../public/locales/en/support.json";
|
import support from "~/public/locales/en/support.json";
|
||||||
|
|
||||||
declare module "next-i18next" {
|
declare module "next-i18next" {
|
||||||
interface Resources {
|
interface Resources {
|
||||||
|
|
12
package.json
|
@ -16,11 +16,15 @@
|
||||||
"@floating-ui/react-dom-interactions": "^0.4.0",
|
"@floating-ui/react-dom-interactions": "^0.4.0",
|
||||||
"@headlessui/react": "^1.5.0",
|
"@headlessui/react": "^1.5.0",
|
||||||
"@next/bundle-analyzer": "^12.1.0",
|
"@next/bundle-analyzer": "^12.1.0",
|
||||||
"@prisma/client": "^3.13.0",
|
"@prisma/client": "^3.14.0",
|
||||||
"@sentry/nextjs": "^6.19.3",
|
"@sentry/nextjs": "^6.19.3",
|
||||||
"@svgr/webpack": "^6.2.1",
|
"@svgr/webpack": "^6.2.1",
|
||||||
"@tailwindcss/forms": "^0.4.0",
|
"@tailwindcss/forms": "^0.4.0",
|
||||||
"@tailwindcss/typography": "^0.5.2",
|
"@tailwindcss/typography": "^0.5.2",
|
||||||
|
"@trpc/client": "^9.23.2",
|
||||||
|
"@trpc/next": "^9.23.2",
|
||||||
|
"@trpc/react": "^9.23.2",
|
||||||
|
"@trpc/server": "^9.23.2",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
|
@ -37,7 +41,7 @@
|
||||||
"next-i18next": "^10.5.0",
|
"next-i18next": "^10.5.0",
|
||||||
"next-plausible": "^3.1.9",
|
"next-plausible": "^3.1.9",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
"prisma": "^3.13.0",
|
"prisma": "^3.14.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-big-calendar": "^0.38.9",
|
"react-big-calendar": "^0.38.9",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
@ -50,8 +54,10 @@
|
||||||
"react-use": "^17.3.2",
|
"react-use": "^17.3.2",
|
||||||
"smoothscroll-polyfill": "^0.4.4",
|
"smoothscroll-polyfill": "^0.4.4",
|
||||||
"spacetime": "^7.1.2",
|
"spacetime": "^7.1.2",
|
||||||
|
"superjson": "^1.9.1",
|
||||||
"timezone-soft": "^1.3.1",
|
"timezone-soft": "^1.3.1",
|
||||||
"typescript": "^4.5.2"
|
"typescript": "^4.5.2",
|
||||||
|
"zod": "^3.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.20.1",
|
"@playwright/test": "^1.20.1",
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
import "react-big-calendar/lib/css/react-big-calendar.css";
|
|
||||||
import "tailwindcss/tailwind.css";
|
|
||||||
import "../style.css";
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
import { NextPage } from "next";
|
|
||||||
import { AppProps } from "next/app";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import Head from "next/head";
|
|
||||||
import { appWithTranslation } from "next-i18next";
|
|
||||||
import PlausibleProvider from "next-plausible";
|
|
||||||
import toast, { Toaster } from "react-hot-toast";
|
|
||||||
import { MutationCache, QueryClient, QueryClientProvider } from "react-query";
|
|
||||||
|
|
||||||
import Maintenance from "@/components/maintenance";
|
|
||||||
import ModalProvider from "@/components/modal/modal-provider";
|
|
||||||
import PreferencesProvider from "@/components/preferences/preferences-provider";
|
|
||||||
|
|
||||||
const CrispChat = dynamic(() => import("@/components/crisp-chat"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
|
||||||
mutationCache: new MutationCache({
|
|
||||||
onError: (error) => {
|
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 500) {
|
|
||||||
toast.error(
|
|
||||||
"Uh oh! Something went wrong. The issue has been logged and we'll fix it as soon as possible. Please try again later.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const MyApp: NextPage<AppProps> = ({ Component, pageProps }) => {
|
|
||||||
if (process.env.NEXT_PUBLIC_MAINTENANCE_MODE === "1") {
|
|
||||||
return <Maintenance />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<PlausibleProvider
|
|
||||||
domain="rallly.co"
|
|
||||||
customDomain={process.env.PLAUSIBLE_DOMAIN}
|
|
||||||
trackOutboundLinks={true}
|
|
||||||
selfHosted={true}
|
|
||||||
enabled={!!process.env.PLAUSIBLE_DOMAIN}
|
|
||||||
>
|
|
||||||
<PreferencesProvider>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<Head>
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1"
|
|
||||||
/>
|
|
||||||
</Head>
|
|
||||||
<CrispChat />
|
|
||||||
<Toaster />
|
|
||||||
<ModalProvider>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</ModalProvider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
</PreferencesProvider>
|
|
||||||
</PlausibleProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default appWithTranslation(MyApp);
|
|
|
@ -1,35 +0,0 @@
|
||||||
import absoluteUrl from "utils/absolute-url";
|
|
||||||
import { sendEmailTemplate } from "utils/api-utils";
|
|
||||||
import { createToken, withSessionRoute } from "utils/auth";
|
|
||||||
|
|
||||||
export default withSessionRoute(async (req, res) => {
|
|
||||||
switch (req.method) {
|
|
||||||
case "POST": {
|
|
||||||
const email = req.body.email;
|
|
||||||
const homePageUrl = absoluteUrl(req).origin;
|
|
||||||
const token = await createToken({
|
|
||||||
email,
|
|
||||||
guestId: req.session.user?.isGuest ? req.session.user.id : undefined,
|
|
||||||
path: req.body.path,
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginUrl = `${homePageUrl}/login?code=${token}`;
|
|
||||||
|
|
||||||
await sendEmailTemplate({
|
|
||||||
templateName: "login",
|
|
||||||
to: email,
|
|
||||||
subject: "Rallly - Login",
|
|
||||||
templateVars: {
|
|
||||||
loginUrl,
|
|
||||||
homePageUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
res.status(405);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { withSessionRoute } from "utils/auth";
|
|
||||||
|
|
||||||
export default withSessionRoute((req, res) => {
|
|
||||||
req.session.destroy();
|
|
||||||
res.send({ ok: true });
|
|
||||||
});
|
|
|
@ -1,108 +0,0 @@
|
||||||
import { sendEmailTemplate } from "utils/api-utils";
|
|
||||||
import { createToken, withSessionRoute } from "utils/auth";
|
|
||||||
import { nanoid } from "utils/nanoid";
|
|
||||||
|
|
||||||
import { CreatePollPayload } from "../../api-client/create-poll";
|
|
||||||
import { prisma } from "../../db";
|
|
||||||
import absoluteUrl from "../../utils/absolute-url";
|
|
||||||
|
|
||||||
export default withSessionRoute(async (req, res) => {
|
|
||||||
switch (req.method) {
|
|
||||||
case "POST": {
|
|
||||||
const adminUrlId = await nanoid();
|
|
||||||
const payload: CreatePollPayload = req.body;
|
|
||||||
|
|
||||||
const poll = await prisma.poll.create({
|
|
||||||
data: {
|
|
||||||
urlId: await nanoid(),
|
|
||||||
title: payload.title,
|
|
||||||
type: payload.type,
|
|
||||||
timeZone: payload.timeZone,
|
|
||||||
location: payload.location,
|
|
||||||
description: payload.description,
|
|
||||||
authorName: payload.user.name,
|
|
||||||
demo: payload.demo,
|
|
||||||
verified:
|
|
||||||
req.session.user?.isGuest === false &&
|
|
||||||
req.session.user.email === payload.user.email,
|
|
||||||
user: {
|
|
||||||
connectOrCreate: {
|
|
||||||
where: {
|
|
||||||
email: payload.user.email,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: await nanoid(),
|
|
||||||
...payload.user,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
createMany: {
|
|
||||||
data: payload.options.map((value) => ({
|
|
||||||
value,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
links: {
|
|
||||||
createMany: {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
urlId: adminUrlId,
|
|
||||||
role: "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urlId: await nanoid(),
|
|
||||||
role: "participant",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const homePageUrl = absoluteUrl(req).origin;
|
|
||||||
const pollUrl = `${homePageUrl}/admin/${adminUrlId}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (poll.verified) {
|
|
||||||
await sendEmailTemplate({
|
|
||||||
templateName: "new-poll-verified",
|
|
||||||
to: payload.user.email,
|
|
||||||
subject: `Rallly: ${poll.title}`,
|
|
||||||
templateVars: {
|
|
||||||
title: poll.title,
|
|
||||||
name: payload.user.name,
|
|
||||||
pollUrl,
|
|
||||||
homePageUrl,
|
|
||||||
supportEmail: process.env.SUPPORT_EMAIL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const verificationCode = await createToken({
|
|
||||||
pollId: poll.urlId,
|
|
||||||
});
|
|
||||||
const verifyEmailUrl = `${pollUrl}?code=${verificationCode}`;
|
|
||||||
|
|
||||||
await sendEmailTemplate({
|
|
||||||
templateName: "new-poll",
|
|
||||||
to: payload.user.email,
|
|
||||||
subject: `Rallly: ${poll.title} - Verify your email address`,
|
|
||||||
templateVars: {
|
|
||||||
title: poll.title,
|
|
||||||
name: payload.user.name,
|
|
||||||
pollUrl,
|
|
||||||
verifyEmailUrl,
|
|
||||||
homePageUrl,
|
|
||||||
supportEmail: process.env.SUPPORT_EMAIL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({ urlId: adminUrlId, authorName: poll.authorName });
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,148 +0,0 @@
|
||||||
import { GetPollApiResponse } from "api-client/get-poll";
|
|
||||||
import { resetDates } from "utils/legacy-utils";
|
|
||||||
|
|
||||||
import { UpdatePollPayload } from "../../../api-client/update-poll";
|
|
||||||
import { prisma } from "../../../db";
|
|
||||||
import { withLink } from "../../../utils/api-utils";
|
|
||||||
|
|
||||||
export default withLink<
|
|
||||||
GetPollApiResponse | { status: number; message: string }
|
|
||||||
>(async ({ req, res, link }) => {
|
|
||||||
const pollId = link.pollId;
|
|
||||||
|
|
||||||
switch (req.method) {
|
|
||||||
case "GET": {
|
|
||||||
const poll = await prisma.poll.findUnique({
|
|
||||||
where: {
|
|
||||||
urlId: pollId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
options: {
|
|
||||||
orderBy: {
|
|
||||||
value: "asc",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
participants: {
|
|
||||||
include: {
|
|
||||||
votes: true,
|
|
||||||
},
|
|
||||||
orderBy: [
|
|
||||||
{
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
{ name: "desc" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
user: true,
|
|
||||||
links: link.role === "admin",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!poll) {
|
|
||||||
return res.status(404).json({ status: 404, message: "Poll not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
poll.legacy &&
|
|
||||||
// has converted options without timezone
|
|
||||||
poll.options.every(({ value }) => value.indexOf("T") === -1)
|
|
||||||
) {
|
|
||||||
// We need to reset the dates for polls that lost their timezone data because some users
|
|
||||||
// of the old version will end up seeing the wrong dates
|
|
||||||
const fixedPoll = await resetDates(poll.urlId);
|
|
||||||
|
|
||||||
if (fixedPoll) {
|
|
||||||
return res.json({
|
|
||||||
...fixedPoll,
|
|
||||||
role: link.role,
|
|
||||||
urlId: link.urlId,
|
|
||||||
pollId: poll.urlId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
...poll,
|
|
||||||
role: link.role,
|
|
||||||
urlId: link.urlId,
|
|
||||||
pollId: poll.urlId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
case "PATCH": {
|
|
||||||
if (link.role !== "admin") {
|
|
||||||
return res
|
|
||||||
.status(401)
|
|
||||||
.json({ status: 401, message: "Permission denied" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: Partial<UpdatePollPayload> = req.body;
|
|
||||||
if (payload.optionsToDelete && payload.optionsToDelete.length > 0) {
|
|
||||||
await prisma.option.deleteMany({
|
|
||||||
where: {
|
|
||||||
pollId,
|
|
||||||
id: {
|
|
||||||
in: payload.optionsToDelete,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (payload.optionsToAdd && payload.optionsToAdd.length > 0) {
|
|
||||||
await prisma.option.createMany({
|
|
||||||
data: payload.optionsToAdd.map((optionValue) => ({
|
|
||||||
value: optionValue,
|
|
||||||
pollId,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const poll = await prisma.poll.update({
|
|
||||||
where: {
|
|
||||||
urlId: pollId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
title: payload.title,
|
|
||||||
location: payload.location,
|
|
||||||
description: payload.description,
|
|
||||||
timeZone: payload.timeZone,
|
|
||||||
notifications: payload.notifications,
|
|
||||||
closed: payload.closed,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
options: {
|
|
||||||
orderBy: {
|
|
||||||
value: "asc",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
participants: {
|
|
||||||
include: {
|
|
||||||
votes: true,
|
|
||||||
},
|
|
||||||
orderBy: [
|
|
||||||
{
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
{ name: "desc" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
user: true,
|
|
||||||
links: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!poll) {
|
|
||||||
return res.status(404).json({ status: 404, message: "Poll not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
...poll,
|
|
||||||
role: link.role,
|
|
||||||
urlId: link.urlId,
|
|
||||||
pollId: poll.urlId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return res
|
|
||||||
.status(405)
|
|
||||||
.json({ status: 405, message: "Method not allowed" });
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { createGuestUser, withSessionRoute } from "utils/auth";
|
|
||||||
|
|
||||||
import { prisma } from "../../../../db";
|
|
||||||
import { sendNotification, withLink } from "../../../../utils/api-utils";
|
|
||||||
|
|
||||||
export default withSessionRoute(
|
|
||||||
withLink(async ({ req, res, link }) => {
|
|
||||||
switch (req.method) {
|
|
||||||
case "GET": {
|
|
||||||
const comments = await prisma.comment.findMany({
|
|
||||||
where: {
|
|
||||||
pollId: link.pollId,
|
|
||||||
},
|
|
||||||
orderBy: [
|
|
||||||
{
|
|
||||||
createdAt: "asc",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json({ comments });
|
|
||||||
}
|
|
||||||
case "POST": {
|
|
||||||
if (!req.session.user) {
|
|
||||||
await createGuestUser(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newComment = await prisma.comment.create({
|
|
||||||
data: {
|
|
||||||
content: req.body.content,
|
|
||||||
pollId: link.pollId,
|
|
||||||
authorName: req.body.authorName,
|
|
||||||
userId: req.session.user?.isGuest
|
|
||||||
? undefined
|
|
||||||
: req.session.user?.id,
|
|
||||||
guestId: req.session.user?.isGuest
|
|
||||||
? req.session.user.id
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendNotification(req, link.pollId, {
|
|
||||||
type: "newComment",
|
|
||||||
authorName: newComment.authorName,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json(newComment);
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return res
|
|
||||||
.status(405)
|
|
||||||
.json({ status: 405, message: "Method not allowed" });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
import { prisma } from "../../../../../db";
|
|
||||||
import { getQueryParam } from "../../../../../utils/api-utils";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
const urlId = getQueryParam(req, "urlId");
|
|
||||||
const commentId = getQueryParam(req, "commentId");
|
|
||||||
|
|
||||||
const link = await prisma.link.findUnique({ where: { urlId } });
|
|
||||||
|
|
||||||
if (!link) {
|
|
||||||
return res.status(404).end();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (req.method) {
|
|
||||||
case "DELETE":
|
|
||||||
await prisma.comment.delete({
|
|
||||||
where: {
|
|
||||||
id_pollId: {
|
|
||||||
id: commentId,
|
|
||||||
pollId: link.pollId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.end();
|
|
||||||
default:
|
|
||||||
return res
|
|
||||||
.status(405)
|
|
||||||
.json({ status: 405, message: "Method not allowed" });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { createGuestUser, withSessionRoute } from "utils/auth";
|
|
||||||
|
|
||||||
import { AddParticipantPayload } from "../../../../api-client/add-participant";
|
|
||||||
import { prisma } from "../../../../db";
|
|
||||||
import { sendNotification, withLink } from "../../../../utils/api-utils";
|
|
||||||
|
|
||||||
export default withSessionRoute(
|
|
||||||
withLink(async ({ req, res, link }) => {
|
|
||||||
switch (req.method) {
|
|
||||||
case "POST": {
|
|
||||||
const payload: AddParticipantPayload = req.body;
|
|
||||||
|
|
||||||
if (!req.session.user) {
|
|
||||||
await createGuestUser(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
const participant = await prisma.participant.create({
|
|
||||||
data: {
|
|
||||||
pollId: link.pollId,
|
|
||||||
name: payload.name,
|
|
||||||
userId:
|
|
||||||
req.session.user?.isGuest === false
|
|
||||||
? req.session.user.id
|
|
||||||
: undefined,
|
|
||||||
guestId:
|
|
||||||
req.session.user?.isGuest === true
|
|
||||||
? req.session.user.id
|
|
||||||
: undefined,
|
|
||||||
votes: {
|
|
||||||
createMany: {
|
|
||||||
data: payload.votes.map(({ optionId, type }) => ({
|
|
||||||
optionId,
|
|
||||||
type,
|
|
||||||
pollId: link.pollId,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
votes: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendNotification(req, link.pollId, {
|
|
||||||
type: "newParticipant",
|
|
||||||
participantName: participant.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json(participant);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return res.status(405).json({ ok: 1 });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { UpdateParticipantPayload } from "api-client/update-participant";
|
|
||||||
|
|
||||||
import { prisma } from "../../../../../db";
|
|
||||||
import { getQueryParam, withLink } from "../../../../../utils/api-utils";
|
|
||||||
|
|
||||||
export default withLink(async ({ req, res, link }) => {
|
|
||||||
const participantId = getQueryParam(req, "participantId");
|
|
||||||
|
|
||||||
const pollId = link.pollId;
|
|
||||||
switch (req.method) {
|
|
||||||
case "PATCH":
|
|
||||||
const payload: UpdateParticipantPayload = req.body;
|
|
||||||
|
|
||||||
const participant = await prisma.participant.update({
|
|
||||||
where: {
|
|
||||||
id_pollId: {
|
|
||||||
id: participantId,
|
|
||||||
pollId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
votes: {
|
|
||||||
deleteMany: {
|
|
||||||
pollId,
|
|
||||||
},
|
|
||||||
createMany: {
|
|
||||||
data: payload.votes.map(({ optionId, type }) => ({
|
|
||||||
optionId,
|
|
||||||
type,
|
|
||||||
pollId,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: req.body.name,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
votes: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json(participant);
|
|
||||||
|
|
||||||
case "DELETE":
|
|
||||||
await prisma.participant.delete({
|
|
||||||
where: {
|
|
||||||
id_pollId: {
|
|
||||||
id: participantId,
|
|
||||||
pollId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.end();
|
|
||||||
default:
|
|
||||||
return res.status(405);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,109 +0,0 @@
|
||||||
import absoluteUrl from "utils/absolute-url";
|
|
||||||
import {
|
|
||||||
createToken,
|
|
||||||
decryptToken,
|
|
||||||
mergeGuestsIntoUser as mergeUsersWithGuests,
|
|
||||||
withSessionRoute,
|
|
||||||
} from "utils/auth";
|
|
||||||
|
|
||||||
import { prisma } from "../../../../db";
|
|
||||||
import { sendEmailTemplate, withLink } from "../../../../utils/api-utils";
|
|
||||||
|
|
||||||
export default withSessionRoute(
|
|
||||||
withLink(async ({ req, res, link }) => {
|
|
||||||
if (req.method === "POST") {
|
|
||||||
if (link.role !== "admin") {
|
|
||||||
return res
|
|
||||||
.status(401)
|
|
||||||
.json({ status: 401, message: "Only admins can verify polls" });
|
|
||||||
}
|
|
||||||
const verificationCode = req.body ? req.body.verificationCode : undefined;
|
|
||||||
if (!verificationCode) {
|
|
||||||
const poll = await prisma.poll.findUnique({
|
|
||||||
where: {
|
|
||||||
urlId: link.pollId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!poll) {
|
|
||||||
return res
|
|
||||||
.status(404)
|
|
||||||
.json({ status: 404, message: "Poll not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const homePageUrl = absoluteUrl(req).origin;
|
|
||||||
const pollUrl = `${homePageUrl}/admin/${link.urlId}`;
|
|
||||||
const token = await createToken({
|
|
||||||
pollId: link.pollId,
|
|
||||||
});
|
|
||||||
const verifyEmailUrl = `${pollUrl}?code=${token}`;
|
|
||||||
|
|
||||||
await sendEmailTemplate({
|
|
||||||
templateName: "new-poll",
|
|
||||||
to: poll.user.email,
|
|
||||||
subject: `Rallly: ${poll.title} - Verify your email address`,
|
|
||||||
templateVars: {
|
|
||||||
title: poll.title,
|
|
||||||
name: poll.user.name,
|
|
||||||
pollUrl,
|
|
||||||
verifyEmailUrl,
|
|
||||||
homePageUrl,
|
|
||||||
supportEmail: process.env.SUPPORT_EMAIL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send("ok");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { pollId } = await decryptToken<{
|
|
||||||
pollId: string;
|
|
||||||
}>(verificationCode);
|
|
||||||
|
|
||||||
if (pollId !== link.pollId) {
|
|
||||||
res.status(401).json({
|
|
||||||
status: 401,
|
|
||||||
message: "Invalid token",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const poll = await prisma.poll.update({
|
|
||||||
where: {
|
|
||||||
urlId: pollId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
verified: true,
|
|
||||||
},
|
|
||||||
include: { user: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// If logged in as guest, we update all participants
|
|
||||||
// and comments by this guest to the user that we just authenticated
|
|
||||||
if (req.session.user?.isGuest) {
|
|
||||||
await mergeUsersWithGuests(poll.user.id, [req.session.user.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.session.user = {
|
|
||||||
id: poll.user.id,
|
|
||||||
isGuest: false,
|
|
||||||
name: poll.user.name,
|
|
||||||
email: poll.user.email,
|
|
||||||
};
|
|
||||||
await req.session.save();
|
|
||||||
|
|
||||||
return res.send("ok");
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.json({ status: 500, message: "Could not verify poll" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
.status(405)
|
|
||||||
.json({ status: 405, message: "Invalid http method" });
|
|
||||||
}),
|
|
||||||
);
|
|
|
@ -1,135 +0,0 @@
|
||||||
import { VoteType } from "@prisma/client";
|
|
||||||
import { addMinutes } from "date-fns";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import absoluteUrl from "utils/absolute-url";
|
|
||||||
import { nanoid } from "utils/nanoid";
|
|
||||||
|
|
||||||
import { prisma } from "../../../db";
|
|
||||||
|
|
||||||
const participantData: Array<{ name: string; votes: VoteType[] }> = [
|
|
||||||
{
|
|
||||||
name: "Reed",
|
|
||||||
votes: ["yes", "no", "ifNeedBe", "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 default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
switch (req.method) {
|
|
||||||
case "POST": {
|
|
||||||
const adminUrlId = await nanoid();
|
|
||||||
const demoUser = { name: "John Example", email: "noreply@rallly.co" };
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
const options: Array<{ value: string; id: string }> = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < optionValues.length; i++) {
|
|
||||||
options.push({ id: await nanoid(), value: optionValues[i] });
|
|
||||||
}
|
|
||||||
|
|
||||||
const participants: Array<{
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
guestId: 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 = await nanoid();
|
|
||||||
participants.push({
|
|
||||||
id: participantId,
|
|
||||||
name,
|
|
||||||
guestId: "user-demo",
|
|
||||||
createdAt: addMinutes(today, i * -1),
|
|
||||||
});
|
|
||||||
|
|
||||||
options.forEach((option, index) => {
|
|
||||||
votes.push({
|
|
||||||
optionId: option.id,
|
|
||||||
participantId,
|
|
||||||
type: participantVotes[index],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const homePageUrl = absoluteUrl(req).origin;
|
|
||||||
|
|
||||||
await prisma.poll.create({
|
|
||||||
data: {
|
|
||||||
urlId: await nanoid(),
|
|
||||||
title: "Lunch Meeting Demo",
|
|
||||||
type: "date",
|
|
||||||
location: "Starbucks, 901 New York Avenue",
|
|
||||||
description: `This poll has been automatically generated just for you! Feel free to try out all the different features and when you're ready, you can go to ${homePageUrl}/new to make a new poll.`,
|
|
||||||
authorName: "Johnny",
|
|
||||||
verified: true,
|
|
||||||
demo: true,
|
|
||||||
user: {
|
|
||||||
connectOrCreate: {
|
|
||||||
where: {
|
|
||||||
email: demoUser.email,
|
|
||||||
},
|
|
||||||
create: demoUser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
createMany: {
|
|
||||||
data: options,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
links: {
|
|
||||||
createMany: {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
role: "admin",
|
|
||||||
urlId: adminUrlId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "participant",
|
|
||||||
urlId: await nanoid(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
participants: {
|
|
||||||
createMany: {
|
|
||||||
data: participants,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
createMany: {
|
|
||||||
data: votes,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json({ urlId: adminUrlId });
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return res.status(405).json({ ok: 1 });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { withSessionRoute } from "utils/auth";
|
|
||||||
|
|
||||||
import { prisma } from "../../db";
|
|
||||||
|
|
||||||
export default withSessionRoute(async (req, res) => {
|
|
||||||
if (req.session.user?.isGuest === false) {
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: req.session.user.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
user: user
|
|
||||||
? { id: user.id, name: user.name, email: user.email, isGuest: false }
|
|
||||||
: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.json({ user: req.session.user ?? null });
|
|
||||||
});
|
|
107
pages/poll.tsx
|
@ -1,107 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
|
||||||
import { usePlausible } from "next-plausible";
|
|
||||||
import React from "react";
|
|
||||||
import { useQuery } from "react-query";
|
|
||||||
import { withSessionSsr } from "utils/auth";
|
|
||||||
|
|
||||||
import ErrorPage from "@/components/error-page";
|
|
||||||
import FullPageLoader from "@/components/full-page-loader";
|
|
||||||
import { PollContextProvider } from "@/components/poll-context";
|
|
||||||
import { SessionProps, withSession } from "@/components/session";
|
|
||||||
|
|
||||||
import { GetPollResponse } from "../api-client/get-poll";
|
|
||||||
import Custom404 from "./404";
|
|
||||||
|
|
||||||
const PollPage = dynamic(() => import("@/components/poll"), { ssr: false });
|
|
||||||
|
|
||||||
const PollPageLoader: NextPage<SessionProps> = () => {
|
|
||||||
const { query } = useRouter();
|
|
||||||
const { t } = useTranslation("app");
|
|
||||||
const urlId = query.urlId as string;
|
|
||||||
|
|
||||||
const [didError, setDidError] = React.useState(false);
|
|
||||||
const [didFailToConvertLegacyPoll, setDidFailToConvertLegacyPoll] =
|
|
||||||
React.useState(false);
|
|
||||||
|
|
||||||
const plausible = usePlausible();
|
|
||||||
|
|
||||||
const { data: poll } = useQuery<GetPollResponse | null>(
|
|
||||||
["getPoll", urlId],
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get<GetPollResponse>(`/api/poll/${urlId}`);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
|
||||||
try {
|
|
||||||
// check if poll exists from the legacy endpoint and convert it if it does
|
|
||||||
const { data } = await axios.get<GetPollResponse>(
|
|
||||||
`/api/legacy/${urlId}`,
|
|
||||||
);
|
|
||||||
plausible("Converted legacy event");
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
if (axios.isAxiosError(err) && err.response?.status === 500) {
|
|
||||||
// failed to convert legacy poll
|
|
||||||
setDidFailToConvertLegacyPoll(true);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onError: () => {
|
|
||||||
setDidError(true);
|
|
||||||
},
|
|
||||||
retry: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (didFailToConvertLegacyPoll) {
|
|
||||||
return (
|
|
||||||
<ErrorPage
|
|
||||||
title="Server error"
|
|
||||||
description="There was a problem retrieving your poll. Please contact support."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (poll) {
|
|
||||||
return (
|
|
||||||
<PollContextProvider value={poll}>
|
|
||||||
<PollPage />
|
|
||||||
</PollContextProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (didError) {
|
|
||||||
return <Custom404 />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <FullPageLoader>{t("loading")}</FullPageLoader>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
|
||||||
async ({ locale = "en", req }) => {
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
...(await serverSideTranslations(locale, ["app"])),
|
|
||||||
user: req.session.user ?? null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return { notFound: true };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export default withSession(PollPageLoader);
|
|
|
@ -36,6 +36,7 @@
|
||||||
"creatingDemo": "Creating demo poll…",
|
"creatingDemo": "Creating demo poll…",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
|
"loadingParticipants": "Loading participants…",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"adminDescription": "Full access to edit this poll.",
|
"adminDescription": "Full access to edit this poll.",
|
||||||
"participant": "Participant",
|
"participant": "Participant",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||||
DATABASE_URL=postgres://your-database/db
|
DATABASE_URL=postgres://your-database/db
|
||||||
SECRET_PASSWORD=minimum-32-characters
|
SECRET_PASSWORD=minimum-32-characters
|
||||||
SUPPORT_EMAIL=foo@yourdomain.com
|
SUPPORT_EMAIL=foo@yourdomain.com
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
defaults.url=https://sentry.io/
|
|
||||||
defaults.org=stack-snap
|
|
||||||
defaults.project=rallly
|
|
||||||
cli.executable=../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli
|
|
|
@ -3,7 +3,8 @@ import Cookies from "js-cookie";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { getPortal } from "utils/selectors";
|
|
||||||
|
import { getPortal } from "@/utils/selectors";
|
||||||
|
|
||||||
import CookiesIllustration from "./cookie-consent/cookies.svg";
|
import CookiesIllustration from "./cookie-consent/cookies.svg";
|
||||||
|
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
@ -4,11 +4,11 @@ import { useRouter } from "next/router";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { usePlausible } from "next-plausible";
|
import { usePlausible } from "next-plausible";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import { useSessionStorage } from "react-use";
|
import { useSessionStorage } from "react-use";
|
||||||
|
|
||||||
import { createPoll } from "../api-client/create-poll";
|
import { encodeDateOption } from "../utils/date-time-utils";
|
||||||
import Button from "../components/button";
|
import { trpc } from "../utils/trpc";
|
||||||
|
import Button from "./button";
|
||||||
import {
|
import {
|
||||||
NewEventData,
|
NewEventData,
|
||||||
PollDetailsData,
|
PollDetailsData,
|
||||||
|
@ -17,11 +17,10 @@ import {
|
||||||
PollOptionsForm,
|
PollOptionsForm,
|
||||||
UserDetailsData,
|
UserDetailsData,
|
||||||
UserDetailsForm,
|
UserDetailsForm,
|
||||||
} from "../components/forms";
|
} from "./forms";
|
||||||
import StandardLayout from "../components/standard-layout";
|
|
||||||
import Steps from "../components/steps";
|
|
||||||
import { encodeDateOption } from "../utils/date-time-utils";
|
|
||||||
import { SessionProps, useSession, withSession } from "./session";
|
import { SessionProps, useSession, withSession } from "./session";
|
||||||
|
import StandardLayout from "./standard-layout";
|
||||||
|
import Steps from "./steps";
|
||||||
|
|
||||||
type StepName = "eventDetails" | "options" | "userDetails";
|
type StepName = "eventDetails" | "options" | "userDetails";
|
||||||
|
|
||||||
|
@ -95,41 +94,23 @@ const Page: NextPage<CreatePollPageProps> = ({
|
||||||
|
|
||||||
const plausible = usePlausible();
|
const plausible = usePlausible();
|
||||||
|
|
||||||
const { mutate: createEventMutation, isLoading: isCreatingPoll } =
|
const createPoll = trpc.useMutation(["polls.create"], {
|
||||||
useMutation(
|
onSuccess: (poll) => {
|
||||||
() => {
|
setIsRedirecting(true);
|
||||||
const title = required(formData?.eventDetails?.title);
|
plausible("Created poll", {
|
||||||
return createPoll({
|
props: {
|
||||||
title: title,
|
numberOfOptions: formData.options?.options?.length,
|
||||||
type: "date",
|
optionsView: formData?.options?.view,
|
||||||
location: formData?.eventDetails?.location,
|
|
||||||
description: formData?.eventDetails?.description,
|
|
||||||
user: {
|
|
||||||
name: required(formData?.userDetails?.name),
|
|
||||||
email: required(formData?.userDetails?.contact),
|
|
||||||
},
|
|
||||||
timeZone: formData?.options?.timeZone,
|
|
||||||
options: required(formData?.options?.options).map(encodeDateOption),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: (poll) => {
|
|
||||||
setIsRedirecting(true);
|
|
||||||
plausible("Created poll", {
|
|
||||||
props: {
|
|
||||||
numberOfOptions: formData.options?.options?.length,
|
|
||||||
optionsView: formData?.options?.view,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setPersistedFormData(initialNewEventData);
|
|
||||||
router.replace(`/admin/${poll.urlId}`);
|
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
);
|
setPersistedFormData(initialNewEventData);
|
||||||
|
router.replace(`/admin/${poll.urlId}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const isBusy = isRedirecting || isCreatingPoll;
|
const isBusy = isRedirecting || createPoll.isLoading;
|
||||||
|
|
||||||
const handleSubmit = (
|
const handleSubmit = async (
|
||||||
data: PollDetailsData | PollOptionsData | UserDetailsData,
|
data: PollDetailsData | PollOptionsData | UserDetailsData,
|
||||||
) => {
|
) => {
|
||||||
if (currentStepIndex < steps.length - 1) {
|
if (currentStepIndex < steps.length - 1) {
|
||||||
|
@ -140,7 +121,20 @@ const Page: NextPage<CreatePollPageProps> = ({
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// last step
|
// last step
|
||||||
createEventMutation();
|
const title = required(formData?.eventDetails?.title);
|
||||||
|
|
||||||
|
await createPoll.mutateAsync({
|
||||||
|
title: title,
|
||||||
|
type: "date",
|
||||||
|
location: formData?.eventDetails?.location,
|
||||||
|
description: formData?.eventDetails?.description,
|
||||||
|
user: {
|
||||||
|
name: required(formData?.userDetails?.name),
|
||||||
|
email: required(formData?.userDetails?.contact),
|
||||||
|
},
|
||||||
|
timeZone: formData?.options?.timeZone,
|
||||||
|
options: required(formData?.options?.options).map(encodeDateOption),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
import { Comment } from "@prisma/client";
|
|
||||||
import axios from "axios";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { formatRelative } from "date-fns";
|
import { formatRelative } from "date-fns";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { usePlausible } from "next-plausible";
|
import { usePlausible } from "next-plausible";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
|
||||||
|
|
||||||
import {
|
|
||||||
createComment,
|
|
||||||
CreateCommentPayload,
|
|
||||||
} from "../../api-client/create-comment";
|
|
||||||
import { requiredString } from "../../utils/form-validation";
|
import { requiredString } from "../../utils/form-validation";
|
||||||
|
import { trpc } from "../../utils/trpc";
|
||||||
import Button from "../button";
|
import Button from "../button";
|
||||||
import CompactButton from "../compact-button";
|
import CompactButton from "../compact-button";
|
||||||
import Dropdown, { DropdownItem } from "../dropdown";
|
import Dropdown, { DropdownItem } from "../dropdown";
|
||||||
|
@ -25,29 +19,20 @@ import { usePoll } from "../poll-context";
|
||||||
import { usePreferences } from "../preferences/use-preferences";
|
import { usePreferences } from "../preferences/use-preferences";
|
||||||
import { isUnclaimed, useSession } from "../session";
|
import { isUnclaimed, useSession } from "../session";
|
||||||
|
|
||||||
export interface DiscussionProps {
|
|
||||||
pollId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CommentForm {
|
interface CommentForm {
|
||||||
authorName: string;
|
authorName: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
const Discussion: React.VoidFunctionComponent = () => {
|
||||||
pollId,
|
|
||||||
}) => {
|
|
||||||
const { locale } = usePreferences();
|
const { locale } = usePreferences();
|
||||||
const getCommentsQueryKey = ["poll", pollId, "comments"];
|
const queryClient = trpc.useContext();
|
||||||
const queryClient = useQueryClient();
|
const {
|
||||||
const { data: comments } = useQuery(
|
poll: { pollId },
|
||||||
getCommentsQueryKey,
|
} = usePoll();
|
||||||
async () => {
|
|
||||||
const res = await axios.get<{
|
const { data: comments } = trpc.useQuery(
|
||||||
comments: Array<Omit<Comment, "createdAt"> & { createdAt: string }>;
|
["polls.comments.list", { pollId }],
|
||||||
}>(`/api/poll/${pollId}/comments`);
|
|
||||||
return res.data.comments;
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
refetchInterval: 10000, // refetch every 10 seconds
|
refetchInterval: 10000, // refetch every 10 seconds
|
||||||
},
|
},
|
||||||
|
@ -55,44 +40,38 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
||||||
|
|
||||||
const plausible = usePlausible();
|
const plausible = usePlausible();
|
||||||
|
|
||||||
const { mutate: createCommentMutation } = useMutation(
|
const addComment = trpc.useMutation("polls.comments.add", {
|
||||||
(payload: CreateCommentPayload) => {
|
onSuccess: (newComment) => {
|
||||||
// post comment
|
session.refresh();
|
||||||
return createComment(payload);
|
queryClient.setQueryData(
|
||||||
|
["polls.comments.list", { pollId }],
|
||||||
|
(existingComments = []) => {
|
||||||
|
return [...existingComments, newComment];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
plausible("Created comment");
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
onSuccess: (newComment) => {
|
|
||||||
session.refresh();
|
|
||||||
queryClient.setQueryData(getCommentsQueryKey, (comments) => {
|
|
||||||
if (Array.isArray(comments)) {
|
|
||||||
return [...comments, newComment];
|
|
||||||
}
|
|
||||||
return [newComment];
|
|
||||||
});
|
|
||||||
plausible("Created comment");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { poll } = usePoll();
|
const { poll } = usePoll();
|
||||||
|
|
||||||
const { mutate: deleteCommentMutation } = useMutation(
|
const deleteComment = trpc.useMutation("polls.comments.delete", {
|
||||||
async (payload: { pollId: string; commentId: string }) => {
|
onMutate: ({ commentId }) => {
|
||||||
await axios.delete(`/api/poll/${pollId}/comments/${payload.commentId}`);
|
queryClient.setQueryData(
|
||||||
|
["polls.comments.list", { pollId }],
|
||||||
|
(existingComments = []) => {
|
||||||
|
return [...existingComments].filter(({ id }) => id !== commentId);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
onSuccess: () => {
|
||||||
onMutate: () => {
|
plausible("Deleted comment");
|
||||||
plausible("Deleted comment");
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(getCommentsQueryKey);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
const { register, setValue, control, handleSubmit, formState } =
|
const { register, reset, control, handleSubmit, formState } =
|
||||||
useForm<CommentForm>({
|
useForm<CommentForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authorName: "",
|
authorName: "",
|
||||||
|
@ -100,13 +79,6 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = React.useCallback(
|
|
||||||
(commentId: string) => {
|
|
||||||
deleteCommentMutation({ pollId, commentId });
|
|
||||||
},
|
|
||||||
[deleteCommentMutation, pollId],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!comments) {
|
if (!comments) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -130,6 +102,7 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
layoutId={comment.id}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
|
@ -171,7 +144,10 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
||||||
icon={Trash}
|
icon={Trash}
|
||||||
label="Delete comment"
|
label="Delete comment"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDelete(comment.id);
|
deleteComment.mutate({
|
||||||
|
commentId: comment.id,
|
||||||
|
pollId,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -188,22 +164,9 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
className="bg-white p-4"
|
className="bg-white p-4"
|
||||||
onSubmit={handleSubmit((data) => {
|
onSubmit={handleSubmit(async ({ authorName, content }) => {
|
||||||
return new Promise((resolve, reject) => {
|
await addComment.mutateAsync({ authorName, content, pollId });
|
||||||
createCommentMutation(
|
reset({ authorName, content: "" });
|
||||||
{
|
|
||||||
...data,
|
|
||||||
pollId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
setValue("content", "");
|
|
||||||
resolve(data);
|
|
||||||
},
|
|
||||||
onError: reject,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
|
@ -10,8 +10,9 @@ import { Menu } from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { transformOriginByPlacement } from "utils/constants";
|
|
||||||
import { stopPropagation } from "utils/stop-propagation";
|
import { transformOriginByPlacement } from "@/utils/constants";
|
||||||
|
import { stopPropagation } from "@/utils/stop-propagation";
|
||||||
|
|
||||||
const MotionMenuItems = motion(Menu.Items);
|
const MotionMenuItems = motion(Menu.Items);
|
||||||
|
|
|
@ -9,9 +9,9 @@ import { Listbox } from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { addMinutes, format, isSameDay, setHours, setMinutes } from "date-fns";
|
import { addMinutes, format, isSameDay, setHours, setMinutes } from "date-fns";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { stopPropagation } from "utils/stop-propagation";
|
|
||||||
|
|
||||||
import { usePreferences } from "@/components/preferences/use-preferences";
|
import { usePreferences } from "@/components/preferences/use-preferences";
|
||||||
|
import { stopPropagation } from "@/utils/stop-propagation";
|
||||||
|
|
||||||
import ChevronDown from "../../../icons/chevron-down.svg";
|
import ChevronDown from "../../../icons/chevron-down.svg";
|
||||||
import { styleMenuItem } from "../../../menu-styles";
|
import { styleMenuItem } from "../../../menu-styles";
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 744 B After Width: | Height: | Size: 744 B |
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 332 B |
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 313 B |
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 291 B |
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 468 B |
Before Width: | Height: | Size: 674 B After Width: | Height: | Size: 674 B |
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 343 B After Width: | Height: | Size: 343 B |
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 354 B |
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
Before Width: | Height: | Size: 273 B After Width: | Height: | Size: 273 B |
Before Width: | Height: | Size: 273 B After Width: | Height: | Size: 273 B |
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 274 B |
Before Width: | Height: | Size: 273 B After Width: | Height: | Size: 273 B |
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B |