mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-20 20:36:19 +02:00
✨ Use dialogs to finalize and duplicate polls (#1099)
This commit is contained in:
parent
dee3e1b7d0
commit
2185ec5b83
17 changed files with 442 additions and 397 deletions
5
apps/web/src/app/[locale]/[...rest]/page.tsx
Normal file
5
apps/web/src/app/[locale]/[...rest]/page.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
export default function CatchAllPage() {
|
||||||
|
notFound();
|
||||||
|
}
|
|
@ -1,33 +0,0 @@
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import { XCircleIcon } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { getTranslation } from "@/app/i18n";
|
|
||||||
import {
|
|
||||||
PageDialog,
|
|
||||||
PageDialogDescription,
|
|
||||||
PageDialogFooter,
|
|
||||||
PageDialogHeader,
|
|
||||||
PageDialogTitle,
|
|
||||||
} from "@/components/page-dialog";
|
|
||||||
|
|
||||||
export default async function NotFound() {
|
|
||||||
// TODO (Luke Vella) [2023-10-31]: No way to get locale from not-found
|
|
||||||
// See: https://github.com/vercel/next.js/discussions/43179
|
|
||||||
const { t } = await getTranslation("en");
|
|
||||||
return (
|
|
||||||
<PageDialog icon={XCircleIcon}>
|
|
||||||
<PageDialogHeader>
|
|
||||||
<PageDialogTitle>{t("authErrorTitle")}</PageDialogTitle>
|
|
||||||
<PageDialogDescription>
|
|
||||||
{t("authErrorDescription")}
|
|
||||||
</PageDialogDescription>
|
|
||||||
</PageDialogHeader>
|
|
||||||
<PageDialogFooter>
|
|
||||||
<Button asChild variant="primary">
|
|
||||||
<Link href="/login">{t("authErrorCta")}</Link>
|
|
||||||
</Button>
|
|
||||||
</PageDialogFooter>
|
|
||||||
</PageDialog>
|
|
||||||
);
|
|
||||||
}
|
|
83
apps/web/src/app/[locale]/poll/[urlId]/duplicate-dialog.tsx
Normal file
83
apps/web/src/app/[locale]/poll/[urlId]/duplicate-dialog.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
"use client";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogProps,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@rallly/ui/dialog";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { DuplicateForm } from "@/app/[locale]/poll/[urlId]/duplicate-form";
|
||||||
|
import { PayWallDialogContent } from "@/app/[locale]/poll/[urlId]/pay-wall-dialog-content";
|
||||||
|
import { trpc } from "@/app/providers";
|
||||||
|
import { Trans } from "@/components/trans";
|
||||||
|
import { usePostHog } from "@/utils/posthog";
|
||||||
|
|
||||||
|
const formName = "duplicate-form";
|
||||||
|
export function DuplicateDialog({
|
||||||
|
pollId,
|
||||||
|
pollTitle,
|
||||||
|
...props
|
||||||
|
}: DialogProps & { pollId: string; pollTitle: string }) {
|
||||||
|
const queryClient = trpc.useUtils();
|
||||||
|
const duplicate = trpc.polls.duplicate.useMutation();
|
||||||
|
const posthog = usePostHog();
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<PayWallDialogContent>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans i18nKey="duplicate" />
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans i18nKey="duplicateDescription" />
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DuplicateForm
|
||||||
|
name={formName}
|
||||||
|
defaultValues={{
|
||||||
|
title: pollTitle,
|
||||||
|
}}
|
||||||
|
onSubmit={(data) => {
|
||||||
|
duplicate.mutate(
|
||||||
|
{ pollId, newTitle: data.title },
|
||||||
|
{
|
||||||
|
onSuccess: async (res) => {
|
||||||
|
posthog?.capture("duplicate poll", {
|
||||||
|
pollId,
|
||||||
|
newPollId: res.id,
|
||||||
|
});
|
||||||
|
queryClient.invalidate();
|
||||||
|
router.push(`/poll/${res.id}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button>
|
||||||
|
<Trans i18nKey="cancel" />
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={duplicate.isLoading}
|
||||||
|
variant="primary"
|
||||||
|
form={formName}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="duplicate" />
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</PayWallDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
67
apps/web/src/app/[locale]/poll/[urlId]/duplicate-form.tsx
Normal file
67
apps/web/src/app/[locale]/poll/[urlId]/duplicate-form.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
"use client";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@rallly/ui/form";
|
||||||
|
import { Input } from "@rallly/ui/input";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Trans } from "@/components/trans";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
title: z.string().trim().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DuplicateFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export function DuplicateForm({
|
||||||
|
name,
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
onSubmit?: (data: DuplicateFormData) => void;
|
||||||
|
defaultValues?: DuplicateFormData;
|
||||||
|
}) {
|
||||||
|
const form = useForm<DuplicateFormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id={name}
|
||||||
|
onSubmit={form.handleSubmit((data) => {
|
||||||
|
onSubmit?.(data);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey="duplicateTitleLabel" defaults="Title" />
|
||||||
|
</FormLabel>
|
||||||
|
<Input {...field} className="w-full" />
|
||||||
|
<FormDescription>
|
||||||
|
<Trans
|
||||||
|
i18nKey="duplicateTitleDescription"
|
||||||
|
defaults="Hint: Give your new poll a unique title"
|
||||||
|
/>
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,125 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@rallly/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
} from "@rallly/ui/form";
|
|
||||||
import { Input } from "@rallly/ui/input";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { PayWall } from "@/components/pay-wall";
|
|
||||||
import { usePoll } from "@/components/poll-context";
|
|
||||||
import { Trans } from "@/components/trans";
|
|
||||||
import { NextPageWithLayout } from "@/types";
|
|
||||||
import { usePostHog } from "@/utils/posthog";
|
|
||||||
import { trpc } from "@/utils/trpc/client";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
title: z.string().trim().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
|
||||||
const { poll } = usePoll();
|
|
||||||
const duplicate = trpc.polls.duplicate.useMutation();
|
|
||||||
const router = useRouter();
|
|
||||||
const posthog = usePostHog();
|
|
||||||
const pollLink = `/poll/${poll.id}`;
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
title: poll.title,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PayWall>
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit((data) => {
|
|
||||||
//submit
|
|
||||||
duplicate.mutate(
|
|
||||||
{ pollId: poll.id, newTitle: data.title },
|
|
||||||
{
|
|
||||||
onSuccess: async (res) => {
|
|
||||||
posthog?.capture("duplicate poll", {
|
|
||||||
pollId: poll.id,
|
|
||||||
newPollId: res.id,
|
|
||||||
});
|
|
||||||
router.push(`/poll/${res.id}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
<Trans i18nKey="duplicate" defaults="Duplicate" />
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
<Trans
|
|
||||||
i18nKey="duplicateDescription"
|
|
||||||
defaults="Create a new poll based on this one"
|
|
||||||
/>
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="title"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
<Trans i18nKey="duplicateTitleLabel" defaults="Title" />
|
|
||||||
</FormLabel>
|
|
||||||
<Input className="w-full" {...field} />
|
|
||||||
<FormDescription>
|
|
||||||
<Trans
|
|
||||||
i18nKey="duplicateTitleDescription"
|
|
||||||
defaults="Hint: Give your new poll a unique title"
|
|
||||||
/>
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="justify-between">
|
|
||||||
<Button asChild>
|
|
||||||
<Link href={pollLink}>
|
|
||||||
<Trans i18nKey="cancel" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
loading={duplicate.isLoading}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
<Trans i18nKey="duplicate" defaults="Duplicate" />
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</PayWall>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
|
@ -1,96 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import {
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@rallly/ui/card";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
import { Card } from "@/components/card";
|
|
||||||
import { PayWall } from "@/components/pay-wall";
|
|
||||||
import { FinalizePollForm } from "@/components/poll/manage-poll/finalize-poll-dialog";
|
|
||||||
import { Trans } from "@/components/trans";
|
|
||||||
import { usePlan } from "@/contexts/plan";
|
|
||||||
import { usePoll } from "@/contexts/poll";
|
|
||||||
import { NextPageWithLayout } from "@/types";
|
|
||||||
import { usePostHog } from "@/utils/posthog";
|
|
||||||
import { trpc } from "@/utils/trpc/client";
|
|
||||||
|
|
||||||
const FinalizationForm = () => {
|
|
||||||
const plan = usePlan();
|
|
||||||
const poll = usePoll();
|
|
||||||
const posthog = usePostHog();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const redirectBackToPoll = () => {
|
|
||||||
router.replace(`/poll/${poll.id}`);
|
|
||||||
};
|
|
||||||
const queryClient = trpc.useUtils();
|
|
||||||
|
|
||||||
const bookDate = trpc.polls.book.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.polls.invalidate();
|
|
||||||
redirectBackToPoll();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
<Trans i18nKey="finalize" />
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
<Trans
|
|
||||||
i18nKey="finalizeDescription"
|
|
||||||
defaults="Select a final date for your event."
|
|
||||||
/>
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<FinalizePollForm
|
|
||||||
name="finalize"
|
|
||||||
onSubmit={(data) => {
|
|
||||||
if (plan === "paid") {
|
|
||||||
bookDate.mutateAsync({
|
|
||||||
pollId: poll.id,
|
|
||||||
optionId: data.selectedOptionId,
|
|
||||||
notify: data.notify,
|
|
||||||
});
|
|
||||||
|
|
||||||
posthog?.capture("finalize poll", {
|
|
||||||
pollId: poll.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<Button onClick={redirectBackToPoll}>
|
|
||||||
<Trans i18nKey="cancel" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
loading={bookDate.isLoading}
|
|
||||||
form="finalize"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
<Trans i18nKey="finalize" />
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Page: NextPageWithLayout = () => {
|
|
||||||
return (
|
|
||||||
<PayWall>
|
|
||||||
<FinalizationForm />
|
|
||||||
</PayWall>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Badge } from "@rallly/ui/badge";
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { DialogClose, DialogContent } from "@rallly/ui/dialog";
|
||||||
|
import { m } from "framer-motion";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { Trans } from "@/components/trans";
|
||||||
|
import { usePlan } from "@/contexts/plan";
|
||||||
|
|
||||||
|
export function PayWallDialogContent({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const plan = usePlan();
|
||||||
|
|
||||||
|
if (plan === "free") {
|
||||||
|
return (
|
||||||
|
<DialogContent size="sm">
|
||||||
|
<div>
|
||||||
|
<m.div
|
||||||
|
transition={{
|
||||||
|
delay: 0.5,
|
||||||
|
duration: 0.4,
|
||||||
|
type: "spring",
|
||||||
|
bounce: 0.5,
|
||||||
|
}}
|
||||||
|
initial={{ opacity: 0, y: -50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Badge variant="primary">
|
||||||
|
<Trans i18nKey="planPro" />
|
||||||
|
</Badge>
|
||||||
|
</m.div>
|
||||||
|
<div className="mt-4 space-y-6">
|
||||||
|
<div className="space-y-2 text-center">
|
||||||
|
<h2 className="text-center text-xl font-bold">
|
||||||
|
<Trans defaults="Pro Feature" i18nKey="proFeature" />
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mx-auto max-w-xs text-center text-sm leading-relaxed">
|
||||||
|
<Trans
|
||||||
|
i18nKey="upgradeOverlaySubtitle2"
|
||||||
|
defaults="Please upgrade to a paid plan to use this feature. This is how we keep the lights on :)"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<Button variant="primary" asChild>
|
||||||
|
<Link href="/settings/billing">
|
||||||
|
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button>
|
||||||
|
<Trans i18nKey="notToday" defaults="Not Today" />
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
|
@ -222,7 +222,7 @@ function DiscussionInner() {
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
onSelect={() => {
|
onClick={() => {
|
||||||
deleteComment.mutate({
|
deleteComment.mutate({
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { useDialog } from "@rallly/ui/dialog";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
@ -25,14 +26,99 @@ import {
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { DuplicateDialog } from "@/app/[locale]/poll/[urlId]/duplicate-dialog";
|
||||||
|
import { trpc } from "@/app/providers";
|
||||||
|
import { FinalizePollDialog } from "@/components/poll/manage-poll/finalize-poll-dialog";
|
||||||
import { ProFeatureBadge } from "@/components/pro-feature-badge";
|
import { ProFeatureBadge } from "@/components/pro-feature-badge";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
|
||||||
|
|
||||||
import { DeletePollDialog } from "./manage-poll/delete-poll-dialog";
|
import { DeletePollDialog } from "./manage-poll/delete-poll-dialog";
|
||||||
import { useCsvExporter } from "./manage-poll/use-csv-exporter";
|
import { useCsvExporter } from "./manage-poll/use-csv-exporter";
|
||||||
|
|
||||||
|
function PauseResumeToggle() {
|
||||||
|
const poll = usePoll();
|
||||||
|
const queryClient = trpc.useUtils();
|
||||||
|
const resume = trpc.polls.resume.useMutation({
|
||||||
|
onSuccess: (_data, vars) => {
|
||||||
|
queryClient.polls.get.setData({ urlId: vars.pollId }, (oldData) => {
|
||||||
|
if (!oldData) return oldData;
|
||||||
|
return {
|
||||||
|
...oldData,
|
||||||
|
status: "live",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
queryClient.polls.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pause = trpc.polls.pause.useMutation({
|
||||||
|
onSuccess: (_data, vars) => {
|
||||||
|
queryClient.polls.get.setData({ urlId: vars.pollId }, (oldData) => {
|
||||||
|
if (!oldData) return oldData;
|
||||||
|
return {
|
||||||
|
...oldData,
|
||||||
|
status: "paused",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
queryClient.polls.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (poll.status === "paused") {
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
resume.mutate(
|
||||||
|
{ pollId: poll.id },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.polls.get.setData({ urlId: poll.id }, (oldData) => {
|
||||||
|
if (!oldData) return oldData;
|
||||||
|
return {
|
||||||
|
...oldData,
|
||||||
|
status: "live",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon>
|
||||||
|
<PlayIcon />
|
||||||
|
</Icon>
|
||||||
|
<Trans i18nKey="resumePoll" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
pause.mutate(
|
||||||
|
{ pollId: poll.id },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.polls.get.setData({ urlId: poll.id }, (oldData) => {
|
||||||
|
if (!oldData) return oldData;
|
||||||
|
return {
|
||||||
|
...oldData,
|
||||||
|
status: "paused",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon>
|
||||||
|
<PauseIcon />
|
||||||
|
</Icon>
|
||||||
|
<Trans i18nKey="pausePoll" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ManagePoll: React.FunctionComponent<{
|
const ManagePoll: React.FunctionComponent<{
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}> = ({ disabled }) => {
|
}> = ({ disabled }) => {
|
||||||
|
@ -54,42 +140,10 @@ const ManagePoll: React.FunctionComponent<{
|
||||||
queryClient.polls.invalidate();
|
queryClient.polls.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const pause = trpc.polls.pause.useMutation({
|
|
||||||
onMutate: () => {
|
|
||||||
queryClient.polls.get.setData({ urlId: poll.id }, (oldPoll) => {
|
|
||||||
if (!oldPoll) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...oldPoll,
|
|
||||||
closed: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.polls.invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const resume = trpc.polls.resume.useMutation({
|
|
||||||
onMutate: () => {
|
|
||||||
queryClient.polls.get.setData({ urlId: poll.id }, (oldPoll) => {
|
|
||||||
if (!oldPoll) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...oldPoll,
|
|
||||||
closed: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.polls.invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [showDeletePollDialog, setShowDeletePollDialog] = React.useState(false);
|
const [showDeletePollDialog, setShowDeletePollDialog] = React.useState(false);
|
||||||
|
const duplicateDialog = useDialog();
|
||||||
|
const finalizeDialog = useDialog();
|
||||||
const { exportToCsv } = useCsvExporter();
|
const { exportToCsv } = useCsvExporter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -101,14 +155,16 @@ const ManagePoll: React.FunctionComponent<{
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Trans i18nKey="manage" />
|
<Trans i18nKey="manage" />
|
||||||
<ChevronDownIcon className="size-4" />
|
<Icon>
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<>
|
<>
|
||||||
{poll.status === "finalized" ? (
|
{poll.status === "finalized" ? (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => {
|
onClick={() => {
|
||||||
reopen.mutate({ pollId: poll.id });
|
reopen.mutate({ pollId: poll.id });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -119,37 +175,19 @@ const ManagePoll: React.FunctionComponent<{
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem asChild disabled={!!poll.event}>
|
<DropdownMenuItem
|
||||||
<Link href={`/poll/${poll.id}/finalize`}>
|
disabled={!!poll.event}
|
||||||
<DropdownMenuItemIconLabel icon={CalendarCheck2Icon}>
|
onClick={() => {
|
||||||
<Trans i18nKey="finishPoll" defaults="Finalize" />
|
finalizeDialog.trigger();
|
||||||
<ProFeatureBadge />
|
}}
|
||||||
</DropdownMenuItemIconLabel>
|
>
|
||||||
</Link>
|
<Icon>
|
||||||
|
<CalendarCheck2Icon />
|
||||||
|
</Icon>
|
||||||
|
<Trans i18nKey="finishPoll" defaults="Finalize" />
|
||||||
|
<ProFeatureBadge />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{poll.status === "live" ? (
|
<PauseResumeToggle />
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
pause.mutate({ pollId: poll.id });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon>
|
|
||||||
<PauseIcon />
|
|
||||||
</Icon>
|
|
||||||
<Trans i18nKey="pausePoll" defaults="Pause" />
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
resume.mutate({ pollId: poll.id });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon>
|
|
||||||
<PlayIcon />
|
|
||||||
</Icon>
|
|
||||||
<Trans i18nKey="resumePoll" defaults="Resume" />
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -176,18 +214,21 @@ const ManagePoll: React.FunctionComponent<{
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={exportToCsv}>
|
<DropdownMenuItem onClick={exportToCsv}>
|
||||||
<DropdownMenuItemIconLabel icon={DownloadIcon}>
|
<DropdownMenuItemIconLabel icon={DownloadIcon}>
|
||||||
<Trans i18nKey="exportToCsv" />
|
<Trans i18nKey="exportToCsv" />
|
||||||
</DropdownMenuItemIconLabel>
|
</DropdownMenuItemIconLabel>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem
|
||||||
<Link href={`/poll/${poll.id}/duplicate`}>
|
onClick={() => {
|
||||||
<DropdownMenuItemIconLabel icon={CopyIcon}>
|
duplicateDialog.trigger();
|
||||||
<Trans i18nKey="duplicate" defaults="Duplicate" />
|
}}
|
||||||
<ProFeatureBadge />
|
>
|
||||||
</DropdownMenuItemIconLabel>
|
<DropdownMenuItemIconLabel icon={CopyIcon}>
|
||||||
</Link>
|
<Trans i18nKey="duplicate" defaults="Duplicate" />
|
||||||
|
<ProFeatureBadge />
|
||||||
|
</DropdownMenuItemIconLabel>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
@ -196,8 +237,8 @@ const ManagePoll: React.FunctionComponent<{
|
||||||
setShowDeletePollDialog(true);
|
setShowDeletePollDialog(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-4" />
|
<TrashIcon className="size-4 opacity-75" />
|
||||||
<Trans i18nKey="deletePoll" />
|
<Trans i18nKey="delete" />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
@ -206,6 +247,12 @@ const ManagePoll: React.FunctionComponent<{
|
||||||
open={showDeletePollDialog}
|
open={showDeletePollDialog}
|
||||||
onOpenChange={setShowDeletePollDialog}
|
onOpenChange={setShowDeletePollDialog}
|
||||||
/>
|
/>
|
||||||
|
<DuplicateDialog
|
||||||
|
pollId={poll.id}
|
||||||
|
pollTitle={poll.title}
|
||||||
|
{...duplicateDialog.dialogProps}
|
||||||
|
/>
|
||||||
|
<FinalizePollDialog {...finalizeDialog.dialogProps} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogProps,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@rallly/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
@ -14,7 +24,9 @@ import React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { DateIcon } from "@/components/date-icon";
|
import { PayWallDialogContent } from "@/app/[locale]/poll/[urlId]/pay-wall-dialog-content";
|
||||||
|
import { trpc } from "@/app/providers";
|
||||||
|
import { DateIconInner } from "@/components/date-icon";
|
||||||
import { useParticipants } from "@/components/participants-provider";
|
import { useParticipants } from "@/components/participants-provider";
|
||||||
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
import { ConnectedScoreSummary } from "@/components/poll/score-summary";
|
||||||
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
import { VoteSummaryProgressBar } from "@/components/vote-summary-progress-bar";
|
||||||
|
@ -58,7 +70,12 @@ const useScoreByOptionId = () => {
|
||||||
}, [responses, options]);
|
}, [responses, options]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageSize = 5;
|
function DateIcon({ start }: { start: Date }) {
|
||||||
|
const poll = usePoll();
|
||||||
|
const { adjustTimeZone } = useDayjs();
|
||||||
|
const d = adjustTimeZone(start, !poll.timeZone);
|
||||||
|
return <DateIconInner dow={d.format("ddd")} day={d.format("D")} />;
|
||||||
|
}
|
||||||
|
|
||||||
export const FinalizePollForm = ({
|
export const FinalizePollForm = ({
|
||||||
name,
|
name,
|
||||||
|
@ -68,7 +85,6 @@ export const FinalizePollForm = ({
|
||||||
onSubmit?: (data: FinalizeFormData) => void;
|
onSubmit?: (data: FinalizeFormData) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const poll = usePoll();
|
const poll = usePoll();
|
||||||
const [max, setMax] = React.useState(pageSize);
|
|
||||||
|
|
||||||
const { adjustTimeZone } = useDayjs();
|
const { adjustTimeZone } = useDayjs();
|
||||||
const scoreByOptionId = useScoreByOptionId();
|
const scoreByOptionId = useScoreByOptionId();
|
||||||
|
@ -110,7 +126,9 @@ export const FinalizePollForm = ({
|
||||||
<form
|
<form
|
||||||
id={name}
|
id={name}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
onSubmit={form.handleSubmit((data) => onSubmit?.(data))}
|
onSubmit={form.handleSubmit((data) => {
|
||||||
|
onSubmit?.(data);
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
@ -125,9 +143,9 @@ export const FinalizePollForm = ({
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
className="grid gap-2"
|
className="grid max-h-96 gap-2 overflow-y-auto rounded-lg border bg-gray-100 p-2"
|
||||||
>
|
>
|
||||||
{options.slice(0, max).map((option) => {
|
{options.map((option) => {
|
||||||
const start = adjustTimeZone(
|
const start = adjustTimeZone(
|
||||||
option.startTime,
|
option.startTime,
|
||||||
!poll.timeZone,
|
!poll.timeZone,
|
||||||
|
@ -143,25 +161,17 @@ export const FinalizePollForm = ({
|
||||||
key={option.id}
|
key={option.id}
|
||||||
htmlFor={option.id}
|
htmlFor={option.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex select-none items-center gap-4 rounded-md border bg-white p-3 text-base",
|
"group flex select-none items-start gap-4 rounded-lg border bg-white p-3 text-base",
|
||||||
field.value === option.id
|
field.value === option.id ? "" : "",
|
||||||
? "bg-primary-50 border-primary"
|
|
||||||
: "hover:bg-gray-50",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="hidden">
|
<RadioGroupItem id={option.id} value={option.id} />
|
||||||
<RadioGroupItem id={option.id} value={option.id} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<DateIcon date={start} />
|
|
||||||
</div>
|
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="flex">
|
<div className="flex gap-x-4">
|
||||||
|
<DateIcon start={option.start} />
|
||||||
<div className="grow whitespace-nowrap">
|
<div className="grow whitespace-nowrap">
|
||||||
<div className="text-sm font-semibold">
|
<div className="text-sm font-medium">
|
||||||
{option.duration > 0
|
{start.format("LL")}
|
||||||
? start.format("LL")
|
|
||||||
: start.format("LL")}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground text-sm">
|
||||||
{option.duration > 0 ? (
|
{option.duration > 0 ? (
|
||||||
|
@ -180,7 +190,7 @@ export const FinalizePollForm = ({
|
||||||
<ConnectedScoreSummary optionId={option.id} />
|
<ConnectedScoreSummary optionId={option.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-4">
|
||||||
<VoteSummaryProgressBar
|
<VoteSummaryProgressBar
|
||||||
{...scoreByOptionId[option.id]}
|
{...scoreByOptionId[option.id]}
|
||||||
total={participants.length}
|
total={participants.length}
|
||||||
|
@ -192,19 +202,6 @@ export const FinalizePollForm = ({
|
||||||
})}
|
})}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{max < options.length ? (
|
|
||||||
<div className="absolute bottom-0 mt-2 w-full bg-gradient-to-t from-white via-white to-white/10 px-3 py-8">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
setMax((oldMax) => oldMax + pageSize);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="showMore" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -268,3 +265,58 @@ export const FinalizePollForm = ({
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function FinalizePollDialog(props: DialogProps) {
|
||||||
|
const poll = usePoll();
|
||||||
|
const queryClient = trpc.useUtils();
|
||||||
|
const scheduleEvent = trpc.polls.book.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<PayWallDialogContent>
|
||||||
|
<DialogContent size="2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans i18nKey="finalize" />
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans
|
||||||
|
i18nKey="finalizeDescription"
|
||||||
|
defaults="Select a final date for your event."
|
||||||
|
/>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FinalizePollForm
|
||||||
|
name="finalize-form"
|
||||||
|
onSubmit={(data) => {
|
||||||
|
scheduleEvent.mutate({
|
||||||
|
pollId: poll.id,
|
||||||
|
optionId: data.selectedOptionId,
|
||||||
|
notify: data.notify,
|
||||||
|
});
|
||||||
|
props.onOpenChange?.(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button>
|
||||||
|
<Trans i18nKey="cancel" />
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
loading={scheduleEvent.isLoading}
|
||||||
|
type="submit"
|
||||||
|
form="finalize-form"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="finalize" />
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</PayWallDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -168,7 +168,7 @@ export const UserDropdown = ({ className }: { className?: string }) => {
|
||||||
</IfGuest>
|
</IfGuest>
|
||||||
<IfAuthenticated>
|
<IfAuthenticated>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => {
|
onClick={() => {
|
||||||
logout();
|
logout();
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2"
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const VoteSummaryProgressBar = (props: {
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className="h-full bg-green-500 hover:opacity-75"
|
className="h-full bg-green-500 opacity-75 hover:opacity-100"
|
||||||
style={{
|
style={{
|
||||||
width: (props.yes.length / props.total) * 100 + "%",
|
width: (props.yes.length / props.total) * 100 + "%",
|
||||||
}}
|
}}
|
||||||
|
@ -44,7 +44,7 @@ export const VoteSummaryProgressBar = (props: {
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className="h-full bg-amber-400 hover:opacity-75"
|
className="h-full bg-amber-400 opacity-75 hover:opacity-100"
|
||||||
style={{
|
style={{
|
||||||
width: (props.ifNeedBe.length / props.total) * 100 + "%",
|
width: (props.ifNeedBe.length / props.total) * 100 + "%",
|
||||||
}}
|
}}
|
||||||
|
@ -57,7 +57,7 @@ export const VoteSummaryProgressBar = (props: {
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gray-300 hover:opacity-75"
|
className="h-full bg-gray-300 opacity-75 hover:opacity-100"
|
||||||
style={{
|
style={{
|
||||||
width: (props.no.length / props.total) * 100 + "%",
|
width: (props.no.length / props.total) * 100 + "%",
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { FileSearchIcon } from "lucide-react";
|
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import ErrorPage from "@/components/error-page";
|
|
||||||
import { NextPageWithLayout } from "@/types";
|
|
||||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
|
||||||
|
|
||||||
const Custom404: NextPageWithLayout = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<ErrorPage
|
|
||||||
icon={FileSearchIcon}
|
|
||||||
title={t("errors_notFoundTitle")}
|
|
||||||
description={t("errors_notFoundDescription")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStaticProps = getStaticTranslations;
|
|
||||||
|
|
||||||
export default Custom404;
|
|
|
@ -35,7 +35,7 @@ test.describe.serial(() => {
|
||||||
const manageButton = page.getByText("Manage");
|
const manageButton = page.getByText("Manage");
|
||||||
await manageButton.waitFor();
|
await manageButton.waitFor();
|
||||||
await manageButton.click();
|
await manageButton.click();
|
||||||
await page.click("text=Delete poll");
|
await page.click("text=Delete");
|
||||||
|
|
||||||
const deletePollDialog = page.getByRole("dialog");
|
const deletePollDialog = page.getByRole("dialog");
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"animate-in data-[state=open]:fade-in data-[state=open]:slide-in-from-top-8 shadow-huge z-50 grid w-full translate-y-0 gap-4 overflow-hidden rounded-md bg-gray-50 p-4 lg:p-5",
|
"animate-in data-[state=open]:fade-in data-[state=open]:slide-in-from-top-8 shadow-huge z-50 grid w-full translate-y-0 gap-4 overflow-hidden rounded-md bg-gray-50 p-4",
|
||||||
{
|
{
|
||||||
"sm:max-w-sm": size === "sm",
|
"sm:max-w-sm": size === "sm",
|
||||||
"sm:max-w-md": size === "md",
|
"sm:max-w-md": size === "md",
|
||||||
|
@ -96,7 +96,7 @@ const DialogFooter = ({
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted-background -mx-5 -mb-5 flex flex-col-reverse gap-2 border-t px-3 py-2.5 sm:flex-row sm:justify-end sm:rounded-b-md",
|
"bg-muted-background -mx-4 -mb-4 flex flex-col-reverse gap-2 border-t px-3 py-2.5 sm:flex-row sm:justify-end sm:rounded-b-md",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export type InputProps = Omit<
|
||||||
|
|
||||||
const inputVariants = cva(
|
const inputVariants = cva(
|
||||||
cn(
|
cn(
|
||||||
"focus:visible:border-primary-400 focus:visible:ring-primary-200 focus:visible:ring-2",
|
"focus-visible:border-primary-400 focus-visible:ring-offset-1 focus-visible:outline-none focus-visible:ring-primary-200 focus-visible:ring-1",
|
||||||
"border-input placeholder:text-muted-foreground h-9 rounded border bg-gray-50 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50",
|
"border-input placeholder:text-muted-foreground h-9 rounded border bg-gray-50 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input placeholder:text-muted-foreground flex min-h-[80px] rounded border bg-gray-50 px-2 py-2 text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
"border-input placeholder:text-muted-foreground flex min-h-[80px] rounded border bg-gray-50 px-2 py-2 text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
"focus-visible:ring-offset-input-background focus-visible:ring-1 focus-visible:ring-offset-1",
|
"focus-visible:ring-offset-input-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1",
|
||||||
"focus-visible:border-primary-400 focus-visible:ring-primary-100",
|
"focus-visible:border-primary-400 focus-visible:ring-primary-100",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue