mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-15 01:46:47 +02:00
UI refresh (#476)
This commit is contained in:
parent
e6a13c235d
commit
bba880ff4c
53 changed files with 1198 additions and 1014 deletions
|
@ -20,6 +20,7 @@
|
|||
"@next/bundle-analyzer": "^12.3.4",
|
||||
"@next/font": "^13.1.3",
|
||||
"@prisma/client": "^4.9.0",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@sentry/nextjs": "^7.33.0",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
|
@ -60,6 +61,7 @@
|
|||
"spacetime": "^7.1.4",
|
||||
"superjson": "^1.9.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"timezone-soft": "^1.3.1",
|
||||
"typescript": "^4.9.4",
|
||||
"zod": "^3.20.2"
|
||||
|
|
|
@ -67,7 +67,10 @@
|
|||
"name": "Name",
|
||||
"namePlaceholder": "Jessie Smith",
|
||||
"new": "New",
|
||||
"adminPollTitle": "{{title}}: Admin",
|
||||
"newPoll": "New poll",
|
||||
"createNew": "Create new",
|
||||
"home": "Home",
|
||||
"next": "Next",
|
||||
"nextMonth": "Next month",
|
||||
"no": "No",
|
||||
|
|
134
src/components/admin-control.tsx
Normal file
134
src/components/admin-control.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useMount } from "react-use";
|
||||
|
||||
import { Button } from "@/components/button";
|
||||
import Share from "@/components/icons/share.svg";
|
||||
|
||||
import { trpc, trpcNext } from "../utils/trpc";
|
||||
import { useParticipants } from "./participants-provider";
|
||||
import ManagePoll from "./poll/manage-poll";
|
||||
import { useUpdatePollMutation } from "./poll/mutations";
|
||||
import NotificationsToggle from "./poll/notifications-toggle";
|
||||
import { UnverifiedPollNotice } from "./poll/unverified-poll-notice";
|
||||
import { usePoll } from "./poll-context";
|
||||
import Sharing from "./sharing";
|
||||
import { useUser } from "./user-provider";
|
||||
|
||||
export const AdminControls = (props: { children?: React.ReactNode }) => {
|
||||
const { poll, urlId } = usePoll();
|
||||
const { t } = useTranslation("app");
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryClient = trpcNext.useContext();
|
||||
|
||||
const session = useUser();
|
||||
|
||||
const { mutate: updatePollMutation } = useUpdatePollMutation();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (router.query.unsubscribe) {
|
||||
updatePollMutation(
|
||||
{ urlId: urlId, notifications: false },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("notificationsDisabled"));
|
||||
posthog.capture("unsubscribed from notifications");
|
||||
},
|
||||
},
|
||||
);
|
||||
router.replace(`/admin/${router.query.urlId}`, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
}
|
||||
}, [urlId, router, updatePollMutation, t]);
|
||||
|
||||
const verifyEmail = trpc.useMutation(["polls.verification.verify"], {
|
||||
onSuccess: () => {
|
||||
toast.success(t("pollHasBeenVerified"));
|
||||
queryClient.poll.invalidate();
|
||||
session.refresh();
|
||||
posthog.capture("verified email");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("linkHasExpired"));
|
||||
},
|
||||
onSettled: () => {
|
||||
router.replace(`/admin/${router.query.urlId}`, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { participants } = useParticipants();
|
||||
|
||||
const [isSharingVisible, setIsSharingVisible] = React.useState(
|
||||
participants.length === 0,
|
||||
);
|
||||
|
||||
useMount(() => {
|
||||
const { code } = router.query;
|
||||
if (typeof code === "string" && !poll.verified) {
|
||||
verifyEmail.mutate({ code, pollId: poll.id });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="mb-4 flex justify-end">
|
||||
<div className="flex gap-2">
|
||||
<NotificationsToggle />
|
||||
<ManagePoll placement="bottom-end" />
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Share />}
|
||||
onClick={() => {
|
||||
setIsSharingVisible(!isSharingVisible);
|
||||
}}
|
||||
>
|
||||
{t("share")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence initial={false}>
|
||||
{isSharingVisible ? (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
height: 0,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
className="rounded-md border bg-white shadow-sm"
|
||||
>
|
||||
<Sharing
|
||||
className="p-4"
|
||||
onHide={() => {
|
||||
setIsSharingVisible(false);
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<motion.div className="relative z-10 space-y-4" layout="position">
|
||||
{poll.verified === false ? <UnverifiedPollNotice /> : null}
|
||||
{props.children}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -4,11 +4,11 @@ import Logo from "~/public/logo.svg";
|
|||
|
||||
export const AuthLayout = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="h-full bg-slate-500/10 p-8">
|
||||
<div className="h-full bg-gray-100 p-3 sm:p-8">
|
||||
<div className="flex h-full items-start justify-center">
|
||||
<div className="w-[480px] max-w-full overflow-hidden rounded-lg border bg-white shadow-sm">
|
||||
<div className="bg-pattern border-b border-t-4 border-t-primary-500 bg-slate-500/5 p-4 text-center sm:p-8">
|
||||
<Logo className="inline-block h-6 text-primary-500 sm:h-7" />
|
||||
<Logo className="inline-block h-7 text-primary-500" />
|
||||
</div>
|
||||
<div className="p-4 sm:p-6">{children}</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import Logo from "~/public/logo.svg";
|
||||
|
||||
import { Logo } from "../logo";
|
||||
import { useModalContext } from "../modal/modal-provider";
|
||||
import { useUser } from "../user-provider";
|
||||
import { LoginForm, RegisterForm } from "./login-form";
|
||||
|
@ -16,7 +15,7 @@ export const LoginModal: React.VoidFunctionComponent<{
|
|||
return (
|
||||
<div className="w-[420px] max-w-full overflow-hidden rounded-lg bg-white shadow-sm">
|
||||
<div className="bg-pattern border-b border-t-4 border-t-primary-500 bg-slate-500/5 p-4 text-center sm:p-8">
|
||||
<Logo className="inline-block h-6 text-primary-500 sm:h-7" />
|
||||
<Logo className="text-2xl" />
|
||||
</div>
|
||||
<div className="p-4 sm:p-6">
|
||||
{hasAccount ? (
|
||||
|
|
|
@ -56,6 +56,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
className,
|
||||
)}
|
||||
{...passThroughProps}
|
||||
role="button"
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
{loading ? (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import posthog from "posthog-js";
|
||||
|
@ -100,7 +99,7 @@ const Page: NextPage<CreatePollPageProps> = ({
|
|||
optionsView: formData?.options?.view,
|
||||
});
|
||||
setPersistedFormData(initialNewEventData);
|
||||
router.replace(`/admin/${res.urlId}?sharing=true`);
|
||||
router.replace(`/admin/${res.urlId}`);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -146,23 +145,22 @@ const Page: NextPage<CreatePollPageProps> = ({
|
|||
|
||||
return (
|
||||
<StandardLayout>
|
||||
<Head>
|
||||
<title>{formData?.eventDetails?.title ?? t("newPoll")}</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Head>
|
||||
<div className="max-w-full py-4 md:px-3 lg:px-6">
|
||||
<div className="mx-auto w-fit max-w-full lg:mx-0">
|
||||
<div className="mb-4 flex items-center justify-center space-x-4 px-4 lg:justify-start">
|
||||
<h1 className="m-0">{t("newPoll")}</h1>
|
||||
<div className="max-w-full px-3 pb-3 sm:p-4">
|
||||
<div className="max-w-full">
|
||||
<div className="max-w-full overflow-hidden rounded-lg border bg-white shadow-sm">
|
||||
<div className="flex justify-between border-b p-4">
|
||||
<h1 className="m-0 text-xl font-semibold text-slate-800">
|
||||
{t("createNew")}
|
||||
</h1>
|
||||
<Steps current={currentStepIndex} total={steps.length} />
|
||||
</div>
|
||||
<div className="overflow-hidden border-t border-b bg-white shadow-sm md:rounded-lg md:border">
|
||||
<div className="">
|
||||
{(() => {
|
||||
switch (currentStepName) {
|
||||
case "eventDetails":
|
||||
return (
|
||||
<PollDetailsForm
|
||||
className="max-w-full px-4 pt-4"
|
||||
className="max-w-full p-3 sm:p-4"
|
||||
name={currentStepName}
|
||||
defaultValues={formData?.eventDetails}
|
||||
onSubmit={handleSubmit}
|
||||
|
@ -183,7 +181,7 @@ const Page: NextPage<CreatePollPageProps> = ({
|
|||
case "userDetails":
|
||||
return (
|
||||
<UserDetailsForm
|
||||
className="grow px-4 pt-4"
|
||||
className="grow p-4"
|
||||
name={currentStepName}
|
||||
defaultValues={formData?.userDetails}
|
||||
onSubmit={handleSubmit}
|
||||
|
@ -220,6 +218,7 @@ const Page: NextPage<CreatePollPageProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StandardLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@ export interface DateCardProps {
|
|||
annotation?: React.ReactNode;
|
||||
day: string;
|
||||
month: string;
|
||||
dow: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
@ -13,28 +12,24 @@ const DateCard: React.VoidFunctionComponent<DateCardProps> = ({
|
|||
annotation,
|
||||
className,
|
||||
day,
|
||||
dow,
|
||||
month,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative mt-1 inline-block h-14 w-14 rounded-md border bg-white text-center shadow-md shadow-slate-100",
|
||||
"relative inline-flex h-14 w-14 items-center justify-center rounded-md border border-slate-200 bg-white p-1 text-center",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{annotation ? (
|
||||
<div className="absolute -top-3 -right-3 z-20">{annotation}</div>
|
||||
) : null}
|
||||
<div className="relative -mt-2 mb-[-1px] text-xs text-slate-400">
|
||||
<span className="relative z-10 inline-block px-1 after:absolute after:left-0 after:top-[7px] after:-z-10 after:inline-block after:w-full after:border-t after:border-white after:content-['']">
|
||||
{dow.substring(0, 3)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="-mb-1 text-center text-lg text-rose-500">{day}</div>
|
||||
<div className="text-center text-xs font-semibold uppercase text-gray-800">
|
||||
<div>
|
||||
<div className="text-center text-xs font-semibold uppercase leading-normal text-slate-500">
|
||||
{month}
|
||||
</div>
|
||||
<div className="text-center text-xl font-bold leading-none">{day}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import clsx from "clsx";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import * as React from "react";
|
||||
|
@ -81,36 +80,24 @@ const Discussion: React.VoidFunctionComponent = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden border-t border-b shadow-sm md:rounded-lg md:border">
|
||||
<div className="border-b bg-white px-4 py-2">
|
||||
<div className="overflow-hidden rounded-md border shadow-sm">
|
||||
<div className="border-b bg-white p-3">
|
||||
<div className="font-medium">{t("comments")}</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx({
|
||||
"space-y-3 border-b bg-slate-50 p-4": comments.length > 0,
|
||||
"bg-pattern space-y-3 border-b p-3": comments.length > 0,
|
||||
})}
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
{comments.map((comment) => {
|
||||
const canDelete =
|
||||
admin || session.ownsObject(comment) || isUnclaimed(comment);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layoutId={comment.id}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="flex"
|
||||
key={comment.id}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, y: 10 }}
|
||||
animate={{ scale: 1, y: 0 }}
|
||||
exit={{ scale: 0.8 }}
|
||||
<div className="flex" key={comment.id}>
|
||||
<div
|
||||
data-testid="comment"
|
||||
className="w-fit rounded-xl border bg-white px-3 py-2 shadow-sm"
|
||||
className="w-fit rounded-md border bg-white px-3 py-2 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<UserAvatar
|
||||
|
@ -144,14 +131,13 @@ const Discussion: React.VoidFunctionComponent = () => {
|
|||
<div className="w-fit whitespace-pre-wrap">
|
||||
<TruncatedLinkify>{comment.content}</TruncatedLinkify>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<form
|
||||
className="bg-white p-4"
|
||||
className="bg-white p-3"
|
||||
onSubmit={handleSubmit(async ({ authorName, content }) => {
|
||||
await addComment.mutateAsync({ authorName, content, pollId });
|
||||
reset({ authorName, content: "" });
|
||||
|
|
|
@ -8,14 +8,11 @@ import {
|
|||
} from "@floating-ui/react-dom-interactions";
|
||||
import { Menu } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import { motion } from "framer-motion";
|
||||
import * as React from "react";
|
||||
|
||||
import { transformOriginByPlacement } from "@/utils/constants";
|
||||
import { stopPropagation } from "@/utils/stop-propagation";
|
||||
|
||||
const MotionMenuItems = motion(Menu.Items);
|
||||
|
||||
export interface DropdownProps {
|
||||
trigger?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
|
@ -55,13 +52,9 @@ const Dropdown: React.VoidFunctionComponent<DropdownProps> = ({
|
|||
</Menu.Button>
|
||||
<FloatingPortal>
|
||||
{open ? (
|
||||
<MotionMenuItems
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, scale: 0.9, y: -10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: -10 }}
|
||||
<Menu.Items
|
||||
className={clsx(
|
||||
"z-50 divide-gray-100 rounded-md bg-white p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none",
|
||||
"z-50 animate-popIn divide-gray-100 rounded-md bg-white p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none",
|
||||
animationOrigin,
|
||||
)}
|
||||
onMouseDown={stopPropagation}
|
||||
|
@ -73,7 +66,7 @@ const Dropdown: React.VoidFunctionComponent<DropdownProps> = ({
|
|||
}}
|
||||
>
|
||||
{children}
|
||||
</MotionMenuItems>
|
||||
</Menu.Items>
|
||||
) : null}
|
||||
</FloatingPortal>
|
||||
</>
|
||||
|
|
|
@ -35,12 +35,7 @@ const ErrorPage: React.VoidFunctionComponent<ComponentProps> = ({
|
|||
</div>
|
||||
<p>{description}</p>
|
||||
<div className="flex justify-center space-x-3">
|
||||
<Link
|
||||
href="/"
|
||||
passHref={true}
|
||||
className="btn-default"
|
||||
legacyBehavior
|
||||
>
|
||||
<Link href="/" passHref={true} className="btn-default">
|
||||
{t("goToHome")}
|
||||
</Link>
|
||||
<Button icon={<Chat />} onClick={showCrispChat}>
|
||||
|
|
|
@ -36,7 +36,6 @@ export const PollDetailsForm: React.VoidFunctionComponent<
|
|||
<form
|
||||
id={name}
|
||||
className={clsx("max-w-full", className)}
|
||||
style={{ width: 500 }}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="formField">
|
||||
|
@ -61,7 +60,7 @@ export const PollDetailsForm: React.VoidFunctionComponent<
|
|||
{...register("location")}
|
||||
/>
|
||||
</div>
|
||||
<div className="formField">
|
||||
<div>
|
||||
<label htmlFor="description">{t("description")}</label>
|
||||
<textarea
|
||||
id="description"
|
||||
|
|
|
@ -81,7 +81,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
|
||||
return (
|
||||
<div className="overflow-hidden lg:flex">
|
||||
<div className="border-b p-4 lg:w-[440px] lg:border-r lg:border-b-0">
|
||||
<div className="border-b p-3 sm:p-4 lg:w-[440px] lg:border-r lg:border-b-0">
|
||||
<div>
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3 flex items-center justify-center space-x-4">
|
||||
|
@ -162,7 +162,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
"text-primary-500": day.today && !day.selected,
|
||||
"border-r": (i + 1) % 7 !== 0,
|
||||
"border-b": i < datepicker.days.length - 7,
|
||||
"font-normal text-white after:absolute after:-z-0 after:h-8 after:w-8 after:animate-popIn after:rounded-full after:bg-green-500 after:content-['']":
|
||||
"font-normal text-white after:absolute after:-z-0 after:h-8 after:w-8 after:rounded-full after:bg-green-500 after:content-['']":
|
||||
day.selected,
|
||||
},
|
||||
)}
|
||||
|
@ -184,7 +184,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
hidden: datepicker.selection.length === 0,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center space-x-3 p-4">
|
||||
<div className="flex items-center space-x-3 p-3 sm:p-4">
|
||||
<div className="grow">
|
||||
<div className="font-medium">{t("specifyTimes")}</div>
|
||||
<div className="text-sm text-slate-400">
|
||||
|
@ -229,7 +229,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow px-4">
|
||||
<div className="grow">
|
||||
{isTimedEvent ? (
|
||||
<div className="divide-y">
|
||||
{Object.keys(optionsByDay)
|
||||
|
@ -239,7 +239,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
return (
|
||||
<div
|
||||
key={dateString}
|
||||
className="space-y-3 py-4 sm:flex sm:space-y-0 sm:space-x-4"
|
||||
className="space-y-3 p-3 sm:flex sm:space-y-0 sm:space-x-4 sm:p-4"
|
||||
>
|
||||
<div>
|
||||
<DateCard
|
||||
|
@ -405,7 +405,7 @@ const MonthCalendar: React.VoidFunctionComponent<DateTimePickerProps> = ({
|
|||
})}
|
||||
</div>
|
||||
) : datepicker.selection.length ? (
|
||||
<div className="grid grid-cols-[repeat(auto-fill,60px)] gap-5 py-4">
|
||||
<div className="grid grid-cols-[repeat(auto-fill,54px)] gap-3 p-3 sm:gap-4 sm:p-4">
|
||||
{datepicker.selection
|
||||
.sort((a, b) => a.getTime() - b.getTime())
|
||||
.map((selectedDate, i) => {
|
||||
|
|
|
@ -143,7 +143,7 @@ const PollOptionsForm: React.VoidFunctionComponent<
|
|||
>
|
||||
{calendarHelpModal}
|
||||
{dateOrTimeRangeModal}
|
||||
<div className="w-full items-center space-y-2 border-b bg-slate-50 py-3 px-4 lg:flex lg:space-y-0 lg:space-x-2">
|
||||
<div className="w-full items-center space-y-2 border-b py-3 px-4 lg:flex lg:space-y-0 lg:space-x-2">
|
||||
<div className="grow">
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
|
@ -32,12 +32,7 @@ export const UserDetailsForm: React.VoidFunctionComponent<
|
|||
}, [watch, onChange]);
|
||||
|
||||
return (
|
||||
<form
|
||||
id={name}
|
||||
className={className}
|
||||
style={{ width: 400 }}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<form id={name} className={className} onSubmit={handleSubmit(onSubmit)}>
|
||||
<h2>{t("yourDetails")}</h2>
|
||||
<div className="formField">
|
||||
<label className="text-slate-500" htmlFor="name">
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useTranslation } from "next-i18next";
|
|||
import * as React from "react";
|
||||
|
||||
import { useDayjs } from "../../utils/dayjs";
|
||||
import DateCard from "../date-card";
|
||||
import { ParticipantRowView } from "../poll/desktop-poll/participant-row";
|
||||
import { ScoreSummary } from "../poll/score-summary";
|
||||
|
||||
|
@ -30,7 +31,7 @@ const participants = [
|
|||
},
|
||||
];
|
||||
|
||||
const options = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"];
|
||||
const options = ["2022-03-14", "2022-03-15", "2022-03-16", "2022-03-17"];
|
||||
|
||||
const PollDemo: React.VoidFunctionComponent = () => {
|
||||
const { t } = useTranslation("homepage");
|
||||
|
@ -65,15 +66,10 @@ const PollDemo: React.VoidFunctionComponent = () => {
|
|||
style={{ width: columnWidth }}
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold leading-9">
|
||||
<div className="text-sm uppercase text-slate-400">
|
||||
{dayjs(d).format("ddd")}
|
||||
</div>
|
||||
<div className="text-2xl">{dayjs(d).format("DD")}</div>
|
||||
<div className="text-xs font-medium uppercase text-slate-400/75">
|
||||
{dayjs(d).format("MMM")}
|
||||
</div>
|
||||
</div>
|
||||
<DateCard
|
||||
day={dayjs(d).format("D")}
|
||||
month={dayjs(d).format("MMM")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ScoreSummary yesScore={score} />
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16m-7 6h7" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
Before Width: | Height: | Size: 219 B After Width: | Height: | Size: 220 B |
|
@ -8,7 +8,7 @@ import DotsVertical from "@/components/icons/dots-vertical.svg";
|
|||
import Github from "@/components/icons/github.svg";
|
||||
import Logo from "~/public/logo.svg";
|
||||
|
||||
import Popover from "../popover";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
import Footer from "./page-layout/footer";
|
||||
|
||||
export interface PageLayoutProps {
|
||||
|
@ -75,15 +75,15 @@ const PageLayout: React.VoidFunctionComponent<PageLayoutProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
<Menu className="hidden items-center space-x-8 md:flex" />
|
||||
<Popover
|
||||
placement="left-start"
|
||||
trigger={
|
||||
<Popover>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<button className="text-gray-400 transition-colors hover:text-primary-500 hover:no-underline hover:underline-offset-2 sm:hidden">
|
||||
<DotsVertical className="w-5" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start">
|
||||
<Menu className="flex flex-col space-y-2" />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="md:min-h-[calc(100vh-460px)]">{children}</div>
|
||||
|
|
16
src/components/layouts/participant-layout.tsx
Normal file
16
src/components/layouts/participant-layout.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { MobileNavigation } from "./standard-layout/mobile-navigation";
|
||||
|
||||
export const ParticipantLayout = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-pattern min-h-full sm:space-y-8">
|
||||
<MobileNavigation />
|
||||
<div className="mx-auto max-w-3xl space-y-4 px-3 pb-8">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,413 +1,19 @@
|
|||
import clsx from "clsx";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
||||
import { LoginLink, useLoginModal } from "@/components/auth/login-modal";
|
||||
import Dropdown, { DropdownItem, DropdownProps } from "@/components/dropdown";
|
||||
import Adjustments from "@/components/icons/adjustments.svg";
|
||||
import Cash from "@/components/icons/cash.svg";
|
||||
import Discord from "@/components/icons/discord.svg";
|
||||
import DotsVertical from "@/components/icons/dots-vertical.svg";
|
||||
import Github from "@/components/icons/github.svg";
|
||||
import Login from "@/components/icons/login.svg";
|
||||
import Logout from "@/components/icons/logout.svg";
|
||||
import Menu from "@/components/icons/menu.svg";
|
||||
import Pencil from "@/components/icons/pencil.svg";
|
||||
import Question from "@/components/icons/question-mark-circle.svg";
|
||||
import Spinner from "@/components/icons/spinner.svg";
|
||||
import Support from "@/components/icons/support.svg";
|
||||
import Twitter from "@/components/icons/twitter.svg";
|
||||
import User from "@/components/icons/user.svg";
|
||||
import UserCircle from "@/components/icons/user-circle.svg";
|
||||
import ModalProvider, {
|
||||
useModalContext,
|
||||
} from "@/components/modal/modal-provider";
|
||||
import Popover from "@/components/popover";
|
||||
import Preferences from "@/components/preferences";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
import { DayjsProvider } from "@/utils/dayjs";
|
||||
import Logo from "~/public/logo.svg";
|
||||
|
||||
const HomeLink = () => {
|
||||
return (
|
||||
<Link href="/">
|
||||
<Logo className="inline-block w-28 text-primary-500 transition-colors active:text-primary-600 lg:w-32" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNavigation: React.VoidFunctionComponent = () => {
|
||||
const { user, isUpdating } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 z-40 flex h-12 w-full shrink-0 items-center justify-between border-b bg-gray-50
|
||||
px-4 lg:hidden"
|
||||
>
|
||||
<div>
|
||||
<HomeLink />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{user ? null : (
|
||||
<LoginLink className="flex w-full cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300">
|
||||
<Login className="h-5 opacity-75" />
|
||||
<span className="inline-block">{t("app:login")}</span>
|
||||
</LoginLink>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{user ? (
|
||||
<UserDropdown
|
||||
placement="bottom-end"
|
||||
trigger={
|
||||
<motion.button
|
||||
initial={{ y: -50, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{
|
||||
y: -50,
|
||||
opacity: 0,
|
||||
}}
|
||||
data-testid="user"
|
||||
className="group inline-flex w-full items-center space-x-2 rounded-lg px-2 py-1 text-left transition-colors hover:bg-slate-500/10 active:bg-slate-500/20"
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-5 animate-spin" />
|
||||
) : (
|
||||
<UserCircle className="w-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden max-w-[120px] truncate font-medium xs:block">
|
||||
{user.shortName}
|
||||
</div>
|
||||
</motion.button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<Popover
|
||||
placement="bottom-end"
|
||||
trigger={
|
||||
<button
|
||||
type="button"
|
||||
className="group flex items-center whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
>
|
||||
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500" />
|
||||
<span className="ml-2 hidden sm:block">
|
||||
{t("app:preferences")}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<Preferences />
|
||||
</Popover>
|
||||
<Popover
|
||||
placement="bottom-end"
|
||||
trigger={
|
||||
<button
|
||||
type="button"
|
||||
className="group flex items-center rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
>
|
||||
<Menu className="w-5 group-hover:text-primary-500" />
|
||||
<span className="ml-2 hidden sm:block">{t("app:menu")}</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<AppMenu className="-m-2" />
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
return (
|
||||
<div className={clsx("space-y-1", className)}>
|
||||
<Link
|
||||
href="/new"
|
||||
className="flex cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 pr-4 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
>
|
||||
<Pencil className="h-5 opacity-75 " />
|
||||
<span className="inline-block">{t("app:newPoll")}</span>
|
||||
</Link>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://support.rallly.co"
|
||||
className="flex cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 pr-4 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Support className="h-5 opacity-75" />
|
||||
<span className="inline-block">{t("common:support")}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserDropdown: React.VoidFunctionComponent<DropdownProps> = ({
|
||||
children,
|
||||
...forwardProps
|
||||
}) => {
|
||||
const { logout, user } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
const { openLoginModal } = useLoginModal();
|
||||
const modalContext = useModalContext();
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dropdown {...forwardProps}>
|
||||
{children}
|
||||
{user.isGuest ? (
|
||||
<DropdownItem
|
||||
icon={Question}
|
||||
label={t("app:whatsThis")}
|
||||
onClick={() => {
|
||||
modalContext.render({
|
||||
showClose: true,
|
||||
content: (
|
||||
<div className="w-96 max-w-full p-6 pt-28">
|
||||
<div className="absolute left-0 -top-8 w-full text-center">
|
||||
<div className="inline-flex h-20 w-20 items-center justify-center rounded-full border-8 border-white bg-gradient-to-b from-purple-400 to-primary-500">
|
||||
<User className="h-7 text-white" />
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="text-lg font-medium leading-snug">
|
||||
Guest
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
{user.shortName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>{t("app:guestSessionNotice")}</p>
|
||||
<div>
|
||||
<a
|
||||
href="https://support.rallly.co/guest-sessions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t("app:guestSessionReadMore")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
overlayClosable: true,
|
||||
footer: null,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{!user.isGuest ? (
|
||||
<DropdownItem
|
||||
href="/profile"
|
||||
icon={User}
|
||||
label={t("app:yourProfile")}
|
||||
/>
|
||||
) : null}
|
||||
{user.isGuest ? (
|
||||
<DropdownItem
|
||||
icon={Login}
|
||||
label={t("app:login")}
|
||||
onClick={openLoginModal}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownItem
|
||||
icon={Logout}
|
||||
label={user.isGuest ? t("app:forgetMe") : t("app:logout")}
|
||||
onClick={() => {
|
||||
if (user?.isGuest) {
|
||||
modalContext.render({
|
||||
title: t("app:areYouSure"),
|
||||
description: t("app:endingGuestSessionNotice"),
|
||||
|
||||
onOk: logout,
|
||||
okButtonProps: {
|
||||
type: "danger",
|
||||
},
|
||||
okText: t("app:endSession"),
|
||||
cancelText: t("app:cancel"),
|
||||
});
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
import { MobileNavigation } from "./standard-layout/mobile-navigation";
|
||||
|
||||
const StandardLayout: React.VoidFunctionComponent<{
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, ...rest }) => {
|
||||
const { user, isUpdating } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
|
||||
return (
|
||||
<ModalProvider>
|
||||
<DayjsProvider>
|
||||
<div
|
||||
className="relative flex min-h-full flex-col bg-gray-50 lg:flex-row"
|
||||
{...rest}
|
||||
>
|
||||
<div className="bg-pattern relative min-h-full" {...rest}>
|
||||
<MobileNavigation />
|
||||
<div className="hidden grow px-4 pt-6 pb-5 lg:block">
|
||||
<div className="sticky top-6 float-right w-48 items-start">
|
||||
<div className="mb-8 px-3">
|
||||
<HomeLink />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Link
|
||||
href="/new"
|
||||
className="group mb-1 flex items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20"
|
||||
>
|
||||
<Pencil className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
||||
<span className="grow text-left">{t("app:newPoll")}</span>
|
||||
</Link>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://support.rallly.co"
|
||||
className="group mb-1 flex items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Support className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
||||
<span className="grow text-left">{t("common:support")}</span>
|
||||
</a>
|
||||
<Popover
|
||||
placement="right-start"
|
||||
trigger={
|
||||
<button className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
|
||||
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
||||
<span className="grow text-left">
|
||||
{t("app:preferences")}
|
||||
</span>
|
||||
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<Preferences />
|
||||
</Popover>
|
||||
{user ? null : (
|
||||
<LoginLink className="group flex w-full items-center space-x-3 whitespace-nowrap rounded-md px-3 py-1 font-medium text-slate-600 transition-colors hover:bg-slate-500/10 hover:text-slate-600 hover:no-underline active:bg-slate-500/20">
|
||||
<Login className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
||||
<span className="grow text-left">{t("app:login")}</span>
|
||||
</LoginLink>
|
||||
)}
|
||||
</div>
|
||||
<AnimatePresence initial={false}>
|
||||
{user ? (
|
||||
<UserDropdown
|
||||
className="mb-4 w-full"
|
||||
placement="bottom-end"
|
||||
trigger={
|
||||
<motion.button
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{
|
||||
x: -20,
|
||||
opacity: 0,
|
||||
transition: { duration: 0.2 },
|
||||
}}
|
||||
className="group w-full rounded-lg p-2 px-3 text-left text-inherit transition-colors hover:bg-slate-500/10 active:bg-slate-500/20"
|
||||
>
|
||||
<div className="flex w-full items-center space-x-3">
|
||||
<div className="relative">
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-5 animate-spin" />
|
||||
) : (
|
||||
<UserCircle className="h-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
||||
)}
|
||||
</div>
|
||||
<div className="grow overflow-hidden">
|
||||
<div className="truncate font-medium leading-snug text-slate-600">
|
||||
{user.shortName}
|
||||
</div>
|
||||
<div className="truncate text-xs text-slate-500">
|
||||
{user.isGuest ? t("app:guest") : t("app:user")}
|
||||
</div>
|
||||
</div>
|
||||
<DotsVertical className="h-4 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</div>
|
||||
</motion.button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 grow">
|
||||
<div className="max-w-full pt-12 md:w-[1024px] lg:min-h-[calc(100vh-64px)] lg:pt-0">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex flex-col items-center space-y-4 px-6 pt-3 pb-6 text-slate-400 lg:h-16 lg:flex-row lg:space-y-0 lg:space-x-6 lg:py-0 lg:px-8 lg:pb-3">
|
||||
<div>
|
||||
<Link
|
||||
href="https://rallly.co"
|
||||
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
|
||||
>
|
||||
<Logo className="h-5" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hidden text-slate-300 lg:block">•</div>
|
||||
<div className="flex items-center justify-center space-x-6 md:justify-start">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://support.rallly.co"
|
||||
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t("common:support")}
|
||||
</a>
|
||||
<Link
|
||||
href="https://github.com/lukevella/rallly/discussions"
|
||||
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
|
||||
>
|
||||
{t("common:discussions")}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://blog.rallly.co"
|
||||
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
|
||||
>
|
||||
{t("common:blog")}
|
||||
</Link>
|
||||
<div className="hidden text-slate-300 lg:block">•</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
<a
|
||||
href="https://twitter.com/ralllyco"
|
||||
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
|
||||
>
|
||||
<Twitter className="h-5 w-5" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/lukevella/rallly"
|
||||
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
|
||||
>
|
||||
<Github className="h-5 w-5" />
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/uzg4ZcHbuM"
|
||||
className="text-sm text-slate-400 transition-colors hover:text-primary-500 hover:no-underline"
|
||||
>
|
||||
<Discord className="h-5 w-5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden text-slate-300 lg:block">•</div>
|
||||
<a
|
||||
href="https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E"
|
||||
className="inline-flex h-8 items-center rounded-full bg-slate-100 pl-2 pr-3 text-sm text-slate-400 transition-colors hover:bg-primary-500 hover:text-white hover:no-underline focus:ring-2 focus:ring-primary-500 focus:ring-offset-1 active:bg-primary-600"
|
||||
>
|
||||
<Cash className="mr-1 inline-block w-5" />
|
||||
<span>{t("app:donate")}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto max-w-4xl">{children}</div>
|
||||
</div>
|
||||
</DayjsProvider>
|
||||
</ModalProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
11
src/components/layouts/standard-layout/home-link.tsx
Normal file
11
src/components/layouts/standard-layout/home-link.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import Logo from "~/public/logo.svg";
|
||||
|
||||
export const HomeLink = (props: { className?: string }) => {
|
||||
return (
|
||||
<Link href="/" className={props.className}>
|
||||
<Logo className="inline-block w-28 text-primary-500 transition-colors active:text-primary-600 lg:w-32" />
|
||||
</Link>
|
||||
);
|
||||
};
|
156
src/components/layouts/standard-layout/mobile-navigation.tsx
Normal file
156
src/components/layouts/standard-layout/mobile-navigation.tsx
Normal file
|
@ -0,0 +1,156 @@
|
|||
import clsx from "clsx";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
||||
import { LoginLink } from "@/components/auth/login-modal";
|
||||
import Adjustments from "@/components/icons/adjustments.svg";
|
||||
import Home from "@/components/icons/home.svg";
|
||||
import Login from "@/components/icons/login.svg";
|
||||
import Menu from "@/components/icons/menu.svg";
|
||||
import Pencil from "@/components/icons/pencil.svg";
|
||||
import Support from "@/components/icons/support.svg";
|
||||
import UserCircle from "@/components/icons/user-circle.svg";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/popover";
|
||||
import Preferences from "@/components/preferences";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
|
||||
import { Logo } from "../../logo";
|
||||
import { UserDropdown } from "./user-dropdown";
|
||||
|
||||
export const MobileNavigation = (props: { className?: string }) => {
|
||||
const { user, isUpdating } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
|
||||
const [isPinned, setIsPinned] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const scrollHandler = () => {
|
||||
if (window.scrollY > 0) {
|
||||
setIsPinned(true);
|
||||
} else {
|
||||
setIsPinned(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", scrollHandler);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", scrollHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"sticky top-0 z-40 flex h-12 w-full shrink-0 items-center justify-between border-b p-3 transition-all",
|
||||
{
|
||||
"bg-gray-50/75 shadow-sm backdrop-blur-md ": isPinned,
|
||||
"border-transparent bg-gray-50/0 shadow-none": !isPinned,
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<button
|
||||
role="button"
|
||||
type="button"
|
||||
className="group flex items-center rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
>
|
||||
<Menu className="mr-2 w-5 group-hover:text-primary-500" />
|
||||
<Logo />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start">
|
||||
<AppMenu />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{user ? null : (
|
||||
<LoginLink className="flex w-full cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300">
|
||||
<Login className="h-5 opacity-75" />
|
||||
<span className="inline-block">{t("app:login")}</span>
|
||||
</LoginLink>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{user ? (
|
||||
<UserDropdown
|
||||
placement="bottom-end"
|
||||
trigger={
|
||||
<button
|
||||
role="button"
|
||||
data-testid="user"
|
||||
className={clsx(
|
||||
"group inline-flex w-full items-center space-x-2 rounded-lg px-2 py-1 text-left transition-colors hover:bg-slate-500/10 active:bg-slate-500/20",
|
||||
{
|
||||
"opacity-50": isUpdating,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<UserCircle className="w-5 opacity-75 group-hover:text-primary-500 group-hover:opacity-100" />
|
||||
</div>
|
||||
<div className="max-w-[120px] truncate font-medium xs:block">
|
||||
{user.isGuest ? t("app:guest") : user.shortName}
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<button
|
||||
role="button"
|
||||
type="button"
|
||||
className="group flex items-center whitespace-nowrap rounded-md px-2 py-1 font-medium text-slate-600 transition-colors hover:bg-gray-200 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
>
|
||||
<Adjustments className="h-5 opacity-75 group-hover:text-primary-500" />
|
||||
<span className="ml-2 hidden sm:block">
|
||||
{t("app:preferences")}
|
||||
</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end">
|
||||
<Preferences className="p-2" />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
return (
|
||||
<div className={clsx("space-y-1", className)}>
|
||||
<Link
|
||||
href="/"
|
||||
className="flex cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 pr-4 font-medium text-slate-600 transition-colors hover:bg-slate-50 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
>
|
||||
<Home className="h-5 opacity-75 " />
|
||||
<span className="inline-block">{t("app:home")}</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/new"
|
||||
className="flex cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 pr-4 font-medium text-slate-600 transition-colors hover:bg-slate-50 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
>
|
||||
<Pencil className="h-5 opacity-75 " />
|
||||
<span className="inline-block">{t("app:createNew")}</span>
|
||||
</Link>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://support.rallly.co"
|
||||
className="flex cursor-pointer items-center space-x-2 whitespace-nowrap rounded-md px-2 py-1 pr-4 font-medium text-slate-600 transition-colors hover:bg-slate-50 hover:text-slate-600 hover:no-underline active:bg-gray-300"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Support className="h-5 opacity-75" />
|
||||
<span className="inline-block">{t("common:support")}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
105
src/components/layouts/standard-layout/user-dropdown.tsx
Normal file
105
src/components/layouts/standard-layout/user-dropdown.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
||||
import { useLoginModal } from "@/components/auth/login-modal";
|
||||
import Dropdown, { DropdownItem, DropdownProps } from "@/components/dropdown";
|
||||
import Login from "@/components/icons/login.svg";
|
||||
import Logout from "@/components/icons/logout.svg";
|
||||
import Question from "@/components/icons/question-mark-circle.svg";
|
||||
import User from "@/components/icons/user.svg";
|
||||
import { useModalContext } from "@/components/modal/modal-provider";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
|
||||
export const UserDropdown: React.VoidFunctionComponent<DropdownProps> = ({
|
||||
children,
|
||||
...forwardProps
|
||||
}) => {
|
||||
const { logout, user } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
const { openLoginModal } = useLoginModal();
|
||||
const modalContext = useModalContext();
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dropdown {...forwardProps}>
|
||||
{children}
|
||||
{user.isGuest ? (
|
||||
<DropdownItem
|
||||
icon={Question}
|
||||
label={t("app:whatsThis")}
|
||||
onClick={() => {
|
||||
modalContext.render({
|
||||
showClose: true,
|
||||
content: (
|
||||
<div className="w-96 max-w-full p-6 pt-28">
|
||||
<div className="absolute left-0 -top-8 w-full text-center">
|
||||
<div className="inline-flex h-20 w-20 items-center justify-center rounded-full border-8 border-white bg-gradient-to-b from-purple-400 to-primary-500">
|
||||
<User className="h-7 text-white" />
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="text-lg font-medium leading-snug">
|
||||
Guest
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
{user.shortName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>{t("app:guestSessionNotice")}</p>
|
||||
<div>
|
||||
<a
|
||||
href="https://support.rallly.co/guest-sessions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link"
|
||||
>
|
||||
{t("app:guestSessionReadMore")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
overlayClosable: true,
|
||||
footer: null,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{!user.isGuest ? (
|
||||
<DropdownItem
|
||||
href="/profile"
|
||||
icon={User}
|
||||
label={t("app:yourProfile")}
|
||||
/>
|
||||
) : null}
|
||||
{user.isGuest ? (
|
||||
<DropdownItem
|
||||
icon={Login}
|
||||
label={t("app:login")}
|
||||
onClick={openLoginModal}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownItem
|
||||
icon={Logout}
|
||||
label={user.isGuest ? t("app:forgetMe") : t("app:logout")}
|
||||
onClick={() => {
|
||||
if (user?.isGuest) {
|
||||
modalContext.render({
|
||||
title: t("app:areYouSure"),
|
||||
description: t("app:endingGuestSessionNotice"),
|
||||
|
||||
onOk: logout,
|
||||
okButtonProps: {
|
||||
type: "danger",
|
||||
},
|
||||
okText: t("app:endSession"),
|
||||
cancelText: t("app:cancel"),
|
||||
});
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
18
src/components/logo.tsx
Normal file
18
src/components/logo.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import clsx from "clsx";
|
||||
|
||||
export const Logo = (props: { className?: string; color?: boolean }) => {
|
||||
const { color = true } = props;
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"font-semibold uppercase tracking-widest",
|
||||
{
|
||||
"text-primary-500": color,
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
Rallly
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -69,6 +69,7 @@ const Modal: React.VoidFunctionComponent<ModalProps> = ({
|
|||
<div className="mx-4 max-w-full overflow-hidden rounded-xl bg-white shadow-xl xs:rounded-xl">
|
||||
{showClose ? (
|
||||
<button
|
||||
role="button"
|
||||
className="absolute right-5 top-1 z-10 rounded-lg p-2 text-slate-400 transition-colors hover:bg-slate-500/10 hover:text-slate-500 active:bg-slate-500/20"
|
||||
onClick={onCancel}
|
||||
>
|
||||
|
|
|
@ -20,13 +20,21 @@ const NameInput: React.ForwardRefRenderFunction<
|
|||
const { t } = useTranslation("app");
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
{value ? (
|
||||
<UserAvatar
|
||||
name={value ?? defaultValue ?? ""}
|
||||
className="absolute left-2"
|
||||
/>
|
||||
) : null}
|
||||
<input
|
||||
ref={ref}
|
||||
className={clsx("input pl-[35px]", className)}
|
||||
className={clsx(
|
||||
"input",
|
||||
{
|
||||
"pl-9": value || defaultValue,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
placeholder={t("yourName")}
|
||||
value={value}
|
||||
{...forwardProps}
|
||||
|
|
|
@ -1,165 +1,22 @@
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useMount } from "react-use";
|
||||
|
||||
import { Button } from "@/components/button";
|
||||
import Discussion from "@/components/discussion";
|
||||
import LockClosed from "@/components/icons/lock-closed.svg";
|
||||
import Share from "@/components/icons/share.svg";
|
||||
import DesktopPoll from "@/components/poll/desktop-poll";
|
||||
import MobilePoll from "@/components/poll/mobile-poll";
|
||||
import { preventWidows } from "@/utils/prevent-widows";
|
||||
|
||||
import { trpc, trpcNext } from "../utils/trpc";
|
||||
import { useParticipants } from "./participants-provider";
|
||||
import ManagePoll from "./poll/manage-poll";
|
||||
import { useUpdatePollMutation } from "./poll/mutations";
|
||||
import NotificationsToggle from "./poll/notifications-toggle";
|
||||
import PollSubheader from "./poll/poll-subheader";
|
||||
import TruncatedLinkify from "./poll/truncated-linkify";
|
||||
import { UnverifiedPollNotice } from "./poll/unverified-poll-notice";
|
||||
import { useTouchBeacon } from "./poll/use-touch-beacon";
|
||||
import { UserAvatarProvider } from "./poll/user-avatar";
|
||||
import VoteIcon from "./poll/vote-icon";
|
||||
import { usePoll } from "./poll-context";
|
||||
import Sharing from "./sharing";
|
||||
import { useUser } from "./user-provider";
|
||||
|
||||
const checkIfWideScreen = () => window.innerWidth > 640;
|
||||
|
||||
const useWideScreen = () => {
|
||||
const [isWideScreen, setIsWideScreen] = React.useState(checkIfWideScreen);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => setIsWideScreen(checkIfWideScreen());
|
||||
|
||||
window.addEventListener("resize", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isWideScreen;
|
||||
};
|
||||
|
||||
export const AdminControls = () => {
|
||||
const { poll, urlId } = usePoll();
|
||||
const { t } = useTranslation("app");
|
||||
|
||||
const isWideScreen = useWideScreen();
|
||||
|
||||
const router = useRouter();
|
||||
const [isSharingVisible, setSharingVisible] = React.useState(
|
||||
!!router.query.sharing,
|
||||
);
|
||||
|
||||
const queryClient = trpcNext.useContext();
|
||||
|
||||
const session = useUser();
|
||||
|
||||
const { mutate: updatePollMutation } = useUpdatePollMutation();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (router.query.unsubscribe) {
|
||||
updatePollMutation(
|
||||
{ urlId: urlId, notifications: false },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("notificationsDisabled"));
|
||||
posthog.capture("unsubscribed from notifications");
|
||||
},
|
||||
},
|
||||
);
|
||||
router.replace(`/admin/${router.query.urlId}`, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
}
|
||||
}, [urlId, router, updatePollMutation, t]);
|
||||
|
||||
const verifyEmail = trpc.useMutation(["polls.verification.verify"], {
|
||||
onSuccess: () => {
|
||||
toast.success(t("pollHasBeenVerified"));
|
||||
queryClient.poll.invalidate();
|
||||
session.refresh();
|
||||
posthog.capture("verified email");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("linkHasExpired"));
|
||||
},
|
||||
onSettled: () => {
|
||||
router.replace(`/admin/${router.query.urlId}`, undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useMount(() => {
|
||||
const { code } = router.query;
|
||||
if (typeof code === "string" && !poll.verified) {
|
||||
verifyEmail.mutate({ code, pollId: poll.id });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex space-x-2 px-4 md:justify-end md:px-0">
|
||||
<NotificationsToggle />
|
||||
<ManagePoll placement={isWideScreen ? "bottom-end" : "bottom-start"} />
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Share />}
|
||||
onClick={() => {
|
||||
setSharingVisible((value) => !value);
|
||||
}}
|
||||
>
|
||||
{t("share")}
|
||||
</Button>
|
||||
</div>
|
||||
<AnimatePresence initial={false}>
|
||||
{isSharingVisible ? (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
height: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
height: "auto",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
height: 0,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<Sharing
|
||||
onHide={() => {
|
||||
setSharingVisible(false);
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
{poll.verified === false ? (
|
||||
<div className="m-4 overflow-hidden rounded-lg border p-4 md:mx-0 md:mt-0">
|
||||
<UnverifiedPollNotice />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Poll = (props: { children?: React.ReactNode }) => {
|
||||
const { t } = useTranslation("app");
|
||||
const { poll } = usePoll();
|
||||
|
@ -172,8 +29,6 @@ export const Poll = (props: { children?: React.ReactNode }) => {
|
|||
[participants],
|
||||
);
|
||||
|
||||
const checkIfWideScreen = () => window.innerWidth > 640;
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => setIsWideScreen(checkIfWideScreen());
|
||||
|
||||
|
@ -189,34 +44,23 @@ export const Poll = (props: { children?: React.ReactNode }) => {
|
|||
|
||||
return (
|
||||
<UserAvatarProvider seed={poll.id} names={names}>
|
||||
<div className="relative max-w-full py-4 md:px-4">
|
||||
<Head>
|
||||
<title>{poll.title}</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Head>
|
||||
<div
|
||||
className="mx-auto max-w-full lg:mx-0"
|
||||
style={{
|
||||
width: Math.max(768, poll.options.length * 95 + 200 + 160),
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="mx-auto max-w-full space-y-3 sm:space-y-4 lg:mx-0">
|
||||
{props.children}
|
||||
{poll.closed ? (
|
||||
<div className="flex bg-sky-100 py-3 px-4 text-sky-700 md:mb-4 md:rounded-lg md:shadow-sm">
|
||||
<div className="mr-2 rounded-md">
|
||||
<LockClosed className="w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{t("pollHasBeenLocked")}</div>
|
||||
<div className="flex items-center gap-3 border border-pink-200 bg-pink-100 p-3 text-pink-600 md:mb-4 md:rounded-md md:shadow-sm">
|
||||
<div className="rounded-md">
|
||||
<LockClosed className="w-5" />
|
||||
</div>
|
||||
<div>{t("pollHasBeenLocked")}</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mb-4 border border-t bg-white md:overflow-hidden md:rounded-md">
|
||||
<div className="p-4 md:border-b md:p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border bg-white shadow-sm md:overflow-hidden">
|
||||
<div className="p-4 sm:p-6">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div
|
||||
className="mb-1 text-2xl font-semibold text-slate-700 md:text-left md:text-3xl"
|
||||
className="mb-1 text-2xl font-semibold text-slate-800 sm:text-3xl"
|
||||
data-testid="poll-title"
|
||||
>
|
||||
{preventWidows(poll.title)}
|
||||
|
@ -261,10 +105,16 @@ export const Poll = (props: { children?: React.ReactNode }) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<React.Suspense fallback={null}>
|
||||
{participants ? <PollComponent /> : null}
|
||||
</React.Suspense>
|
||||
</div>
|
||||
|
||||
<React.Suspense fallback={null}>
|
||||
{participants ? (
|
||||
<div className="overflow-hidden rounded-md border bg-white shadow-sm">
|
||||
<PollComponent />
|
||||
</div>
|
||||
) : null}
|
||||
</React.Suspense>
|
||||
|
||||
<React.Suspense fallback={<div className="p-4">{t("loading")}</div>}>
|
||||
<Discussion />
|
||||
</React.Suspense>
|
||||
|
|
|
@ -38,13 +38,7 @@ const Poll: React.VoidFunctionComponent = () => {
|
|||
React.useState<string | null>(null);
|
||||
|
||||
const actionColumnWidth = 100;
|
||||
const columnWidth = Math.min(
|
||||
130,
|
||||
Math.max(
|
||||
90,
|
||||
(width - minSidebarWidth - actionColumnWidth) / options.length,
|
||||
),
|
||||
);
|
||||
const columnWidth = 90;
|
||||
|
||||
const numberOfVisibleColumns = Math.min(
|
||||
options.length,
|
||||
|
@ -117,7 +111,7 @@ const Poll: React.VoidFunctionComponent = () => {
|
|||
style={{ width: pollWidth }}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="flex max-h-[calc(100vh-70px)] flex-col overflow-hidden bg-white">
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
{poll.timeZone ? (
|
||||
<div className="flex h-14 shrink-0 items-center justify-end space-x-4 border-b bg-gray-50 px-4">
|
||||
<div className="flex grow items-center">
|
||||
|
@ -133,9 +127,9 @@ const Poll: React.VoidFunctionComponent = () => {
|
|||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<div className="flex border-b py-2">
|
||||
<div className="flex py-3">
|
||||
<div
|
||||
className="flex shrink-0 items-center py-2 pl-4 pr-2 font-medium"
|
||||
className="flex shrink-0 items-center pl-4 pr-2 font-medium"
|
||||
style={{ width: sidebarWidth }}
|
||||
>
|
||||
<div className="flex h-full grow items-end">
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as React from "react";
|
|||
|
||||
import { usePoll } from "@/components/poll-context";
|
||||
|
||||
import DateCard from "../../date-card";
|
||||
import { ScoreSummary } from "../score-summary";
|
||||
import ControlledScrollArea from "./controlled-scroll-area";
|
||||
import { usePollContext } from "./poll-context";
|
||||
|
@ -37,21 +38,13 @@ const PollHeader: React.VoidFunctionComponent = () => {
|
|||
return (
|
||||
<div
|
||||
key={optionId}
|
||||
className="shrink-0 space-y-3 py-3 text-center"
|
||||
className="shrink-0 space-y-3 text-center"
|
||||
style={{ width: columnWidth }}
|
||||
onMouseOver={() => setActiveOptionId(optionId)}
|
||||
onMouseOut={() => setActiveOptionId(null)}
|
||||
>
|
||||
<div>
|
||||
<div className="leading-9">
|
||||
<div className="text-xs font-semibold uppercase text-slate-500/75">
|
||||
{option.dow}
|
||||
</div>
|
||||
<div className="text-2xl font-semibold">{option.day}</div>
|
||||
<div className="text-xs font-medium uppercase text-slate-500/50">
|
||||
{option.month}
|
||||
</div>
|
||||
</div>
|
||||
<DateCard day={option.day} month={option.month} />
|
||||
</div>
|
||||
{option.type === "timeSlot" ? (
|
||||
<TimeRange
|
||||
|
|
|
@ -160,7 +160,7 @@ const ManagePoll: React.VoidFunctionComponent<{
|
|||
location: poll.location ?? "",
|
||||
description: poll.description ?? "",
|
||||
}}
|
||||
className="p-4"
|
||||
className="w-[500px] p-3 sm:p-4"
|
||||
onSubmit={(data) => {
|
||||
//submit
|
||||
updatePollMutation(
|
||||
|
|
|
@ -61,21 +61,8 @@ const MobilePoll: React.VoidFunctionComponent = () => {
|
|||
});
|
||||
|
||||
const { reset, handleSubmit, control, formState } = form;
|
||||
const [selectedParticipantId, setSelectedParticipantId] = React.useState<
|
||||
string | undefined
|
||||
>(() => {
|
||||
if (admin) {
|
||||
// don't select a particpant if admin
|
||||
return;
|
||||
}
|
||||
const { user } = session;
|
||||
if (user) {
|
||||
const userParticipant = participants.find(
|
||||
(participant) => participant.userId === user.id,
|
||||
);
|
||||
return userParticipant?.id;
|
||||
}
|
||||
});
|
||||
const [selectedParticipantId, setSelectedParticipantId] =
|
||||
React.useState<string | undefined>();
|
||||
|
||||
const selectedParticipant = selectedParticipantId
|
||||
? getParticipantById(selectedParticipantId)
|
||||
|
@ -98,7 +85,6 @@ const MobilePoll: React.VoidFunctionComponent = () => {
|
|||
<FormProvider {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="border-t border-b bg-white shadow-sm"
|
||||
onSubmit={handleSubmit(async ({ name, votes }) => {
|
||||
if (selectedParticipant) {
|
||||
await updateParticipant.mutateAsync({
|
||||
|
@ -119,8 +105,8 @@ const MobilePoll: React.VoidFunctionComponent = () => {
|
|||
}
|
||||
})}
|
||||
>
|
||||
<div className="sticky top-[47px] z-30 flex flex-col space-y-2 border-b bg-gray-50 p-3">
|
||||
<div className="flex space-x-3">
|
||||
<div className="flex flex-col space-y-2 border-b bg-gray-50 p-2">
|
||||
<div className="flex space-x-2">
|
||||
{!isEditing ? (
|
||||
<Listbox
|
||||
value={selectedParticipantId}
|
||||
|
@ -189,6 +175,7 @@ const MobilePoll: React.VoidFunctionComponent = () => {
|
|||
rules={{ validate: requiredString }}
|
||||
render={({ field }) => (
|
||||
<NameInput
|
||||
autoFocus={true}
|
||||
disabled={formState.isSubmitting}
|
||||
className={clsx("input w-full", {
|
||||
"input-error": formState.errors.name,
|
||||
|
@ -209,7 +196,7 @@ const MobilePoll: React.VoidFunctionComponent = () => {
|
|||
{t("cancel")}
|
||||
</Button>
|
||||
) : selectedParticipant ? (
|
||||
<div className="flex space-x-3">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
icon={<Pencil />}
|
||||
disabled={
|
||||
|
@ -284,9 +271,6 @@ const MobilePoll: React.VoidFunctionComponent = () => {
|
|||
selectedParticipantId={selectedParticipantId}
|
||||
options={pollContext.options}
|
||||
editable={isEditing}
|
||||
groupClassName={
|
||||
pollContext.pollType === "timeSlot" ? "top-[151px]" : "top-[108px]"
|
||||
}
|
||||
group={(option) => {
|
||||
if (option.type === "timeSlot") {
|
||||
return `${option.dow} ${option.day} ${option.month}`;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from "react";
|
||||
|
||||
import DateCard from "../../date-card";
|
||||
import PollOption, { PollOptionProps } from "./poll-option";
|
||||
|
||||
export interface DateOptionProps extends PollOptionProps {
|
||||
|
@ -14,11 +15,11 @@ const DateOption: React.VoidFunctionComponent<DateOptionProps> = ({
|
|||
}) => {
|
||||
return (
|
||||
<PollOption {...rest}>
|
||||
<div className="font-semibold leading-9">
|
||||
<span className="text-2xl">{day}</span>
|
||||
|
||||
<span className="text-sm uppercase text-slate-400">{dow}</span>
|
||||
</div>
|
||||
{/**
|
||||
* Intentionally using the month prop for the day of week here as a temporary measure
|
||||
* until we update this component.
|
||||
*/}
|
||||
<DateCard day={day} month={dow} />
|
||||
</PollOption>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ const GroupedOptions: React.VoidFunctionComponent<GroupedOptionsProps> = ({
|
|||
<div key={day}>
|
||||
<div
|
||||
className={clsx(
|
||||
"sticky z-10 flex border-b bg-gray-50/80 py-2 px-4 text-sm font-semibold shadow-sm backdrop-blur-md",
|
||||
"flex border-b bg-gray-50/80 py-2 px-4 text-sm font-semibold shadow-sm backdrop-blur-md",
|
||||
groupClassName,
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -154,7 +154,7 @@ const SummarizedParticipantList: React.VoidFunctionComponent<{
|
|||
return (
|
||||
<div className="flex -space-x-1">
|
||||
{participants
|
||||
.slice(0, participants.length <= 8 ? 8 : 7)
|
||||
.slice(0, participants.length <= 6 ? 6 : 5)
|
||||
.map((participant, i) => {
|
||||
return (
|
||||
<UserAvatar
|
||||
|
@ -164,9 +164,9 @@ const SummarizedParticipantList: React.VoidFunctionComponent<{
|
|||
/>
|
||||
);
|
||||
})}
|
||||
{participants.length > 8 ? (
|
||||
{participants.length > 6 ? (
|
||||
<span className="inline-flex h-5 items-center justify-center rounded-full bg-slate-100 px-1 text-xs font-medium ring-1 ring-white">
|
||||
+{participants.length - 7}
|
||||
+{participants.length - 5}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -189,7 +189,7 @@ const PollOption: React.VoidFunctionComponent<PollOptionProps> = ({
|
|||
const [active, setActive] = React.useState(false);
|
||||
return (
|
||||
<div
|
||||
className={clsx("space-y-4 overflow-hidden p-4", {
|
||||
className={clsx("space-y-4 overflow-hidden p-3", {
|
||||
"bg-slate-400/5": editable && active,
|
||||
})}
|
||||
onTouchStart={() => setActive(editable)}
|
||||
|
@ -200,18 +200,26 @@ const PollOption: React.VoidFunctionComponent<PollOptionProps> = ({
|
|||
}}
|
||||
>
|
||||
<div className="flex select-none transition duration-75">
|
||||
<div className="flex grow space-x-8">
|
||||
<div>{children}</div>
|
||||
<div className="flex grow items-center justify-end">
|
||||
<div className="flex grow gap-3">
|
||||
<div className="shrink-0">{children}</div>
|
||||
<AnimatePresence initial={false}>
|
||||
{showVotes ? null : (
|
||||
<motion.div
|
||||
exit={{ opacity: 0 }}
|
||||
className="flex min-w-0 grow items-center justify-end gap-2 overflow-hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onTouchStart={(e) => e.stopPropagation()}
|
||||
className="flex justify-end rounded-lg p-2 active:bg-slate-500/10"
|
||||
className="flex justify-end gap-2 rounded-lg p-2 active:bg-slate-500/10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpanded((value) => !value);
|
||||
}}
|
||||
>
|
||||
{participants.length > 0 ? (
|
||||
<SummarizedParticipantList participants={participants} />
|
||||
) : null}
|
||||
<ScoreSummary yesScore={yesScore} />
|
||||
<ChevronDown
|
||||
className={clsx("h-5 text-slate-400 transition-transform", {
|
||||
|
@ -219,14 +227,16 @@ const PollOption: React.VoidFunctionComponent<PollOptionProps> = ({
|
|||
})}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<CollapsibleContainer
|
||||
expanded={showVotes}
|
||||
className="relative flex justify-center"
|
||||
>
|
||||
{editable ? (
|
||||
<div className="flex h-full w-14 items-center justify-center">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<VoteSelector
|
||||
ref={selectorRef}
|
||||
value={vote}
|
||||
|
@ -247,11 +257,10 @@ const PollOption: React.VoidFunctionComponent<PollOptionProps> = ({
|
|||
</CollapsibleContainer>
|
||||
</div>
|
||||
<AnimatePresence initial={false}>
|
||||
{expanded ? <PollOptionVoteSummary optionId={optionId} /> : null}
|
||||
</AnimatePresence>
|
||||
{!expanded && participants.length > 0 ? (
|
||||
<SummarizedParticipantList participants={participants} />
|
||||
{expanded && !editable ? (
|
||||
<PollOptionVoteSummary optionId={optionId} />
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -33,13 +33,6 @@ const PollSubheader: React.VoidFunctionComponent = () => {
|
|||
</Badge>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{poll.demo ? (
|
||||
<Tooltip content={<Trans t={t} i18nKey="demoPollNotice" />}>
|
||||
<Badge color="blue" className="ml-1">
|
||||
Demo
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="hidden md:inline"> • </span>
|
||||
<span className="whitespace-nowrap">
|
||||
|
|
|
@ -12,20 +12,18 @@ export const UnverifiedPollNotice = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="md:flex md:justify-between md:space-x-4">
|
||||
<div className="mb-4 md:mb-0 md:w-2/3">
|
||||
<div className="space-y-3 rounded-md border border-amber-200 bg-amber-100 p-3 text-gray-700 shadow-sm">
|
||||
<div className="p-1">
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="unverifiedMessage"
|
||||
values={{ email: poll.user.email }}
|
||||
components={{
|
||||
b: (
|
||||
<span className="whitespace-nowrap font-medium text-slate-700" />
|
||||
),
|
||||
b: <span className="whitespace-nowrap font-bold text-slate-700" />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
requestVerificationEmail.mutate({
|
||||
|
@ -33,8 +31,9 @@ export const UnverifiedPollNotice = () => {
|
|||
adminUrlId: poll.adminUrlId,
|
||||
});
|
||||
}}
|
||||
disabled={requestVerificationEmail.isSuccess}
|
||||
loading={requestVerificationEmail.isLoading}
|
||||
className="rounded px-3 py-2 font-semibold shadow-sm"
|
||||
disabled={requestVerificationEmail.isSuccess}
|
||||
>
|
||||
{requestVerificationEmail.isSuccess
|
||||
? "Vertification email sent"
|
||||
|
|
|
@ -1,72 +1,34 @@
|
|||
import {
|
||||
flip,
|
||||
FloatingPortal,
|
||||
offset,
|
||||
Placement,
|
||||
shift,
|
||||
useFloating,
|
||||
} from "@floating-ui/react-dom-interactions";
|
||||
import { Popover as HeadlessPopover } from "@headlessui/react";
|
||||
"use client";
|
||||
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import clsx from "clsx";
|
||||
import { motion } from "framer-motion";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
|
||||
import { transformOriginByPlacement } from "@/utils/constants";
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
interface PopoverProps {
|
||||
trigger: React.ReactElement;
|
||||
children?: React.ReactNode;
|
||||
placement?: Placement;
|
||||
}
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const MotionPanel = motion(HeadlessPopover.Panel);
|
||||
|
||||
const Popover: React.VoidFunctionComponent<PopoverProps> = ({
|
||||
children,
|
||||
trigger,
|
||||
placement: preferredPlacement,
|
||||
}) => {
|
||||
const { reference, floating, x, y, strategy, placement } = useFloating({
|
||||
placement: preferredPlacement,
|
||||
strategy: "fixed",
|
||||
middleware: [offset(5), flip(), shift({ padding: 10 })],
|
||||
});
|
||||
|
||||
const origin = transformOriginByPlacement[placement];
|
||||
|
||||
return (
|
||||
<HeadlessPopover as={React.Fragment}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<HeadlessPopover.Button ref={reference} as="div">
|
||||
{trigger}
|
||||
</HeadlessPopover.Button>
|
||||
<FloatingPortal>
|
||||
{open ? (
|
||||
<MotionPanel
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, scale: 0.9, y: -10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: -10 }}
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={clsx(
|
||||
"z-30 max-w-full translate-x-4 rounded-lg border bg-white p-4 shadow-md",
|
||||
origin,
|
||||
"z-50 animate-popIn rounded-md border bg-white p-1 shadow-md outline-none",
|
||||
{
|
||||
"origin-top-left": align === "start",
|
||||
"origin-top-right": align === "end",
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
position: strategy,
|
||||
left: x ?? "",
|
||||
top: y ?? "",
|
||||
}}
|
||||
ref={floating}
|
||||
>
|
||||
{children}
|
||||
</MotionPanel>
|
||||
) : null}
|
||||
</FloatingPortal>
|
||||
</>
|
||||
)}
|
||||
</HeadlessPopover>
|
||||
);
|
||||
};
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export default Popover;
|
||||
export { Popover, PopoverContent, PopoverTrigger };
|
||||
|
|
|
@ -6,7 +6,7 @@ import React from "react";
|
|||
import { useDayjs } from "../utils/dayjs";
|
||||
import { LanguageSelect } from "./poll/language-selector";
|
||||
|
||||
const Preferences: React.VoidFunctionComponent = () => {
|
||||
const Preferences = (props: { className?: string }) => {
|
||||
const { t } = useTranslation(["app", "common"]);
|
||||
|
||||
const { weekStartsOn, setWeekStartsOn, timeFormat, setTimeFormat } =
|
||||
|
@ -14,8 +14,8 @@ const Preferences: React.VoidFunctionComponent = () => {
|
|||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className={props.className}>
|
||||
<div className="mb-2 space-y-2">
|
||||
<div className="grow text-sm text-slate-500">
|
||||
{t("common:language")}
|
||||
</div>
|
||||
|
|
|
@ -35,7 +35,7 @@ export const Profile: React.VoidFunctionComponent = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl py-4 lg:mx-0">
|
||||
<div className="-mt-3 space-y-3 p-3 sm:space-y-4 sm:p-4">
|
||||
<Head>
|
||||
<title>
|
||||
{t("profileUser", {
|
||||
|
@ -43,14 +43,14 @@ export const Profile: React.VoidFunctionComponent = () => {
|
|||
})}
|
||||
</title>
|
||||
</Head>
|
||||
<div className="mb-4 flex items-center px-4">
|
||||
<div className="mr-4 inline-flex h-14 w-14 items-center justify-center rounded-lg bg-primary-50">
|
||||
<div className="flex gap-4 rounded-md border bg-white p-3 shadow-sm">
|
||||
<div className="inline-flex h-12 w-12 items-center justify-center rounded border border-primary-200/50 bg-primary-50">
|
||||
<User className="h-7 text-primary-500" />
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
data-testid="user-name"
|
||||
className="mb-0 text-xl font-medium leading-tight"
|
||||
className="mb-0 text-lg font-semibold leading-tight"
|
||||
>
|
||||
{user.shortName}
|
||||
</div>
|
||||
|
@ -58,13 +58,21 @@ export const Profile: React.VoidFunctionComponent = () => {
|
|||
{user.isGuest ? t("guest") : t("user")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex grow justify-end">
|
||||
<Link className="btn-default" href="/logout">
|
||||
{t("logout")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="rounded-md border bg-white shadow-sm">
|
||||
<UserDetails userId={user.id} name={user.name} email={user.email} />
|
||||
</div>
|
||||
{createdPolls ? (
|
||||
<div className="card p-0">
|
||||
<div className="flex items-center justify-between border-b p-4 shadow-sm">
|
||||
<div className="text-lg text-slate-700">{t("yourPolls")}</div>
|
||||
<div className="rounded-md border bg-white shadow-sm">
|
||||
<div className="flex items-center justify-between border-b px-3 py-2 shadow-sm">
|
||||
<div className="font-semibold text-slate-800 ">
|
||||
{t("yourPolls")}
|
||||
</div>
|
||||
<Link href="/new" className="btn-default">
|
||||
<Pencil className="mr-1 h-5" />
|
||||
{t("newPoll")}
|
||||
|
|
|
@ -49,10 +49,9 @@ export const UserDetails: React.VoidFunctionComponent<UserDetailsProps> = ({
|
|||
}
|
||||
reset(data);
|
||||
})}
|
||||
className="card mb-4 p-0"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b p-4 shadow-sm">
|
||||
<div className="text-lg text-slate-700 ">{t("yourDetails")}</div>
|
||||
<div className="flex items-center justify-between border-b px-3 py-2 shadow-sm">
|
||||
<div className="font-semibold text-slate-700 ">{t("yourDetails")}</div>
|
||||
<MotionButton
|
||||
variants={{
|
||||
hidden: { opacity: 0, x: 10 },
|
||||
|
|
|
@ -29,7 +29,7 @@ const Sharing: React.VoidFunctionComponent<SharingProps> = ({
|
|||
const participantUrl = `${window.location.origin}/p/${poll.participantUrlId}`;
|
||||
const [didCopy, setDidCopy] = React.useState(false);
|
||||
return (
|
||||
<div className={clsx("card p-4", className)}>
|
||||
<div className={className}>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<div className="text-lg font-semibold text-slate-700">
|
||||
{t("shareLink")}
|
||||
|
@ -52,7 +52,7 @@ const Sharing: React.VoidFunctionComponent<SharingProps> = ({
|
|||
<input
|
||||
readOnly={true}
|
||||
className={clsx(
|
||||
"mb-4 w-full rounded-md bg-gray-100 p-2 text-slate-600 transition-all md:mb-0 md:p-3 md:text-lg",
|
||||
"mb-4 w-full rounded-md bg-gray-100 p-2 text-slate-600 transition-colors md:mb-0 md:p-3 md:text-lg",
|
||||
{
|
||||
"bg-slate-50 opacity-75": didCopy,
|
||||
},
|
||||
|
|
48
src/components/sticky.tsx
Normal file
48
src/components/sticky.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
export const useDetectSticky = <E extends HTMLElement>(
|
||||
top: number,
|
||||
): [React.RefObject<E>, boolean] => {
|
||||
const [pinned, setPinned] = React.useState(false);
|
||||
const ref = React.useRef<E>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) => setPinned(e.intersectionRatio < 1),
|
||||
{ rootMargin: `-${top + 1}px 0px 0px 0px`, threshold: [1] },
|
||||
);
|
||||
|
||||
observer.observe(ref.current);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [top]);
|
||||
|
||||
return [ref, pinned];
|
||||
};
|
||||
|
||||
export const Sticky: React.VoidFunctionComponent<{
|
||||
children?: React.ReactNode | React.FunctionComponent<{ isPinned: boolean }>;
|
||||
className?: string | ((isPinned: boolean) => string);
|
||||
top: number;
|
||||
}> = ({ className, children, top, ...rest }) => {
|
||||
const [ref, isPinned] = useDetectSticky<HTMLDivElement>(top);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"sticky",
|
||||
typeof className === "function" ? className(isPinned) : className,
|
||||
)}
|
||||
style={{ top }}
|
||||
{...rest}
|
||||
>
|
||||
{typeof children === "function" ? children({ isPinned }) : children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -52,11 +52,15 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
|||
const { t } = useTranslation("app");
|
||||
|
||||
const queryClient = trpcNext.useContext();
|
||||
const { data: user, isFetching } = trpcNext.whoami.get.useQuery();
|
||||
const { data: user } = trpcNext.whoami.get.useQuery();
|
||||
|
||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||
|
||||
const logout = trpcNext.whoami.destroy.useMutation({
|
||||
onSuccess: () => {
|
||||
queryClient.whoami.invalidate();
|
||||
onSuccess: async () => {
|
||||
setIsUpdating(true);
|
||||
await queryClient.whoami.invalidate();
|
||||
setIsUpdating(false);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -96,7 +100,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
|||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
isUpdating: isFetching,
|
||||
isUpdating,
|
||||
user: { ...user, shortName },
|
||||
refresh: () => {
|
||||
return queryClient.whoami.invalidate();
|
||||
|
|
|
@ -15,6 +15,7 @@ import { Toaster } from "react-hot-toast";
|
|||
import Maintenance from "@/components/maintenance";
|
||||
|
||||
import { useCrispChat } from "../components/crisp-chat";
|
||||
import ModalProvider from "../components/modal/modal-provider";
|
||||
import { absoluteUrl } from "../utils/absolute-url";
|
||||
import { trpcNext } from "../utils/trpc";
|
||||
|
||||
|
@ -76,7 +77,9 @@ const MyApp: NextPage<AppProps> = ({ Component, pageProps }) => {
|
|||
--font-noto: ${noto.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
<ModalProvider>
|
||||
<Component {...pageProps} />
|
||||
</ModalProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { GetServerSideProps, NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
@ -6,38 +7,43 @@ import React from "react";
|
|||
import FullPageLoader from "@/components/full-page-loader";
|
||||
import StandardLayout from "@/components/layouts/standard-layout";
|
||||
import { ParticipantsProvider } from "@/components/participants-provider";
|
||||
import { AdminControls, Poll } from "@/components/poll";
|
||||
import { Poll } from "@/components/poll";
|
||||
import { PollContextProvider } from "@/components/poll-context";
|
||||
import { withSession } from "@/components/user-provider";
|
||||
import { withSessionSsr } from "@/utils/auth";
|
||||
import { trpcNext } from "@/utils/trpc";
|
||||
import { withPageTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
import { AdminControls } from "../../components/admin-control";
|
||||
|
||||
const PollPageLoader: NextPage = () => {
|
||||
const { query } = useRouter();
|
||||
const { t } = useTranslation("app");
|
||||
const urlId = query.urlId as string;
|
||||
|
||||
const pollQuery = trpcNext.poll.getByAdminUrlId.useQuery(
|
||||
{ urlId },
|
||||
{
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
const pollQuery = trpcNext.poll.getByAdminUrlId.useQuery({ urlId });
|
||||
|
||||
const poll = pollQuery.data;
|
||||
|
||||
if (poll) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("adminPollTitle", { title: poll.title })}</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Head>
|
||||
<ParticipantsProvider pollId={poll.id}>
|
||||
<StandardLayout>
|
||||
<PollContextProvider poll={poll} urlId={urlId} admin={true}>
|
||||
<Poll>
|
||||
<AdminControls />
|
||||
</Poll>
|
||||
<div className="flex flex-col space-y-3 p-3 sm:space-y-4 sm:p-4">
|
||||
<AdminControls>
|
||||
<Poll />
|
||||
</AdminControls>
|
||||
</div>
|
||||
</PollContextProvider>
|
||||
</StandardLayout>
|
||||
</ParticipantsProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { GetServerSideProps } from "next";
|
||||
import Head from "next/head";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import CreatePoll from "@/components/create-poll";
|
||||
|
||||
|
@ -6,7 +8,19 @@ import { withSession } from "../components/user-provider";
|
|||
import { withSessionSsr } from "../utils/auth";
|
||||
import { withPageTranslations } from "../utils/with-page-translations";
|
||||
|
||||
export default withSession(CreatePoll);
|
||||
const Page = () => {
|
||||
const { t } = useTranslation("app");
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("createNew")}</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Head>
|
||||
<CreatePoll />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default withSession(Page);
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
||||
withPageTranslations(["common", "app"]),
|
||||
|
|
|
@ -1,47 +1,61 @@
|
|||
import { GetServerSideProps, NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
||||
import FullPageLoader from "@/components/full-page-loader";
|
||||
import { ParticipantsProvider } from "@/components/participants-provider";
|
||||
import { Poll } from "@/components/poll";
|
||||
import { PollContextProvider } from "@/components/poll-context";
|
||||
import { withSession } from "@/components/user-provider";
|
||||
import { useUser, withSession } from "@/components/user-provider";
|
||||
import { withSessionSsr } from "@/utils/auth";
|
||||
import { trpcNext } from "@/utils/trpc";
|
||||
import { withPageTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
import StandardLayout from "../../components/layouts/standard-layout";
|
||||
import ModalProvider from "../../components/modal/modal-provider";
|
||||
import { ParticipantLayout } from "../../components/layouts/participant-layout";
|
||||
import { DayjsProvider } from "../../utils/dayjs";
|
||||
|
||||
const PollPageLoader: NextPage = () => {
|
||||
const Page: NextPage = () => {
|
||||
const { query } = useRouter();
|
||||
const { t } = useTranslation("app");
|
||||
const urlId = query.urlId as string;
|
||||
|
||||
const pollQuery = trpcNext.poll.getByParticipantUrlId.useQuery({ urlId });
|
||||
|
||||
const { user } = useUser();
|
||||
const poll = pollQuery.data;
|
||||
|
||||
const { t } = useTranslation("app");
|
||||
if (poll) {
|
||||
return (
|
||||
<ModalProvider>
|
||||
<>
|
||||
<Head>
|
||||
<title>{poll.title}</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Head>
|
||||
<DayjsProvider>
|
||||
<ParticipantsProvider pollId={poll.id}>
|
||||
<StandardLayout>
|
||||
<ParticipantLayout>
|
||||
<PollContextProvider poll={poll} urlId={urlId} admin={false}>
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{user.id === poll.user.id ? (
|
||||
<Link
|
||||
className="btn-default"
|
||||
href={`/admin/${poll.adminUrlId}`}
|
||||
>
|
||||
← {t("goToAdmin")}
|
||||
</Link>
|
||||
) : null}
|
||||
<Poll />
|
||||
</div>
|
||||
</PollContextProvider>
|
||||
</StandardLayout>
|
||||
</ParticipantLayout>
|
||||
</ParticipantsProvider>
|
||||
</DayjsProvider>
|
||||
</ModalProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <FullPageLoader>{t("loading")}</FullPageLoader>;
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
||||
|
@ -55,4 +69,4 @@ export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
|||
},
|
||||
);
|
||||
|
||||
export default withSession(PollPageLoader);
|
||||
export default withSession(Page);
|
||||
|
|
|
@ -35,7 +35,6 @@ export const trpcNext = createTRPCNext<AppRouter>({
|
|||
defaultOptions: {
|
||||
queries: {
|
||||
cacheTime: Infinity,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
},
|
||||
},
|
||||
mutationCache: new MutationCache({
|
||||
|
|
22
style.css
22
style.css
|
@ -4,7 +4,7 @@
|
|||
|
||||
@layer base {
|
||||
html {
|
||||
@apply h-full overflow-auto bg-slate-50 font-sans text-base text-slate-600;
|
||||
@apply h-full bg-slate-50 font-sans text-base text-slate-600;
|
||||
}
|
||||
body,
|
||||
#__next {
|
||||
|
@ -34,7 +34,7 @@
|
|||
@apply mb-1 block text-sm text-slate-800;
|
||||
}
|
||||
button {
|
||||
@apply cursor-default outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1;
|
||||
@apply outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1;
|
||||
}
|
||||
|
||||
#floating-ui-root {
|
||||
|
@ -50,7 +50,7 @@
|
|||
@apply mb-4;
|
||||
}
|
||||
.input {
|
||||
@apply appearance-none rounded-md border border-gray-200 px-2 py-1 text-slate-700 shadow-sm placeholder:text-slate-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500;
|
||||
@apply appearance-none rounded-md border border-gray-200 px-2 py-1 text-slate-700 placeholder:text-slate-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500;
|
||||
}
|
||||
input.input {
|
||||
@apply h-9;
|
||||
|
@ -65,14 +65,14 @@
|
|||
@apply h-4 w-4 rounded border-slate-300 text-primary-500 shadow-sm focus:ring-primary-500;
|
||||
}
|
||||
.btn {
|
||||
@apply inline-flex h-9 cursor-default select-none items-center justify-center whitespace-nowrap rounded-md border px-3 font-medium shadow-sm transition-all;
|
||||
@apply inline-flex h-9 select-none items-center justify-center whitespace-nowrap rounded-md border px-3 font-medium shadow-sm transition-all;
|
||||
}
|
||||
a.btn {
|
||||
@apply cursor-pointer hover:no-underline;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
@apply btn bg-white text-slate-700 hover:bg-slate-100/10 active:bg-slate-500/10;
|
||||
@apply btn bg-white text-slate-700 hover:bg-gray-50 active:bg-slate-100;
|
||||
}
|
||||
|
||||
a.btn-default {
|
||||
|
@ -98,15 +98,15 @@
|
|||
}
|
||||
|
||||
.segment-button {
|
||||
@apply flex h-9 text-center shadow-sm;
|
||||
@apply flex h-9 text-center;
|
||||
}
|
||||
|
||||
.segment-button button {
|
||||
@apply inline-flex grow items-center justify-center border-t border-b border-r bg-white px-4 font-medium transition-colors first:rounded-l first:border-l last:rounded-r hover:bg-slate-50 focus:z-10 focus-visible:ring-2 focus-visible:ring-offset-0 active:bg-slate-100;
|
||||
@apply inline-flex grow items-center justify-center border-t border-b border-r bg-gray-50 px-4 transition-colors first:rounded-l first:border-l last:rounded-r hover:bg-slate-50 focus:z-10 focus-visible:ring-2 focus-visible:ring-offset-0 active:bg-slate-100;
|
||||
}
|
||||
|
||||
.segment-button .segment-button-active {
|
||||
@apply text-primary-500;
|
||||
@apply pointer-events-none bg-white text-slate-800;
|
||||
}
|
||||
|
||||
.menu {
|
||||
|
@ -118,7 +118,7 @@
|
|||
}
|
||||
|
||||
.menu-item {
|
||||
@apply block w-full cursor-default select-none truncate px-4 py-2 text-left;
|
||||
@apply block w-full select-none truncate px-4 py-2 text-left;
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
|
@ -207,3 +207,7 @@
|
|||
.rbc-header.rbc-today {
|
||||
@apply bg-white text-rose-600;
|
||||
}
|
||||
|
||||
.rbc-button-link {
|
||||
@apply m-1 w-full;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,13 @@ module.exports = {
|
|||
},
|
||||
popIn: {
|
||||
"0%": {
|
||||
transform: "scale(0.8)",
|
||||
transform: "scale(0.8) translateY(-10px)",
|
||||
opacity: "0",
|
||||
},
|
||||
"100%": {
|
||||
transform: "scale(1)",
|
||||
transform: "scale(1) translateY(0px)",
|
||||
opacity: "1",
|
||||
translateY: "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -44,5 +45,9 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
|
||||
plugins: [
|
||||
require("@tailwindcss/forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("tailwindcss-animate"),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -11,5 +11,5 @@ test("should default to english", async ({ browser }) => {
|
|||
const context = await browser.newContext({ locale: "mt" });
|
||||
const page = await context.newPage();
|
||||
await page.goto("/new");
|
||||
await expect(page.locator("h1", { hasText: "New poll" })).toBeVisible();
|
||||
await expect(page.locator("h1", { hasText: "Create new" })).toBeVisible();
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@ test("should be able to vote and comment on a poll", async ({ page }) => {
|
|||
await page.type('[placeholder="Your name…"]', "Test user");
|
||||
|
||||
await page.click("text=Save");
|
||||
await expect(page.locator("text='Test user'")).toBeVisible();
|
||||
await expect(page.locator("data-testid=user")).toBeVisible();
|
||||
await expect(
|
||||
page.locator("data-testid=participant-selector").locator("text=You"),
|
||||
|
@ -22,9 +21,6 @@ test("should be able to vote and comment on a poll", async ({ page }) => {
|
|||
await page.click("text=Edit");
|
||||
await page.click("data-testid=poll-option >> nth=1");
|
||||
await page.click("text=Save");
|
||||
await expect(page.locator("data-testid=poll-option >> nth=1 ")).toContainText(
|
||||
"2",
|
||||
);
|
||||
|
||||
await page.click("data-testid=delete-participant-button");
|
||||
await page.locator("button", { hasText: "Delete" }).click();
|
||||
|
|
295
yarn.lock
295
yarn.lock
|
@ -1004,6 +1004,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.13.10", "@babel/runtime@^7.20.6":
|
||||
version "7.20.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
|
||||
integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.13.8", "@babel/runtime@^7.18.6":
|
||||
version "7.20.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9"
|
||||
|
@ -1011,13 +1018,6 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.10"
|
||||
|
||||
"@babel/runtime@^7.20.6":
|
||||
version "7.20.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
|
||||
integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz"
|
||||
|
@ -1090,6 +1090,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.6.2.tgz#f2813f0e5f3d5ed7af5029e1a082203dadf02b7d"
|
||||
integrity sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg==
|
||||
|
||||
"@floating-ui/core@^0.7.3":
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86"
|
||||
integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==
|
||||
|
||||
"@floating-ui/dom@^0.4.5":
|
||||
version "0.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.4.5.tgz#2e88d16646119cc67d44683f75ee99840475bbfa"
|
||||
|
@ -1097,6 +1102,13 @@
|
|||
dependencies:
|
||||
"@floating-ui/core" "^0.6.2"
|
||||
|
||||
"@floating-ui/dom@^0.5.3":
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.5.4.tgz#4eae73f78bcd4bd553ae2ade30e6f1f9c73fe3f1"
|
||||
integrity sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^0.7.3"
|
||||
|
||||
"@floating-ui/react-dom-interactions@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.4.0.tgz#b4d951aaa3b0a66cd0b2787a7bf9d5d7b2f12021"
|
||||
|
@ -1106,6 +1118,14 @@
|
|||
aria-hidden "^1.1.3"
|
||||
use-isomorphic-layout-effect "^1.1.1"
|
||||
|
||||
"@floating-ui/react-dom@0.7.2":
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.7.2.tgz#0bf4ceccb777a140fc535c87eb5d6241c8e89864"
|
||||
integrity sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^0.5.3"
|
||||
use-isomorphic-layout-effect "^1.1.1"
|
||||
|
||||
"@floating-ui/react-dom@^0.6.3":
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.6.3.tgz#7b64cfd4fd12e4a0515dbf1b2be16e48c9a06c5a"
|
||||
|
@ -1331,6 +1351,197 @@
|
|||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.9.0.tgz#05a1411964e047c1bc43f777c7a1c69f86a2a26c"
|
||||
integrity sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==
|
||||
|
||||
"@radix-ui/primitive@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253"
|
||||
integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-arrow@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz#5246adf79e97f89e819af68da51ddcf349ecf1c4"
|
||||
integrity sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
|
||||
"@radix-ui/react-compose-refs@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
|
||||
integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-context@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
|
||||
integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-dismissable-layer@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.2.tgz#f04d1061bddf00b1ca304148516b9ddc62e45fb2"
|
||||
integrity sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-escape-keydown" "1.0.2"
|
||||
|
||||
"@radix-ui/react-focus-guards@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
|
||||
integrity sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-focus-scope@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.1.tgz#faea8c25f537c5a5c38c50914b63722db0e7f951"
|
||||
integrity sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
|
||||
"@radix-ui/react-id@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e"
|
||||
integrity sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
|
||||
"@radix-ui/react-popover@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.3.tgz#65ae2ee1fca2d7fd750308549eb8e0857c6160fe"
|
||||
integrity sha512-YwedSukfWsyJs3/yP3yXUq44k4/JBe3jqU63Z8v2i19qZZ3dsx32oma17ztgclWPNuqp3A+Xa9UiDlZHyVX8Vg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-context" "1.0.0"
|
||||
"@radix-ui/react-dismissable-layer" "1.0.2"
|
||||
"@radix-ui/react-focus-guards" "1.0.0"
|
||||
"@radix-ui/react-focus-scope" "1.0.1"
|
||||
"@radix-ui/react-id" "1.0.0"
|
||||
"@radix-ui/react-popper" "1.1.0"
|
||||
"@radix-ui/react-portal" "1.0.1"
|
||||
"@radix-ui/react-presence" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-slot" "1.0.1"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-popper@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.0.tgz#2be7e4c0cd4581f54277ca33a981c9037d2a8e60"
|
||||
integrity sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@floating-ui/react-dom" "0.7.2"
|
||||
"@radix-ui/react-arrow" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-context" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
"@radix-ui/react-use-rect" "1.0.0"
|
||||
"@radix-ui/react-use-size" "1.0.0"
|
||||
"@radix-ui/rect" "1.0.0"
|
||||
|
||||
"@radix-ui/react-portal@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.1.tgz#169c5a50719c2bb0079cf4c91a27aa6d37e5dd33"
|
||||
integrity sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
|
||||
"@radix-ui/react-presence@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz#814fe46df11f9a468808a6010e3f3ca7e0b2e84a"
|
||||
integrity sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
|
||||
"@radix-ui/react-primitive@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz#c1ebcce283dd2f02e4fbefdaa49d1cb13dbc990a"
|
||||
integrity sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.1"
|
||||
|
||||
"@radix-ui/react-slot@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz#e7868c669c974d649070e9ecbec0b367ee0b4d81"
|
||||
integrity sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
|
||||
"@radix-ui/react-use-callback-ref@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90"
|
||||
integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-controllable-state@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
|
||||
integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
|
||||
"@radix-ui/react-use-escape-keydown@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz#09ab6455ab240b4f0a61faf06d4e5132c4d639f6"
|
||||
integrity sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
|
||||
"@radix-ui/react-use-layout-effect@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc"
|
||||
integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-rect@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz#b040cc88a4906b78696cd3a32b075ed5b1423b3e"
|
||||
integrity sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/rect" "1.0.0"
|
||||
|
||||
"@radix-ui/react-use-size@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz#a0b455ac826749419f6354dc733e2ca465054771"
|
||||
integrity sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
|
||||
"@radix-ui/rect@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.0.tgz#0dc8e6a829ea2828d53cbc94b81793ba6383bf3c"
|
||||
integrity sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@restart/hooks@^0.4.7":
|
||||
version "0.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.7.tgz#d79ca6472c01ce04389fc73d4a79af1b5e33cd39"
|
||||
|
@ -2126,6 +2337,13 @@ argparse@^1.0.7:
|
|||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
aria-hidden@^1.1.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.2.tgz#8c4f7cc88d73ca42114106fdf6f47e68d31475b8"
|
||||
integrity sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
aria-hidden@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.1.3.tgz#bb48de18dc84787a3c6eee113709c473c64ec254"
|
||||
|
@ -2754,6 +2972,11 @@ dequal@^2.0.2:
|
|||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
|
||||
detect-node-es@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
|
||||
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
|
||||
|
||||
detective@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
|
||||
|
@ -3541,6 +3764,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
|
|||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
get-nonce@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
|
||||
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
|
||||
|
||||
get-symbol-description@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
|
||||
|
@ -5120,11 +5348,39 @@ react-overlays@^5.2.0:
|
|||
uncontrollable "^7.2.1"
|
||||
warning "^4.0.3"
|
||||
|
||||
react-remove-scroll-bar@^2.3.3:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9"
|
||||
integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==
|
||||
dependencies:
|
||||
react-style-singleton "^2.2.1"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-remove-scroll@2.5.5:
|
||||
version "2.5.5"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
|
||||
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
|
||||
dependencies:
|
||||
react-remove-scroll-bar "^2.3.3"
|
||||
react-style-singleton "^2.2.1"
|
||||
tslib "^2.1.0"
|
||||
use-callback-ref "^1.3.0"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-ssr-prepass@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz#bc4ca7fcb52365e6aea11cc254a3d1bdcbd030c5"
|
||||
integrity sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==
|
||||
|
||||
react-style-singleton@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
|
||||
integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==
|
||||
dependencies:
|
||||
get-nonce "^1.0.0"
|
||||
invariant "^2.2.4"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-universal-interface@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz"
|
||||
|
@ -5824,6 +6080,11 @@ table@^6.0.4:
|
|||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
tailwindcss-animate@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.5.tgz#a6720e3b0616e1ff922b32846729881c626a069d"
|
||||
integrity sha512-UU3qrOJ4lFQABY+MVADmBm+0KW3xZyhMdRvejwtXqYOL7YjHYxmuREFAZdmVG5LPe5E9CAst846SLC4j5I3dcw==
|
||||
|
||||
tailwindcss@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.4.tgz#afe3477e7a19f3ceafb48e4b083e292ce0dc0250"
|
||||
|
@ -5942,6 +6203,11 @@ tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3:
|
|||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
||||
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
|
||||
|
||||
tslib@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz"
|
||||
|
@ -6046,11 +6312,26 @@ uri-js@^4.2.2:
|
|||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-callback-ref@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5"
|
||||
integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-isomorphic-layout-effect@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
||||
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
||||
|
||||
use-sidecar@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
|
||||
integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==
|
||||
dependencies:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue