mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-24 03:37:23 +02:00
First public commit
This commit is contained in:
commit
e05cd62e53
228 changed files with 17717 additions and 0 deletions
36
pages/api/poll/[urlId]/comments/[commentId].ts
Normal file
36
pages/api/poll/[urlId]/comments/[commentId].ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
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" });
|
||||
}
|
||||
}
|
86
pages/api/poll/[urlId]/comments/index.ts
Normal file
86
pages/api/poll/[urlId]/comments/index.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import absoluteUrl from "utils/absolute-url";
|
||||
import { prisma } from "../../../../../db";
|
||||
import {
|
||||
getAdminLink,
|
||||
sendEmailTemplate,
|
||||
withLink,
|
||||
} from "../../../../../utils/api-utils";
|
||||
|
||||
export default 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": {
|
||||
const newComment = await prisma.comment.create({
|
||||
data: {
|
||||
content: req.body.content,
|
||||
pollId: link.pollId,
|
||||
authorName: req.body.authorName,
|
||||
},
|
||||
});
|
||||
|
||||
if (link.role === "participant") {
|
||||
const poll = await prisma.poll.findUnique({
|
||||
where: {
|
||||
urlId: link.pollId,
|
||||
},
|
||||
include: {
|
||||
links: true,
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (poll?.notifications && poll.verified && !poll.demo) {
|
||||
// Get the admin link
|
||||
const adminLink = getAdminLink(poll.links);
|
||||
|
||||
if (adminLink) {
|
||||
const homePageUrl = absoluteUrl(req).origin;
|
||||
const pollUrl = `${homePageUrl}/admin/${adminLink.urlId}`;
|
||||
const unsubscribeUrl = `${pollUrl}?unsubscribe=true`;
|
||||
|
||||
try {
|
||||
await sendEmailTemplate({
|
||||
templateName: "new-comment",
|
||||
to: poll.user.email,
|
||||
subject: `Rallly: ${poll.title} - New Comment`,
|
||||
templateVars: {
|
||||
title: poll.title,
|
||||
name: poll.authorName,
|
||||
author: newComment.authorName,
|
||||
pollUrl,
|
||||
homePageUrl: absoluteUrl(req).origin,
|
||||
supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
|
||||
unsubscribeUrl,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
console.log(`Missing admin link for poll: ${link.pollId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(newComment);
|
||||
}
|
||||
|
||||
default:
|
||||
return res
|
||||
.status(405)
|
||||
.json({ status: 405, message: "Method not allowed" });
|
||||
}
|
||||
});
|
138
pages/api/poll/[urlId]/index.ts
Normal file
138
pages/api/poll/[urlId]/index.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
import { GetPollApiResponse } from "api-client/get-poll";
|
||||
import { NextApiResponse } from "next";
|
||||
import { UpdatePollPayload } from "../../../../api-client/update-poll";
|
||||
import { prisma } from "../../../../db";
|
||||
import { exclude, withLink } from "../../../../utils/api-utils";
|
||||
|
||||
export default withLink(
|
||||
async (
|
||||
req,
|
||||
res: NextApiResponse<
|
||||
GetPollApiResponse | { status: number; message: string }
|
||||
>,
|
||||
link,
|
||||
) => {
|
||||
const pollId = link.pollId;
|
||||
|
||||
switch (req.method) {
|
||||
case "GET": {
|
||||
const poll = await prisma.poll.findUnique({
|
||||
where: {
|
||||
urlId: pollId,
|
||||
},
|
||||
include: {
|
||||
options: {
|
||||
include: {
|
||||
votes: true,
|
||||
},
|
||||
},
|
||||
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" });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
...exclude(poll, "verificationCode"),
|
||||
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: {
|
||||
include: {
|
||||
votes: true,
|
||||
},
|
||||
},
|
||||
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({
|
||||
...exclude(poll, "verificationCode"),
|
||||
role: link.role,
|
||||
urlId: link.urlId,
|
||||
pollId: poll.urlId,
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return res
|
||||
.status(405)
|
||||
.json({ status: 405, message: "Method not allowed" });
|
||||
}
|
||||
},
|
||||
);
|
48
pages/api/poll/[urlId]/participant/[participantId].ts
Normal file
48
pages/api/poll/[urlId]/participant/[participantId].ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { prisma } from "../../../../../db";
|
||||
import { withLink, getQueryParam } 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":
|
||||
await prisma.participant.update({
|
||||
where: {
|
||||
id_pollId: {
|
||||
id: participantId,
|
||||
pollId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
votes: {
|
||||
deleteMany: {
|
||||
pollId,
|
||||
},
|
||||
createMany: {
|
||||
data: req.body.votes.map((optionId: string) => ({
|
||||
optionId,
|
||||
pollId,
|
||||
})),
|
||||
},
|
||||
},
|
||||
name: req.body.name,
|
||||
},
|
||||
});
|
||||
|
||||
return res.end();
|
||||
|
||||
case "DELETE":
|
||||
await prisma.participant.delete({
|
||||
where: {
|
||||
id_pollId: {
|
||||
id: participantId,
|
||||
pollId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.end();
|
||||
default:
|
||||
}
|
||||
});
|
82
pages/api/poll/[urlId]/participant/index.ts
Normal file
82
pages/api/poll/[urlId]/participant/index.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import absoluteUrl from "utils/absolute-url";
|
||||
import { AddParticipantPayload } from "../../../../../api-client/add-participant";
|
||||
import { prisma } from "../../../../../db";
|
||||
import {
|
||||
getAdminLink,
|
||||
sendEmailTemplate,
|
||||
withLink,
|
||||
} from "../../../../../utils/api-utils";
|
||||
|
||||
export default withLink(async (req, res, link) => {
|
||||
switch (req.method) {
|
||||
case "POST": {
|
||||
const payload: AddParticipantPayload = req.body;
|
||||
|
||||
const participant = await prisma.participant.create({
|
||||
data: {
|
||||
pollId: link.pollId,
|
||||
name: payload.name,
|
||||
votes: {
|
||||
createMany: {
|
||||
data: payload.votes.map((optionId) => ({
|
||||
optionId,
|
||||
pollId: link.pollId,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
votes: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (link.role === "participant") {
|
||||
const poll = await prisma.poll.findUnique({
|
||||
where: {
|
||||
urlId: link.pollId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
links: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (poll?.notifications && poll.verified && !poll.demo) {
|
||||
// Get the admin link
|
||||
const adminLink = getAdminLink(poll.links);
|
||||
|
||||
if (adminLink) {
|
||||
const homePageUrl = absoluteUrl(req).origin;
|
||||
const pollUrl = `${homePageUrl}/admin/${adminLink.urlId}`;
|
||||
const unsubscribeUrl = `${pollUrl}?unsubscribe=true`;
|
||||
|
||||
try {
|
||||
await sendEmailTemplate({
|
||||
templateName: "new-participant",
|
||||
to: poll.user.email,
|
||||
subject: `Rallly: ${poll.title} - New Participant`,
|
||||
templateVars: {
|
||||
title: poll.title,
|
||||
name: poll.authorName,
|
||||
participantName: participant.name,
|
||||
pollUrl,
|
||||
homePageUrl: absoluteUrl(req).origin,
|
||||
supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
|
||||
unsubscribeUrl,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
console.error(`Missing admin link for poll: ${link.pollId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(participant);
|
||||
}
|
||||
default:
|
||||
return res.status(405).json({ ok: 1 });
|
||||
}
|
||||
});
|
32
pages/api/poll/[urlId]/verify.ts
Normal file
32
pages/api/poll/[urlId]/verify.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { prisma } from "../../../../db";
|
||||
import { withLink } from "../../../../utils/api-utils";
|
||||
|
||||
export default withLink(async (req, res, link) => {
|
||||
if (req.method === "POST") {
|
||||
if (link.role !== "admin") {
|
||||
return res.status(401).end();
|
||||
}
|
||||
const { verificationCode } = req.body;
|
||||
try {
|
||||
await prisma.poll.update({
|
||||
where: {
|
||||
urlId_verificationCode: {
|
||||
urlId: link.pollId,
|
||||
verificationCode,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
verified: true,
|
||||
},
|
||||
});
|
||||
return res.end();
|
||||
} catch {
|
||||
console.error(
|
||||
`Failed to verify poll "${link.pollId}" with code ${verificationCode}`,
|
||||
);
|
||||
return res.status(500).end();
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(405).end();
|
||||
});
|
103
pages/api/poll/create-demo.ts
Normal file
103
pages/api/poll/create-demo.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { addDays, addMinutes, format } from "date-fns";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { nanoid } from "utils/nanoid";
|
||||
|
||||
import { prisma } from "../../../db";
|
||||
|
||||
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 poll = await prisma.poll.create({
|
||||
data: {
|
||||
urlId: await nanoid(),
|
||||
verificationCode: 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 schedule a new poll.",
|
||||
authorName: "John Example",
|
||||
verified: true,
|
||||
demo: true,
|
||||
user: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
email: demoUser.email,
|
||||
},
|
||||
create: demoUser,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
createMany: {
|
||||
data: [...Array(4)].map((_, i) => {
|
||||
return { value: format(addDays(today, i + 1), "yyyy-MM-dd") };
|
||||
}),
|
||||
},
|
||||
},
|
||||
links: {
|
||||
createMany: {
|
||||
data: [
|
||||
{
|
||||
role: "admin",
|
||||
urlId: adminUrlId,
|
||||
},
|
||||
{
|
||||
role: "participant",
|
||||
urlId: await nanoid(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
participants: {
|
||||
createMany: {
|
||||
data: [
|
||||
{ name: "John", createdAt: addMinutes(today, -1) },
|
||||
{ name: "Alex", createdAt: addMinutes(today, -2) },
|
||||
{ name: "Mark", createdAt: addMinutes(today, -3) },
|
||||
{ name: "Samantha", createdAt: addMinutes(today, -4) },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
options: true,
|
||||
participants: true,
|
||||
},
|
||||
});
|
||||
|
||||
const voteData: Array<{
|
||||
optionId: string;
|
||||
participantId: string;
|
||||
pollId: string;
|
||||
}> = [];
|
||||
|
||||
// Randomly generate votes
|
||||
poll.options.forEach((option) => {
|
||||
poll.participants.forEach((participant) => {
|
||||
if (Math.random() >= 0.5) {
|
||||
voteData.push({
|
||||
optionId: option.id,
|
||||
participantId: participant.id,
|
||||
pollId: poll.urlId,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await prisma.vote.createMany({
|
||||
data: voteData,
|
||||
});
|
||||
|
||||
return res.json({ urlId: adminUrlId });
|
||||
}
|
||||
default:
|
||||
return res.status(405).json({ ok: 1 });
|
||||
}
|
||||
}
|
89
pages/api/poll/index.ts
Normal file
89
pages/api/poll/index.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { sendEmailTemplate } from "utils/api-utils";
|
||||
import { nanoid } from "utils/nanoid";
|
||||
import { CreatePollPayload } from "../../../api-client/create-poll";
|
||||
import { prisma } from "../../../db";
|
||||
import absoluteUrl from "../../../utils/absolute-url";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
switch (req.method) {
|
||||
case "POST": {
|
||||
const adminUrlId = await nanoid();
|
||||
const payload: CreatePollPayload = req.body;
|
||||
const poll = await prisma.poll.create({
|
||||
data: {
|
||||
urlId: await nanoid(),
|
||||
verificationCode: await nanoid(),
|
||||
title: payload.title,
|
||||
type: payload.type,
|
||||
timeZone: payload.timeZone,
|
||||
location: payload.location,
|
||||
description: payload.description,
|
||||
authorName: payload.user.name,
|
||||
demo: payload.demo,
|
||||
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}`;
|
||||
const verifyEmailUrl = `${pollUrl}?code=${poll.verificationCode}`;
|
||||
|
||||
try {
|
||||
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.NEXT_PUBLIC_SUPPORT_EMAIL,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return res.json({ urlId: adminUrlId, authorName: poll.authorName });
|
||||
}
|
||||
default:
|
||||
return res.status(405).end();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue