diff --git a/apps/landing/public/locales/en/pricing.json b/apps/landing/public/locales/en/pricing.json index e6ea7fe34..a72a4ac50 100644 --- a/apps/landing/public/locales/en/pricing.json +++ b/apps/landing/public/locales/en/pricing.json @@ -23,5 +23,7 @@ "howToUpgrade": "How do I upgrade to a paid plan?", "howToUpgradeAnswer": "To upgrade, you can go to your billing settings and click on Upgrade.", "cancelSubscription": "How do I cancel my subscription?", - "cancelSubscriptionAnswer": "You can cancel your subscription at any time by going to your billing settings. Once you cancel your subscription, you will still have access to your paid plan until the end of your billing period. After that, you will be downgraded to a free plan." + "cancelSubscriptionAnswer": "You can cancel your subscription at any time by going to your billing settings. Once you cancel your subscription, you will still have access to your paid plan until the end of your billing period. After that, you will be downgraded to a free plan.", + "earlyAccess": "Early bird discount", + "customPollSettings": "Customizable poll settings" } diff --git a/apps/landing/src/pages/pricing.tsx b/apps/landing/src/pages/pricing.tsx index c3ba5a8e6..125890ff4 100644 --- a/apps/landing/src/pages/pricing.tsx +++ b/apps/landing/src/pages/pricing.tsx @@ -1,4 +1,5 @@ -import { CheckIcon, InfoIcon } from "@rallly/icons"; +import { InfoIcon } from "@rallly/icons"; +import { Badge } from "@rallly/ui/badge"; import { BillingPlan, BillingPlanFooter, @@ -23,15 +24,6 @@ import { linkToApp } from "@/lib/linkToApp"; import { NextPageWithLayout } from "@/types"; import { getStaticTranslations } from "@/utils/page-translations"; -const Perk = ({ children }: React.PropsWithChildren) => { - return ( -
  • - - {children} -
  • - ); -}; - const monthlyPriceUsd = 5; const annualPriceUsd = 30; @@ -102,9 +94,17 @@ const Page: NextPageWithLayout = () => { - - - +
    + + + + + + +
    {annualBilling ? ( <> { defaults="Unlimited participants" /> - + + + + - + Login →", "applyToAllDates": "Apply to all dates", "areYouSure": "Are you sure?", - "calendarHelp": "You can't create a poll without any options. Add at least one option to continue.", - "calendarHelpTitle": "Forget something?", "cancel": "Cancel", "changeName": "Change name", "settings": "Settings", "changeNameDescription": "Enter a new name for this participant.", "changeNameInfo": "This will not affect any votes you have already made.", "close": "Close", - "commentPlaceholder": "Leave a comment on this poll (visible to everyone)", "comments": "Comments", "continue": "Continue", "copied": "Copied", @@ -69,7 +66,6 @@ "noDatesSelected": "No dates selected", "notificationsDisabled": "Notifications have been disabled for {title}", "noVotes": "No one has voted for this option", - "ok": "Ok", "optional": "optional", "preferences": "Preferences", "previousMonth": "Previous month", @@ -125,7 +121,6 @@ "languageDescription": "Change your preferred language", "dateAndTime": "Date & Time", "profileDescription": "Change your profile settings", - "back": "Back", "dates": "Dates", "menu": "Menu", "useLocaleDefaults": "Use locale defaults", @@ -157,7 +152,6 @@ "differentOwnerDescription": "This poll was created by a different user. Would you like to transfer ownership to the current user?", "yesTransfer": "Yes, transfer to current user", "noTransfer": "No, take me home", - "createPollDescription": "Create an event and invite participants to vote on the best time to meet.", "share": "Share", "timeShownIn": "Times shown in {timeZone}", "timeShownInLocalTime": "Times shown in local time", @@ -203,5 +197,21 @@ "plan_prioritySupport": "Priority support", "becomeATranslator": "Help translate", "noPolls": "No polls", - "noPollsDescription": "Get started by creating a new poll." + "noPollsDescription": "Get started by creating a new poll.", + "event": "Event", + "describeYourEvent": "Describe what your event is about", + "calendar": "Calendar", + "selectPotentialDates": "Select potential dates or times for your event", + "optionalLabel": "(Optional)", + "calendarHelp": "You can't create a poll without any options. Add at least one option to continue.", + "hideParticipants": "Hide participant list", + "hideParticipantsDescription": "Keep participant details hidden from other participants", + "editSettings": "Edit settings", + "pollSettingsDescription": "Customize the behaviour of your poll", + "commentPlaceholder": "Leave a comment on this poll (visible to everyone)", + "hideScores": "Hide scores", + "hideScoresDescription": "Only show scores until after a participant has voted", + "disableComments": "Disable comments", + "disableCommentsDescription": "Remove the option to leave a comment on the poll", + "planCustomizablePollSettings": "Customizable poll settings" } diff --git a/apps/web/src/components/billing/billing-plans.tsx b/apps/web/src/components/billing/billing-plans.tsx index 3d208a25f..d005615d8 100644 --- a/apps/web/src/components/billing/billing-plans.tsx +++ b/apps/web/src/components/billing/billing-plans.tsx @@ -1,4 +1,3 @@ -import { CheckIcon } from "@rallly/icons"; import { BillingPlan, BillingPlanFooter, @@ -126,15 +125,6 @@ export const BillingPlans = () => { ); }; -const Perk = ({ children }: React.PropsWithChildren) => { - return ( -
  • - - {children} -
  • - ); -}; - export const ProPlan = ({ annual, children, @@ -178,9 +168,15 @@ export const ProPlan = ({ defaults="Unlimited participants" />
    - + - + + + + (v: T | undefined): T => { if (!v) { @@ -46,192 +35,96 @@ export interface CreatePollPageProps { } export const CreatePoll: React.FunctionComponent = () => { - const { t } = useTranslation(); - const router = useRouter(); - const session = useUser(); - - const steps: StepName[] = React.useMemo( - () => - session.user.isGuest - ? ["eventDetails", "options", "userDetails"] - : ["eventDetails", "options"], - [session.user.isGuest], - ); - - const [formData, setFormData] = React.useState({ - currentStep: 0, - }); - - React.useEffect(() => { - const newStep = Math.min(steps.length - 1, formData.currentStep); - if (newStep !== formData.currentStep) { - setFormData((prevData) => ({ - ...prevData, - currentStep: newStep, - })); - } - }, [formData.currentStep, steps.length]); - - const currentStepIndex = formData?.currentStep ?? 0; - - const currentStepName = steps[currentStepIndex]; - - const [isRedirecting, setIsRedirecting] = React.useState(false); - - const posthog = usePostHog(); - const queryClient = trpc.useContext(); - const createPoll = trpc.polls.create.useMutation({ - onSuccess: (res) => { - setIsRedirecting(true); - posthog?.capture("created poll", { - pollId: res.id, - numberOfOptions: formData.options?.options?.length, - optionsView: formData?.options?.view, - }); - queryClient.polls.list.invalidate(); - router.replace(`/poll/${res.id}`); + const form = useForm({ + defaultValues: { + view: "month", + options: [], + hideScores: false, + hideParticipants: false, + disableComments: false, + duration: 60, }, }); - const isBusy = isRedirecting || createPoll.isLoading; + useFormPersist("new-poll", { + watch: form.watch, + setValue: form.setValue, + }); - const handleSubmit = async ( - data: PollDetailsData | PollOptionsData | UserDetailsData, - ) => { - if (currentStepIndex < steps.length - 1) { - setFormData({ - ...formData, - currentStep: currentStepIndex + 1, - [currentStepName]: data, - }); - } else { - // last step - const title = required(formData?.eventDetails?.title); - - await createPoll.mutateAsync({ - title: title, - location: formData?.eventDetails?.location, - description: formData?.eventDetails?.description, - user: session.user.isGuest - ? { - name: required(formData?.userDetails?.name), - email: required(formData?.userDetails?.contact), - } - : undefined, - timeZone: formData?.options?.timeZone, - options: required(formData?.options?.options).map((option) => ({ - startDate: option.type === "date" ? option.date : option.start, - endDate: option.type === "timeSlot" ? option.end : undefined, - })), - }); - } - }; - - const handleChange = ( - data: Partial, - ) => { - setFormData({ - ...formData, - currentStep: currentStepIndex, - [currentStepName]: data, - }); - }; + const posthog = usePostHog(); + const queryClient = trpc.useContext(); + const createPoll = trpc.polls.create.useMutation(); return ( -
    -
    -
    - +
    + { + const title = required(formData?.title); + + await createPoll.mutateAsync( + { + title: title, + location: formData?.location, + description: formData?.description, + timeZone: formData?.timeZone, + hideParticipants: formData?.hideParticipants, + disableComments: formData?.disableComments, + hideScores: formData?.hideScores, + options: required(formData?.options).map((option) => ({ + startDate: option.type === "date" ? option.date : option.start, + endDate: option.type === "timeSlot" ? option.end : undefined, + })), + }, + { + onSuccess: (res) => { + posthog?.capture("created poll", { + pollId: res.id, + numberOfOptions: formData.options?.length, + optionsView: formData?.view, + }); + queryClient.polls.list.invalidate(); + router.replace(`/poll/${res.id}`); + }, + }, + ); + })} + > +
    + + + + + + + + + + + + + + + + + +
    +
    - - - - - - - - - - - {(() => { - switch (currentStepName) { - case "eventDetails": - return ( - - ); - case "options": - return ( - - ); - case "userDetails": - return ( - - ); - } - })()} - - -
    - {currentStepIndex > 0 ? ( - - ) : null} -
    - {currentStepIndex < steps.length - 1 ? ( - - ) : ( - - )} -
    -
    -
    -
    + + ); }; diff --git a/apps/web/src/components/discussion/discussion.tsx b/apps/web/src/components/discussion/discussion.tsx index 9e06f1a98..86b47bef2 100644 --- a/apps/web/src/components/discussion/discussion.tsx +++ b/apps/web/src/components/discussion/discussion.tsx @@ -196,7 +196,10 @@ const Discussion: React.FunctionComponent = () => { className="border-input text-muted-foreground flex w-full rounded border bg-transparent px-3 py-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1" onClick={() => setIsWriting(true)} > - + )}
    diff --git a/apps/web/src/components/event-card.tsx b/apps/web/src/components/event-card.tsx index cb251f671..e244a8966 100644 --- a/apps/web/src/components/event-card.tsx +++ b/apps/web/src/components/event-card.tsx @@ -9,6 +9,7 @@ import { useParticipants } from "@/components/participants-provider"; import { PollStatusBadge } from "@/components/poll-status"; import { TextSummary } from "@/components/text-summary"; import { Trans } from "@/components/trans"; +import { IfParticipantsVisible } from "@/components/visibility"; import { usePoll } from "@/contexts/poll"; import { generateGradient } from "@/utils/color-hash"; import { useDayjs } from "@/utils/dayjs"; @@ -88,15 +89,17 @@ export const EventCard = () => { {!poll.event ? ( ) : ( -
    -
    +
    +
    - + + +
    )}
    diff --git a/apps/web/src/components/forms/index.ts b/apps/web/src/components/forms/index.ts index c8ef778cd..bd9308f3e 100644 --- a/apps/web/src/components/forms/index.ts +++ b/apps/web/src/components/forms/index.ts @@ -3,5 +3,3 @@ export { PollDetailsForm } from "./poll-details-form"; export type { PollOptionsData } from "./poll-options-form/poll-options-form"; export { default as PollOptionsForm } from "./poll-options-form/poll-options-form"; export * from "./types"; -export type { UserDetailsData } from "./user-details-form"; -export { UserDetailsForm } from "./user-details-form"; diff --git a/apps/web/src/components/forms/poll-details-form.tsx b/apps/web/src/components/forms/poll-details-form.tsx index 87e9188f6..7b58d39de 100644 --- a/apps/web/src/components/forms/poll-details-form.tsx +++ b/apps/web/src/components/forms/poll-details-form.tsx @@ -1,13 +1,14 @@ -import { Form, FormItem, FormLabel } from "@rallly/ui/form"; +import { FormField, FormItem, FormLabel, FormMessage } from "@rallly/ui/form"; import { Input } from "@rallly/ui/input"; import { Textarea } from "@rallly/ui/textarea"; import clsx from "clsx"; import { useTranslation } from "next-i18next"; -import * as React from "react"; -import { useForm } from "react-hook-form"; +import { useFormContext } from "react-hook-form"; -import { requiredString } from "../../utils/form-validation"; -import { PollFormProps } from "./types"; +import { Trans } from "@/components/trans"; +import { useFormValidation } from "@/utils/form-validation"; + +import { NewEventData } from "./types"; export interface PollDetailsData { title: string; @@ -15,77 +16,71 @@ export interface PollDetailsData { description: string; } -export const PollDetailsForm: React.FunctionComponent< - PollFormProps -> = ({ name, defaultValues, onSubmit, onChange, className }) => { +export const PollDetailsForm = () => { const { t } = useTranslation(); - const form = useForm({ defaultValues }); + const form = useFormContext(); + const { requiredString } = useFormValidation(); const { - handleSubmit, register, - watch, formState: { errors }, } = form; - React.useEffect(() => { - if (onChange) { - const subscription = watch(onChange); - return () => { - subscription.unsubscribe(); - }; - } - }, [onChange, watch]); - return ( -
    - - {/*
    -

    - -

    -

    - + ( + + {t("title")} + -

    -
    */} - - {t("title")} - - - - {t("location")} - - - - {t("description")} -