Merge branch 'main' of https://github.com/calcom/cal.com into prod-disableimpersonation

This commit is contained in:
sean-brydon 2022-09-15 18:00:53 +01:00
commit d064f5d228
190 changed files with 3116 additions and 1857 deletions

View File

@ -1,7 +1,8 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Copy } from "react-feather";
import { TextAreaField, TextField } from "@calcom/ui/v2/core/form/fields";
import { TextAreaField, TextField, PasswordField } from "@calcom/ui/v2/core/form/fields";
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
export default {
@ -66,3 +67,9 @@ export const TextAreaInput: ComponentStory<typeof TextAreaField> = () => (
);
export const DatePickerInput: ComponentStory<typeof DatePicker> = () => <DatePicker date={new Date()} />;
export const PasswordInput: ComponentStory<typeof PasswordField> = () => (
<TooltipProvider>
<PasswordField />
</TooltipProvider>
);

View File

@ -92,11 +92,6 @@ const Component = ({
<header className="px-4 py-2">
<div className="flex items-center">
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
{isProOnly && user?.plan === "FREE" ? (
<Badge className="ml-2" variant="default">
PRO
</Badge>
) : null}
</div>
<h2 className="text-sm text-gray-500">
<span className="capitalize">{categories[0]}</span> {t("published_by", { author })}

View File

@ -5,15 +5,14 @@ import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState }
import { components, ControlProps } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
import { HorizontalTabs, showToast } from "@calcom/ui/v2";
import { Button, Switch } from "@calcom/ui/v2";
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
import NavTabs from "@components/NavTabs";
import ColorPicker from "@components/ui/colorpicker";
import Select from "@components/ui/form/Select";
@ -38,7 +37,7 @@ type PreviewState = {
brandColor: string;
};
};
const queryParamsForDialog = ["embedType", "tabName", "embedUrl"];
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
const getDimension = (dimension: string) => {
if (dimension.match(/^\d+$/)) {
@ -453,7 +452,7 @@ const embeds: {
const tabs = [
{
name: "HTML",
tabName: "embed-code",
embedTabName: "embed-code",
icon: Icon.FiCode,
type: "code",
Component: forwardRef<
@ -504,7 +503,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
},
{
name: "React",
tabName: "embed-react",
embedTabName: "embed-react",
icon: Icon.FiCode,
type: "code",
Component: forwardRef<
@ -544,7 +543,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
},
{
name: "Preview",
tabName: "embed-preview",
embedTabName: "embed-preview",
icon: Icon.FiEye,
type: "iframe",
Component: forwardRef<
@ -561,7 +560,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
<iframe
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
data-testid="embed-preview"
className="border-1 h-[75vh] border"
className="border-1 h-[100vh] border"
width="100%"
height="100%"
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
@ -678,8 +677,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
};
// Use embed-code as default tab
if (!router.query.tabName) {
router.query.tabName = "embed-code";
if (!router.query.embedTabName) {
router.query.embedTabName = "embed-code";
router.push({
query: {
...router.query,
@ -780,7 +779,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
];
return (
<DialogContent size="xl">
<DialogContent size="lg">
<div className="flex">
<div className="flex w-1/3 flex-col bg-white p-6">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
@ -788,7 +787,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
onClick={() => {
const newQuery = { ...router.query };
delete newQuery.embedType;
delete newQuery.tabName;
delete newQuery.embedTabName;
router.push({
query: {
...newQuery,
@ -871,9 +870,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
embedType === "floating-popup" ? "" : "hidden"
)}>
<div className="text-sm">Button Text</div>
<div className="mb-2 text-sm">Button Text</div>
{/* Default Values should come from preview iframe */}
<TextField
name="buttonText"
@ -895,10 +894,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</div>
<div
className={classNames(
"mt-4 flex items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
"mt-4 flex items-center justify-start",
embedType === "floating-popup" ? "space-x-2" : "hidden"
)}>
<div className="text-sm">Display Calendar Icon Button</div>
<Switch
defaultChecked={true}
onCheckedChange={(checked) => {
@ -913,13 +911,14 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
}}
/>
<div className="text-sm">Display Calendar Icon Button</div>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
)}>
<div>Position of Button</div>
<div className="mb-2">Position of Button</div>
<Select
onChange={(position) => {
setPreviewState((previewState) => {
@ -936,13 +935,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={FloatingPopupPositionOptions}
/>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
)}>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div>Button Color</div>
<div className="w-36">
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
@ -959,13 +954,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/>
</div>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
)}>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div>Text Color</div>
<div className="w-36">
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
@ -1000,10 +991,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-6 text-sm">
<Label className="flex items-center justify-between">
<div>Theme</div>
<Label className="">
<div className="mb-2">Theme</div>
<Select
className="w-36"
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
@ -1030,9 +1021,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="flex items-center justify-between">
<div>{palette.title}</div>
<div className="w-36">
<Label key={palette.name} className="pb-4">
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
@ -1049,33 +1040,33 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</Collapsible>
</div>
</div>
<div className="w-2/3 bg-gray-50 p-6">
<NavTabs data-testid="embed-tabs" tabs={tabs} linkProps={{ shallow: true }} />
<div className="flex w-2/3 flex-col p-6">
<HorizontalTabs tabNameKey="embedTabName" data-testid="embed-tabs" tabs={tabs} />
{tabs.map((tab) => {
return (
<div
key={tab.tabName}
className={classNames(router.query.tabName === tab.tabName ? "block" : "hidden")}>
<div>
<div className={classNames(tab.type === "code" ? "h-[75vh]" : "")}>
{tab.type === "code" ? (
<tab.Component
embedType={embedType}
calLink={calLink}
previewState={previewState}
ref={refOfEmbedCodesRefs.current[tab.name]}
/>
) : (
<tab.Component
embedType={embedType}
calLink={calLink}
previewState={previewState}
ref={iframeRef}
/>
)}
</div>
<div className={router.query.tabName == "embed-preview" ? "block" : "hidden"} />
key={tab.embedTabName}
className={classNames(
router.query.embedTabName === tab.embedTabName ? "flex flex-grow flex-col" : "hidden"
)}>
<div className="flex h-[55vh] flex-grow flex-col">
{tab.type === "code" ? (
<tab.Component
embedType={embedType}
calLink={calLink}
previewState={previewState}
ref={refOfEmbedCodesRefs.current[tab.name]}
/>
) : (
<tab.Component
embedType={embedType}
calLink={calLink}
previewState={previewState}
ref={iframeRef}
/>
)}
</div>
<div className={router.query.embedTabName == "embed-preview" ? "block" : "hidden"} />
<div className="mt-8 flex flex-row-reverse gap-x-2">
{tab.type === "code" ? (
<Button

View File

@ -32,11 +32,6 @@ export default function AppCard(props: AppCardProps) {
</div>
<div className="flex items-center">
<h3 className="font-medium">{props.app.name}</h3>
{props.app.isProOnly && user?.plan === "FREE" ? (
<Badge className="ml-2" variant="default">
PRO
</Badge>
) : null}
</div>
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />

View File

@ -2,6 +2,7 @@ import React from "react";
import { SkeletonText } from "@calcom/ui";
/** @deprecated Use `apps/web/components/v2/availability/SkeletonLoader.tsx` */
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">

View File

@ -65,6 +65,7 @@ function BookingListItem(booking: BookingItemProps) {
};
const isUpcoming = new Date(booking.endTime) >= new Date();
const isPast = new Date(booking.endTime) < new Date();
const isCancelled = booking.status === BookingStatus.CANCELLED;
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
const isRejected = booking.status === BookingStatus.REJECTED;
@ -262,7 +263,7 @@ function BookingListItem(booking: BookingItemProps) {
</Dialog>
<tr className="flex hover:bg-neutral-50">
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-28" onClick={onClick}>
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-48" onClick={onClick}>
<div className="cursor-pointer py-4">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
@ -281,7 +282,7 @@ function BookingListItem(booking: BookingItemProps) {
))}>
<div className="text-gray-600 dark:text-white">
<Icon.FiRefreshCcw
stroke-width="3"
strokeWidth="3"
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
/>
<p className="mt-1 pl-5 text-xs">
@ -305,22 +306,11 @@ function BookingListItem(booking: BookingItemProps) {
</td>
<td className={"flex-1 px-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
<div className="cursor-pointer py-4">
{isPending && (
<Badge variant="orange" className="mb-2 ltr:mr-2 rtl:ml-2">
{t("unconfirmed")}
</Badge>
)}
{!!booking?.eventType?.price && !booking.paid && (
<Badge variant="orange" className="mb-2 ltr:mr-2 rtl:ml-2">
{t("pending_payment")}
</Badge>
)}
<div className="text-sm font-medium text-gray-900">
{startTime}:{" "}
<small className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
</small>
</div>
<div
title={booking.title}
className={classNames(
@ -334,11 +324,15 @@ function BookingListItem(booking: BookingItemProps) {
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
)}
{isPending && <Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>}
{isPending && (
<Badge variant="orange" className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">
{t("unconfirmed")}
</Badge>
)}
</div>
{booking.description && (
<div
className="max-w-52 md:max-w-96 truncate text-sm text-gray-500"
className="max-w-52 md:max-w-96 truncate text-sm text-gray-600"
title={booking.description}>
&quot;{booking.description}&quot;
</div>
@ -366,6 +360,7 @@ function BookingListItem(booking: BookingItemProps) {
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
</>
) : null}
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
{isCancelled && booking.rescheduled && (
<div className="hidden h-full items-center md:flex">
<RequestSentMessage />

View File

@ -2,6 +2,7 @@ import React from "react";
import { SkeletonText } from "@calcom/ui";
/** @deprecated Use `apps/web/components/v2/bookings/SkeletonLoader.tsx` */
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">

View File

@ -742,15 +742,6 @@ const BookingPage = ({
))}
{!eventType.disableGuests && (
<div className="mb-4">
{!guestToggle && (
<label
onClick={() => setGuestToggle(!guestToggle)}
htmlFor="guests"
className="mb-1 block text-sm font-medium hover:cursor-pointer dark:text-white">
{/*<UserAddIcon className="inline-block w-5 h-5 mr-1 -mt-1" />*/}
{t("additional_guests")}
</label>
)}
{guestToggle && (
<div>
<label
@ -858,8 +849,18 @@ const BookingPage = ({
</div>
<div className="flex justify-end space-x-2 rtl:space-x-reverse">
{!guestToggle && (
<Button
type="button"
color="secondary"
onClick={() => setGuestToggle(!guestToggle)}
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mr-auto rounded-md">
<Icon.FiUserPlus className="mr-2" />
{t("additional_guests")}
</Button>
)}
<Button
color="secondary"
color="minimal"
type="button"
onClick={() => router.back()}
// We override this for this component only for now - as we don't support darkmode everywhere in the app

View File

@ -7,6 +7,7 @@ import { useForm } from "react-hook-form";
import type { z } from "zod";
import classNames from "@calcom/lib/classNames";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
@ -338,7 +339,7 @@ function CreateEventTeamsItem(props: {
onSelect={() => props.openModal(props.option)}>
<Avatar
alt={props.option.name || ""}
imageSrc={props.option.image}
imageSrc={props.option.image || WEBAPP_URL + "/" + props.option.slug + "/avatar.png"}
size={6}
className="inline ltr:mr-2 rtl:ml-2"
/>

View File

@ -32,7 +32,7 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
<>
<div className={classNames("text-gray-600 dark:text-white", className)}>
{eventType.description && (
<h2 className="max-w-[280px] overflow-hidden text-ellipsis opacity-60 sm:max-w-[500px]">
<h2 className="max-w-[280px] overflow-hidden text-ellipsis sm:max-w-[500px]">
{eventType.description.substring(0, 100)}
{eventType.description.length > 100 && "..."}
</h2>

View File

@ -1,6 +1,7 @@
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
import { Icon } from "@calcom/ui/Icon";
/** @deprecated Use `apps/web/components/v2/eventtype/SkeletonLoader.tsx` */
function SkeletonLoader() {
return (
<SkeletonContainer>

View File

@ -27,7 +27,7 @@ const CalendarItem = (props: ICalendarItem) => {
type="button"
onClick={(event) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600`;
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
}}
className="ml-auto rounded-md border border-gray-200 py-[10px] px-4 text-sm font-bold">

View File

@ -31,10 +31,11 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
<img src={logo} alt={name} className="m-1 h-8 w-8" />
<div className="mx-4">
<p className="font-sans text-sm font-bold leading-5">
{name}{" "}
<span className="mx-1 rounded-[4px] bg-green-100 py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
{name}
{/* Temporarily removed till we use it on another place */}
{/* <span className="mx-1 rounded-[4px] bg-green-100 py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
{t("default")}
</span>
</span> */}
</p>
<div className="fle-row flex">
<span
@ -44,13 +45,13 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
</span>
</div>
</div>
<Button
{/* Temporarily removed */}
{/* <Button
color="minimal"
type="button"
className="ml-auto flex rounded-md border border-gray-200 py-[10x] px-4 font-sans text-sm">
{t("edit")}
</Button>
</Button> */}
</div>
<div className="h-[1px] w-full border-b border-gray-200" />
<div>

View File

@ -52,7 +52,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
{/* Create event on selected calendar */}
<CreateEventsOnCalendarSelect calendar={destinationCalendar} />
<p className="mt-7 text-sm text-gray-500">{t("connect_calendars_from_app_store")}</p>
<p className="mt-4 text-sm text-gray-500">{t("connect_calendars_from_app_store")}</p>
</>
)}

View File

@ -3,23 +3,16 @@ import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { Schedule } from "@calcom/features/schedules";
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Form } from "@calcom/ui/form/fields";
import { Button } from "@calcom/ui/v2";
import { DEFAULT_SCHEDULE } from "@lib/availability";
import type { Schedule as ScheduleType } from "@lib/types/schedule";
interface ISetupAvailabilityProps {
nextStep: () => void;
defaultScheduleId?: number | null;
defaultAvailability?: { schedule?: TimeRanges[][] };
}
interface ScheduleFormValues {
schedule: ScheduleType;
}
const SetupAvailability = (props: ISetupAvailabilityProps) => {
@ -37,7 +30,9 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
}
const availabilityForm = useForm({
defaultValues: { schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE },
defaultValues: {
schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE,
},
});
const mutationOptions = {
@ -51,7 +46,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const createSchedule = trpc.useMutation("viewer.availability.schedule.create", mutationOptions);
const updateSchedule = trpc.useMutation("viewer.availability.schedule.update", mutationOptions);
return (
<Form<ScheduleFormValues>
<Form
className="w-full bg-white text-black dark:bg-opacity-5 dark:text-white"
form={availabilityForm}
handleSubmit={async (values) => {
@ -75,7 +70,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
}
}
}}>
<Schedule />
<Schedule control={availabilityForm.control} name="schedule" weekStart={1} />
<div>
<Button

View File

@ -13,7 +13,7 @@ import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
import { Icon, StarIconSolid } from "@calcom/ui/Icon";
import { Input, Label } from "@calcom/ui/form/fields";
import { Input, Label } from "@calcom/ui/v2";
export enum UsernameChangeStatusEnum {
NORMAL = "NORMAL",
@ -210,7 +210,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="mt-2 flex rounded-md">
<span
className={classNames(
isInputUsernamePremium ? "border-2 border-orange-400 " : "",
isInputUsernamePremium ? "border-1 border-orange-400 " : "",
"hidden items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex"
)}>
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
@ -225,13 +225,14 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
autoCorrect="none"
disabled={disabled}
className={classNames(
"mt-0 rounded-md rounded-l-none border-l-2 focus:!ring-0",
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
isInputUsernamePremium
? "border-2 border-orange-400 focus:border-2 focus:border-orange-400"
: "border-2 focus:border-2",
? "border-1 focus:border-1 border-orange-400 focus:border-orange-400"
: "border-1 focus:border-2",
markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
: "border-l-gray-300 "
: "border-l-gray-300",
disabled ? "bg-gray-100 text-gray-400 focus:border-0" : ""
)}
value={inputUsernameValue}
onChange={(event) => {
@ -248,11 +249,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
usernameIsAvailable ? "" : ""
)}>
{isInputUsernamePremium ? <StarIconSolid className="mt-[4px] w-6" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? (
<Icon.FiCheck className="mt-[7px] w-6" />
) : (
<></>
)}
{!isInputUsernamePremium && usernameIsAvailable ? <Icon.FiCheck className="mt-2 w-6" /> : <></>}
</span>
</div>
</div>

View File

@ -10,7 +10,7 @@ import { AppRouter } from "@calcom/trpc/server/routers/_app";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { Input, Label } from "@calcom/ui/form/fields";
import { Input, Label } from "@calcom/ui/v2";
interface ICustomUsernameProps {
currentUsername: string | undefined;
@ -122,7 +122,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
autoCapitalize="none"
autoCorrect="none"
className={classNames(
"mt-0 rounded-md rounded-l-none",
"mb-0 mt-0 rounded-md rounded-l-none",
markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: ""
@ -136,7 +136,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
/>
{currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-0 flex flex-row">
<span className={classNames("mx-2 py-1")}>
<span className={classNames("mx-2 py-2")}>
{usernameIsAvailable ? <Icon.FiCheck className="mt-[4px] w-6" /> : <></>}
</span>
</div>

View File

@ -88,7 +88,6 @@ function Select<
: {
/** Light Theme starts */
primary: "var(--brand-color)",
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
/** Light Theme Ends */

View File

@ -114,11 +114,6 @@ const Component = ({
<div className="mb-4 flex items-center">
<img className="min-h-16 min-w-16 h-16 w-16" src={logo} alt={name} />
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
{isProOnly && user?.plan === "FREE" ? (
<Badge className="ml-2" variant="default">
PRO
</Badge>
) : null}
</div>
<h2 className="text-sm font-medium text-gray-600">
<Link href={`categories/${categories[0]}`}>

View File

@ -0,0 +1,46 @@
import React from "react";
import { SkeletonText } from "@calcom/ui/v2";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<li className="group flex w-full items-center justify-between px-2 py-[23px] sm:px-6">
<div className="flex-grow truncate text-sm">
<div className="flex flex-col space-y-2">
<SkeletonText className="h-4 w-32" />
<SkeletonText className="h-2 w-32" />
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
<SkeletonText className="h-6 w-12" />
</div>
</div>
</li>
);
}
export const AvailabilitySelectSkeletonLoader = () => {
return (
<li className="group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3">
<div className="flex-grow truncate text-sm">
<div className="flex justify-between">
<SkeletonText className="h-4 w-32" />
<SkeletonText className="h-4 w-4" />
</div>
</div>
</li>
);
};

View File

@ -0,0 +1,36 @@
import React from "react";
import { SkeletonText } from "@calcom/ui/v2";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<li className="group flex w-full items-center justify-between px-4 py-4 sm:px-6">
<div className="flex-grow truncate text-sm">
<div className="flex">
<div className="flex flex-col space-y-2">
<SkeletonText className="h-5 w-16" />
<SkeletonText className="h-4 w-32" />
</div>
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
<SkeletonText className="h-6 w-16" />
<SkeletonText className="h-6 w-32" />
</div>
</div>
</li>
);
}

View File

@ -11,7 +11,7 @@ import Button from "@calcom/ui/v2/core/Button";
import Select from "@calcom/ui/v2/core/form/Select";
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import { AvailabilitySelectSkeletonLoader } from "@components/v2/availability/SkeletonLoader";
type AvailabilityOption = {
label: string;
@ -117,7 +117,7 @@ export const AvailabilityTab = () => {
))}
</div>
) : (
<span className=" text-gray-500 opacity-50">{t("unavailable")}</span>
<span className="text-gray-500 opacity-50 ">{t("unavailable")}</span>
)}
</li>
);

View File

@ -243,6 +243,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<>
<div className="flex space-x-3 ">
<Switch
data-testid="hashedLinkCheck"
name="hashedLinkCheck"
fitToHeight={true}
defaultChecked={!!value}

View File

@ -6,7 +6,7 @@ import { useFormContext, Controller, useWatch } from "react-hook-form";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PeriodType } from "@calcom/prisma/client";
import { Select, Switch, Label } from "@calcom/ui/v2";
import { Select, Switch, Label, Input } from "@calcom/ui/v2";
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">) => {
@ -196,8 +196,8 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
</RadioGroup.Item>
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
<div className="flex ">
<input
<div className="flex h-9">
<Input
type="number"
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
@ -206,7 +206,7 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
/>
<select
id=""
className="block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
{...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>

View File

@ -191,7 +191,6 @@ export const EventSetupTab = (
{...formMethods.register("title")}
/>
<TextField
required
label={t("description")}
placeholder={t("quick_video_meeting")}
defaultValue={eventType.description ?? ""}

View File

@ -21,6 +21,7 @@ import {
HorizontalTabs,
Switch,
Label,
HorizontalTabItemProps,
} from "@calcom/ui/v2";
import { Dialog } from "@calcom/ui/v2/core/Dialog";
import Dropdown, {
@ -32,6 +33,7 @@ import Shell from "@calcom/ui/v2/core/Shell";
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
import { ClientSuspense } from "@components/ClientSuspense";
import { EmbedButton, EmbedDialog } from "@components/Embed";
type Props = {
children: React.ReactNode;
@ -80,7 +82,7 @@ function EventTypeSingleLayout({
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = [
const navigation: (VerticalTabItemProps & HorizontalTabItemProps)[] = [
{
name: "event_setup_tab_title",
tabName: "setup",
@ -123,7 +125,7 @@ function EventTypeSingleLayout({
icon: Icon.FiZap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
] as VerticalTabItemProps[];
];
// If there is a team put this navigation item within the tabs
if (team)
@ -152,8 +154,11 @@ function EventTypeSingleLayout({
eventType.slug
}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
return (
<Shell
backPath="/event-types"
title={t("event_type_title", { eventTypeTitle: eventType.title })}
heading={eventType.title}
subtitle={eventType.description || ""}
@ -198,8 +203,12 @@ function EventTypeSingleLayout({
showToast("Link copied!", "success");
}}
/>
{/* TODO: Implement embed here @hariom */}
{/* <Button color="secondary" size="icon" StartIcon={Icon.FiCode} /> */}
<EmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={Icon.FiCode}
color="secondary"
size="icon"
/>
<Button
color="secondary"
size="icon"
@ -244,7 +253,11 @@ function EventTypeSingleLayout({
</DropdownMenuContent>
</Dropdown>
<div className="border-l-2 border-gray-300" />
<Button className="ml-4 lg:ml-0" type="submit" form="event-type-form">
<Button
className="ml-4 lg:ml-0"
type="submit"
data-testid="update-eventtype"
form="event-type-form">
{t("save")}
</Button>
</div>
@ -252,10 +265,10 @@ function EventTypeSingleLayout({
<ClientSuspense fallback={<Loader />}>
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
<div className="hidden xl:block">
<VerticalTabs tabs={EventTypeTabs} sticky />
<VerticalTabs className="primary-navigation" tabs={EventTypeTabs} sticky />
</div>
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
<HorizontalTabs tabs={EventTypeTabs} />
<HorizontalTabs tabNameKey="tabName" tabs={EventTypeTabs} />
</div>
<div className="w-full ltr:mr-2 rtl:ml-2">
<div
@ -282,6 +295,7 @@ function EventTypeSingleLayout({
{t("delete_event_type_description") as string}
</ConfirmationDialogContent>
</Dialog>
<EmbedDialog />
</Shell>
);
}

View File

@ -41,6 +41,7 @@ export default function RecurringEventController({
<div className="flex space-x-3 ">
<Switch
name="requireConfirmation"
data-testid="recurring-event-check"
fitToHeight={true}
checked={recurringEventState !== null}
onCheckedChange={(e) => {

View File

@ -0,0 +1,47 @@
import { Icon } from "@calcom/ui/Icon";
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui/v2";
function SkeletonLoader() {
return (
<SkeletonContainer>
<div className="mb-4 flex items-center">
<SkeletonAvatar className="h-8 w-8" />
<div className="space-y-1">
<SkeletonText className="h-4 w-16" />
<SkeletonText className="h-4 w-24" />
</div>
</div>
<ul className="divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
</SkeletonContainer>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<li className="group flex w-full items-center justify-between px-4 py-4 sm:px-6">
<div className="flex-grow truncate text-sm">
<div>
<SkeletonText className="h-5 w-32" />
</div>
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<Icon.FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText className="h-4 w-12" />
</li>
<li className="flex items-center whitespace-nowrap">
<Icon.FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText className="h-4 w-16" />
</li>
</ul>
</div>
</div>
</li>
);
}

View File

@ -76,7 +76,7 @@ export function CalendarSwitch(props: {
/>
{props.defaultSelected && (
<Badge variant="gray">
<Icon.FiArrowLeft /> {t("adding_events_to")}
<Icon.FiArrowLeft className="mr-1" /> {t("adding_events_to")}
</Badge>
)}
</div>

View File

@ -32,10 +32,10 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
</div>
)}
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-8 sm:px-10">
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
{props.children}
</div>
<div className="mt-4 text-center text-sm text-neutral-600">{props.footerText}</div>
<div className="mt-8 text-center text-sm text-neutral-600">{props.footerText}</div>
</div>
</div>
);

View File

@ -1,14 +0,0 @@
import { Team, User } from "@prisma/client";
export function isSuccessRedirectAvailable(
eventType: {
users: {
plan: User["plan"];
}[];
} & {
team: Partial<Team> | null;
}
) {
// As Team Event is available in PRO plan only, just check if it's a team event.
return eventType.users[0]?.plan !== "FREE" || eventType.team;
}

View File

@ -18,6 +18,7 @@ const V2_WHITELIST = [
"/workflows",
"/apps",
"/success",
"/auth/login",
];
const V2_BLACKLIST = [
//
@ -42,11 +43,8 @@ const middleware: NextMiddleware = async (req) => {
return NextResponse.redirect(req.nextUrl);
}
}
/** Display available V2 pages to users who opted-in to early access */
/** Display available V2 pages */
if (
// ⬇ TODO: Remove this line for V2 launch
req.cookies.has("calcom-v2-early-access") &&
// ⬆ TODO: Remove this line for V2 launch
!V2_BLACKLIST.some((p) => url.pathname.startsWith(p)) &&
V2_WHITELIST.some((p) => url.pathname.startsWith(p))
) {

View File

@ -156,19 +156,17 @@ const nextConfig = {
},
{
source: "/settings",
destination: "/settings/profile",
destination: "/settings/my-account/profile",
permanent: true,
},
/* V2 testers get redirected to the new settings */
{
source: "/settings/profile",
has: [{ type: "cookie", key: "calcom-v2-early-access" }],
destination: "/settings/my-account/profile",
permanent: false,
},
{
source: "/settings/security",
has: [{ type: "cookie", key: "calcom-v2-early-access" }],
destination: "/settings/security/password",
permanent: false,
},

View File

@ -115,6 +115,7 @@
"short-uuid": "^4.2.0",
"stripe": "^9.16.0",
"superjson": "1.9.1",
"tailwindcss-radix": "^2.6.0",
"uuid": "^8.3.2",
"web3": "^1.7.5",
"zod": "^3.18.0"

View File

@ -4,7 +4,7 @@ import { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { Toaster } from "react-hot-toast";
import { JSONObject } from "superjson/dist/types";
@ -244,7 +244,6 @@ const getEventTypesWithHiddenFromDB = async (userId: number, plan: UserPlan) =>
metadata: true,
...baseEventTypeSelect,
},
take: plan === UserPlan.FREE ? 1 : undefined,
});
};

View File

@ -93,12 +93,9 @@ async function getUserPageProps(context: GetStaticPropsContext) {
if (!user) return { notFound: true };
const eventTypeIds = user.eventTypes.map((e) => e.id);
const eventTypes = await prisma.eventType.findMany({
where: {
slug,
/* Free users can only display their first eventType */
id: user.plan === UserPlan.FREE ? eventTypeIds[0] : undefined,
OR: [{ userId: user.id }, { users: { some: { id: user.id } } }],
},
// Order is important to ensure that given a slug if there are duplicates, we choose the same event type consistently when showing in event-types list UI(in terms of ordering and disabled event types)

View File

@ -115,7 +115,7 @@ const providers: Provider[] = [
};
},
}),
ImpersonationProvider,
// ImpersonationProvider,
];
if (IS_GOOGLE_LOGIN_ENABLED) {

View File

@ -36,14 +36,12 @@ import { Icon } from "@calcom/ui/Icon";
import Shell from "@calcom/ui/Shell";
import Switch from "@calcom/ui/Switch";
import { Tooltip } from "@calcom/ui/Tooltip";
import { UpgradeToProDialog } from "@calcom/ui/UpgradeToProDialog";
import { Form } from "@calcom/ui/form/fields";
import { QueryCell } from "@lib/QueryCell";
import { asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
import { slugify } from "@lib/slugify";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -132,8 +130,6 @@ const SuccessRedirectEdit = <T extends UseFormReturn<FormValues>>({
formMethods: T;
}) => {
const { t } = useLocale();
const proUpgradeRequired = !isSuccessRedirectAvailable(eventType);
const [modalOpen, setModalOpen] = useState(false);
return (
<>
@ -144,19 +140,12 @@ const SuccessRedirectEdit = <T extends UseFormReturn<FormValues>>({
htmlFor="successRedirectUrl"
className="flex h-full items-center text-sm font-medium text-neutral-700">
{t("redirect_success_booking")}
<span className="ml-1">{proUpgradeRequired && <Badge variant="default">PRO</Badge>}</span>
</label>
</div>
<div className="w-full">
<input
id="successRedirectUrl"
onClick={(e) => {
if (proUpgradeRequired) {
e.preventDefault();
setModalOpen(true);
}
}}
readOnly={proUpgradeRequired}
readOnly={eventType.team !== undefined}
type="url"
className="block w-full rounded-sm border-gray-300 text-sm"
placeholder={t("external_redirect_url")}
@ -164,9 +153,6 @@ const SuccessRedirectEdit = <T extends UseFormReturn<FormValues>>({
{...formMethods.register("successRedirectUrl")}
/>
</div>
<UpgradeToProDialog modalOpen={modalOpen} setModalOpen={setModalOpen}>
{t("redirect_url_upgrade_description")}
</UpgradeToProDialog>
</div>
</>
);

View File

@ -36,11 +36,11 @@ import { HttpError } from "@lib/core/http/error";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import { LinkText } from "@components/ui/LinkText";
import NoCalendarConnectedAlert from "@components/ui/NoCalendarConnectedAlert";
import SkeletonLoader from "@components/v2/eventtype/SkeletonLoader";
import { TRPCClientError } from "@trpc/react";
@ -66,10 +66,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
return (
<Link href={"/event-types/" + type.id}>
<a
className={classNames(
"flex-grow truncate text-sm ",
type.$disabled && "pointer-events-none cursor-not-allowed opacity-30"
)}
className="flex-grow truncate text-sm"
title={`${type.title} ${type.description ? ` ${type.description}` : ""}`}>
<div>
<span
@ -137,7 +134,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
return {
eventTypeGroups: [],
profiles: [],
viewer: { canAddEvents: false, plan: UserPlan.FREE },
viewer: { canAddEvents: true, plan: UserPlan.PRO },
};
return {
...data,
@ -217,42 +214,28 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${CAL_URL}/${embedLink}`;
return (
<li
key={type.id}
className={classNames(type.$disabled && "select-none")}
data-disabled={type.$disabled ? 1 : 0}>
<div
className={classNames(
"flex items-center justify-between hover:bg-neutral-50 ",
type.$disabled && "hover:bg-white"
)}>
<div
className={classNames(
"group flex w-full items-center justify-between px-4 py-4 pr-0 hover:bg-neutral-50 sm:px-6",
type.$disabled && "hover:bg-white"
)}>
{types.length > 1 && !type.$disabled && (
<>
<button
className="invisible absolute left-[3px] -mt-4 mb-4 -ml-4 hidden h-7 w-7 scale-0 items-center justify-center rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[34px]"
onClick={() => moveEventType(index, -1)}>
<Icon.FiArrowUp className="h-5 w-5" />
</button>
<li key={type.id}>
<div className="flex items-center justify-between hover:bg-neutral-50">
<div className="group flex w-full items-center justify-between px-4 py-4 pr-0 hover:bg-neutral-50 sm:px-6">
<button
className="invisible absolute left-[3px] -mt-4 mb-4 -ml-4 hidden h-7 w-7 scale-0 items-center justify-center rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[34px]"
onClick={() => moveEventType(index, -1)}>
<Icon.FiArrowUp className="h-5 w-5" />
</button>
<button
className="invisible absolute left-[3px] mt-8 -ml-4 hidden h-7 w-7 scale-0 items-center justify-center rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[34px]"
onClick={() => moveEventType(index, 1)}>
<Icon.FiArrowDown className="h-5 w-5" />
</button>
<button
className="invisible absolute left-[3px] mt-8 -ml-4 hidden h-7 w-7 scale-0 items-center justify-center rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[34px]"
onClick={() => moveEventType(index, 1)}>
<Icon.FiArrowDown className="h-5 w-5" />
</button>
</>
)}
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.users?.length > 1 && (
<AvatarGroup
border="border-2 border-white"
className={classNames("relative top-1 right-3", type.$disabled && " opacity-30")}
className="relative top-1 right-3"
size={8}
truncateAfter={4}
items={type.users.map((organizer) => ({
@ -262,11 +245,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}))}
/>
)}
<div
className={classNames(
"flex justify-between space-x-2 rtl:space-x-reverse ",
type.$disabled && "pointer-events-none cursor-not-allowed"
)}>
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
<Tooltip side="top" content={t("preview") as string}>
<Button
target="_blank"
@ -274,7 +253,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
size="icon"
color="minimal"
className={classNames(!type.$disabled && "group-hover:text-black")}
className="group-hover:text-black"
StartIcon={Icon.FiExternalLink}
href={calLink}
/>
@ -285,7 +264,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
size="icon"
color="minimal"
className={classNames(type.$disabled ? " opacity-30" : "group-hover:text-black")}
className="group-hover:text-black"
StartIcon={Icon.FiLink}
onClick={() => {
showToast(t("link_copied"), "success");
@ -300,7 +279,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
size="icon"
color="minimal"
className={classNames(type.$disabled ? " opacity-30" : "group-hover:text-black")}
className="group-hover:text-black"
StartIcon={Icon.FiMoreHorizontal}
/>
</DropdownMenuTrigger>
@ -310,7 +289,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<Button
type="button"
size="sm"
disabled={type.$disabled}
color="minimal"
StartIcon={Icon.FiEdit2}
className="w-full">
@ -323,7 +301,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
color="minimal"
size="sm"
disabled={type.$disabled}
data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy}
onClick={() => openModal(group, type)}>
@ -337,10 +314,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
as={Button}
StartIcon={Icon.FiCode}
className={classNames(
"w-full rounded-none",
type.$disabled && " pointer-events-none cursor-not-allowed opacity-30"
)}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
@ -370,13 +344,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="mr-5 flex flex-shrink-0 sm:hidden">
<Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button
type="button"
size="icon"
color="minimal"
className={classNames(type.$disabled && " opacity-30")}
StartIcon={Icon.FiMoreHorizontal}
/>
<Button type="button" size="icon" color="minimal" StartIcon={Icon.FiMoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuContent portalled>
<DropdownMenuItem className="outline-none">
@ -555,9 +523,7 @@ const CTA = () => {
if (!query.data) return null;
return (
<CreateEventTypeButton canAddEvents={query.data.viewer.canAddEvents} options={query.data.profiles} />
);
return <CreateEventTypeButton canAddEvents={true} options={query.data.profiles} />;
};
const WithQuery = withQuery(["viewer.eventTypes"]);
@ -582,23 +548,6 @@ const EventTypesPage = () => {
customLoader={<SkeletonLoader />}
success={({ data }) => (
<>
{data.viewer.plan === "FREE" && !data.viewer.canAddEvents && (
<Alert
severity="warning"
title={<>{t("plan_upgrade")}</>}
message={
<Trans i18nKey="plan_upgrade_instructions">
You can
<LinkText href="/api/upgrade" classNameChildren="underline">
upgrade here
</LinkText>
.
</Trans>
}
className="mb-4"
/>
)}
<NoCalendarConnectedAlert />
{data.eventTypeGroups.map((group, index) => (

View File

@ -1,4 +1,5 @@
import { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Head from "next/head";
import { useRouter } from "next/router";
import { z } from "zod";
@ -180,6 +181,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
...(await serverSideTranslations(context.locale ?? "", ["common"])),
user: {
...user,
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),

View File

@ -9,9 +9,10 @@ export default function MorePage() {
<Shell>
<div className="mt-8 max-w-screen-lg">
<MobileNavigationMoreItems />
<div className="mt-6">
{/* Save it for next preview version
<div className="mt-6">
<UserV2OptInBanner />
</div>
</div> */}
<p className="mt-6 text-xs leading-tight text-gray-500 md:hidden">{t("more_page_footer")}</p>
</div>
</Shell>

View File

@ -29,7 +29,6 @@ import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import { UpgradeToProDialog } from "@calcom/ui/UpgradeToProDialog";
import { Form, PasswordField } from "@calcom/ui/form/fields";
import { Label } from "@calcom/ui/form/fields";
import { withQuery } from "@lib/QueryCell";
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
@ -508,7 +507,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
<div className="text-sm ltr:ml-3 rtl:mr-3">
<label htmlFor="hide-branding" className="font-medium text-gray-700">
{t("disable_cal_branding")}{" "}
{user.plan !== "PRO" && <Badge variant="default">PRO</Badge>}
{user.plan !== "PRO" && <Badge variant="default">TEAM</Badge>}
</label>
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
</div>

View File

@ -125,19 +125,6 @@ export function TeamSettingsPage() {
className="mb-4 "
/>
)}
{team.membership.role === MembershipRole.OWNER && team.requiresUpgrade && (
<Alert
severity="warning"
title={t("upgrade_to_flexible_pro_title")}
message={
<span>
{t("upgrade_to_flexible_pro_message")} <br />
<UpgradeToFlexibleProModal teamId={team.id} />
</span>
}
className="mb-4"
/>
)}
</>
)}

View File

@ -36,7 +36,6 @@ import { EmailInput } from "@calcom/ui/form/fields";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking";
@ -259,7 +258,7 @@ export default function Success(props: SuccessProps) {
);
const customInputs = bookingInfo?.customInputs;
const locationToDisplay = getSuccessPageLocationMessage(location, t);
const locationToDisplay = getSuccessPageLocationMessage(location, t, props?.bookingInfo?.status);
return (
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
@ -276,9 +275,7 @@ export default function Success(props: SuccessProps) {
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
{isSuccessRedirectAvailable(eventType) && eventType.successRedirectUrl ? (
<RedirectionToast url={eventType.successRedirectUrl} />
) : null}{" "}
{eventType.successRedirectUrl ? <RedirectionToast url={eventType.successRedirectUrl} /> : null}{" "}
<div
className={classNames(
shouldAlignCentrally ? "text-center" : "",
@ -825,6 +822,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
select: {
title: true,
uid: true,
status: true,
description: true,
customInputs: true,
user: {

View File

@ -5,13 +5,14 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Icon } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
import { Icon } from "@calcom/ui/Icon";
import { Button, EmailField, Form, PasswordField } from "@calcom/ui/v2";
import SAMLLogin from "@calcom/ui/v2/modules/auth/SAMLLogin";
@ -22,7 +23,7 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import AddToHomescreen from "@components/AddToHomescreen";
import TwoFactor from "@components/auth/TwoFactor";
import AuthContainer from "@components/ui/AuthContainer";
import AuthContainer from "@components/v2/ui/AuthContainer";
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
import { ssrInit } from "@server/lib/ssr";
@ -76,12 +77,9 @@ export default function Login({
callbackUrl = safeCallbackUrl || "";
const LoginFooter = (
<span className="text-gray-600">
{t("dont_have_an_account")}{" "}
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
{t("create_an_account")}
</a>
</span>
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
{t("i_dont_have_an_account")}
</a>
);
const TwoFactorFooter = (
@ -106,7 +104,6 @@ export default function Login({
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}>
<Form
form={form}
className="space-y-6"
handleSubmit={async (values) => {
setErrorMessage(null);
telemetry.event(telemetryEventTypes.login, collectPageParameters());
@ -132,51 +129,52 @@ export default function Login({
{...form.register("csrfToken")}
/>
</div>
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
<EmailField
id="email"
label={t("email_address")}
defaultValue={router.query.email as string}
placeholder="john.doe@example.com"
required
{...form.register("email")}
/>
<div className="relative">
<div className="absolute right-0 -top-[2px]">
<Link href="/auth/forgot-password">
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
{t("forgot_password")}
</a>
</Link>
</div>
<PasswordField
id="password"
type="password"
autoComplete="current-password"
<div className="space-y-6">
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
<EmailField
id="email"
label={t("email_address")}
defaultValue={router.query.email as string}
placeholder="john.doe@example.com"
required
{...form.register("password")}
{...form.register("email")}
/>
<div className="relative">
<div className="absolute right-0 -top-[6px]">
<Link href="/auth/forgot-password">
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
{t("forgot")}
</a>
</Link>
</div>
<PasswordField
id="password"
autoComplete="current-password"
required
className="mb-0"
{...form.register("password")}
/>
</div>
</div>
</div>
{twoFactorRequired && <TwoFactor center />}
{twoFactorRequired && <TwoFactor center />}
{errorMessage && <Alert severity="error" title={errorMessage} />}
<div className="pb-8">
<Button type="submit" color="primary" disabled={isSubmitting} className="w-full">
{errorMessage && <Alert severity="error" title={errorMessage} />}
<Button type="submit" color="primary" disabled={isSubmitting} className="w-full justify-center">
{twoFactorRequired ? t("submit") : t("sign_in")}
</Button>
</div>
</Form>
<hr />
{!twoFactorRequired && (
<>
{isGoogleLoginEnabled && (
<div className="mt-8">
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="my-8" />}
<div className="space-y-3">
{isGoogleLoginEnabled && (
<Button
color="secondary"
className="w-full"
className="w-full justify-center"
data-testid="google"
StartIcon={FaGoogle}
onClick={async (e) => {
e.preventDefault();
// track Google logins. Without personal data/payload
@ -185,17 +183,17 @@ export default function Login({
}}>
{t("signin_with_google")}
</Button>
</div>
)}
{isSAMLLoginEnabled && (
<SAMLLogin
email={form.getValues("email")}
samlTenantID={samlTenantID}
samlProductID={samlProductID}
hostedCal={hostedCal}
setErrorMessage={setErrorMessage}
/>
)}
)}
{isSAMLLoginEnabled && (
<SAMLLogin
email={form.getValues("email")}
samlTenantID={samlTenantID}
samlProductID={samlProductID}
hostedCal={hostedCal}
setErrorMessage={setErrorMessage}
/>
)}
</div>
</>
)}
</AuthContainer>

View File

@ -1,40 +1,65 @@
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { Schedule } from "@calcom/features/schedules";
import { availabilityAsString, DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import Schedule from "@calcom/features/schedules/components/Schedule";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { BadgeCheckIcon } from "@calcom/ui/Icon";
import Shell from "@calcom/ui/Shell";
import { trpc } from "@calcom/trpc/react";
import type { Schedule as ScheduleType } from "@calcom/types/schedule";
import { Icon } from "@calcom/ui";
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
import { Button, Form, showToast, Switch, TextField } from "@calcom/ui/v2";
import Button from "@calcom/ui/v2/core/Button";
import Shell from "@calcom/ui/v2/core/Shell";
import Switch from "@calcom/ui/v2/core/Switch";
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
import { Form, Label } from "@calcom/ui/v2/core/form/fields";
import showToast from "@calcom/ui/v2/core/notifications";
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
import { QueryCell } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import EditableHeading from "@components/ui/EditableHeading";
export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) {
const { t } = useLocale();
const querySchema = z.object({
schedule: stringOrNumber,
});
type AvailabilityFormValues = {
name: string;
schedule: ScheduleType;
timeZone: string;
isDefault: boolean;
};
export default function Availability({ schedule }: { schedule: number }) {
const { t, i18n } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const form = useForm({
defaultValues: {
schedule: props.availability || DEFAULT_SCHEDULE,
isDefault: !!props.isDefault,
timeZone: props.timeZone,
},
});
const { data, isLoading } = trpc.useQuery(["viewer.availability.schedule", { scheduleId: schedule }]);
const form = useForm<AvailabilityFormValues>();
const { control, reset, setValue } = form;
useEffect(() => {
if (!isLoading && data) {
reset({
name: data?.schedule?.name,
schedule: data.availability,
timeZone: data.timeZone,
isDefault: data.isDefault,
});
}
}, [data, isLoading, reset]);
const updateMutation = trpc.useMutation("viewer.availability.schedule.update", {
onSuccess: async ({ schedule }) => {
await utils.invalidateQueries(["viewer.availability.schedule"]);
await utils.refetchQueries(["viewer.availability.schedule"]);
await router.push("/availability");
showToast(
t("availability_updated_successfully", {
@ -52,112 +77,100 @@ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.sc
});
return (
<Form
form={form}
handleSubmit={async (values) => {
updateMutation.mutate({
scheduleId: parseInt(router.query.schedule as string, 10),
name: props.schedule.name,
...values,
});
}}
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1">
<div className="rounded-md border-gray-200 bg-white px-4 py-5 sm:border sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">{t("change_start_end")}</h3>
<Schedule />
</div>
<div className="flex justify-end px-4 pt-4 sm:px-0">
<Button>{t("save")}</Button>
</div>
</div>
<div className="min-w-40 col-span-3 ml-2 space-y-2 lg:col-span-1">
{props.isDefault ? (
<span className="flex items-center">
<BadgeCheckIcon className="mr-1 h-4 w-4" /> {t("default")}
</span>
) : (
<Controller
name="isDefault"
render={({ field: { onChange, value } }) => (
<Switch label={t("set_to_default")} onCheckedChange={onChange} checked={value} />
)}
/>
)}
<div className="xl:max-w-80 w-full">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<div className="mt-1">
<Controller
name="timeZone"
render={({ field: { onChange, value } }) => (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
)}
/>
</div>
</div>
<div className="pt-6">
<TextField
name="aciveOn"
label={t("active_on")}
disabled
value={t("nr_event_type_other", { count: props.schedule.eventType?.length })}
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 text-sm"
<Shell
backPath="/availability"
title={t("availability_title", { availabilityTitle: data?.schedule.name })}
heading={
<EditableHeading title={data?.schedule.name || ""} onChange={(name) => setValue("name", name)} />
}
subtitle={data?.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language })}
<br />
</span>
))}
CTA={
<div className="flex items-center justify-end">
<div className="flex items-center rounded-md px-2 sm:hover:bg-gray-100">
<Label htmlFor="hiddenSwitch" className="mt-2 hidden cursor-pointer self-center pr-2 sm:inline">
{t("set_to_default")}
</Label>
<Switch
id="hiddenSwitch"
disabled={isLoading}
checked={form.watch("isDefault")}
onCheckedChange={(e) => {
form.setValue("isDefault", e);
}}
/>
</div>
<hr className="my-8" />
<div className="rounded-md">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
<VerticalDivider />
<div className="border-l-2 border-gray-300" />
<Button className="ml-4 lg:ml-0" type="submit" form="availability-form">
{t("save")}
</Button>
</div>
}>
<div className="flex items-baseline sm:mt-0">
{/* TODO: Find a better way to guarantee alignment, but for now this'll do. */}
<Icon.FiArrowLeft className=" mr-3 text-transparent hover:cursor-pointer" />
<div className="w-full">
<Form
form={form}
id="availability-form"
handleSubmit={async (values) => {
updateMutation.mutate({
scheduleId: schedule,
...values,
});
}}
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1">
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
{t("change_start_end")}
</h3>
<Schedule control={control} name="schedule" />
</div>
</div>
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Controller
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SkeletonText className="h-6 w-full" />
)
}
/>
</div>
<hr className="my-8" />
<div className="rounded-md">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
</div>
</div>
</div>
</div>
</Form>
</div>
</div>
</Form>
);
}
const querySchema = z.object({
schedule: stringOrNumber,
});
export default function Availability() {
const router = useRouter();
const { i18n } = useLocale();
const { schedule: scheduleId } = router.isReady ? querySchema.parse(router.query) : { schedule: -1 };
const query = trpc.useQuery(["viewer.availability.schedule", { scheduleId }], { enabled: router.isReady });
const [name, setName] = useState<string>();
return (
<div>
<QueryCell
query={query}
success={({ data }) => {
return (
<Shell
heading={<EditableHeading title={name || data.schedule.name} onChange={setName} />}
subtitle={data.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language })}
<br />
</span>
))}>
<AvailabilityForm
{...{ ...data, schedule: { ...data.schedule, name: name || data.schedule.name } }}
/>
</Shell>
);
}}
/>
</div>
</Shell>
);
}

View File

@ -11,7 +11,7 @@ import { EmptyScreen, showToast } from "@calcom/ui/v2";
import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import SkeletonLoader from "@components/availability/SkeletonLoader";
import SkeletonLoader from "@components/v2/availability/SkeletonLoader";
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
const { t } = useLocale();

View File

@ -15,7 +15,7 @@ import BookingLayout from "@calcom/ui/v2/core/layouts/BookingLayout";
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
import SkeletonLoader from "@components/v2/bookings/SkeletonLoader";
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
type BookingOutput = inferQueryOutput<"viewer.bookings">["bookings"][0];
@ -98,7 +98,7 @@ export default function Bookings() {
<div className="pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
{/* TODO: add today only for the current day
<p className="pb-3 text-xs font-medium uppercase leading-4 text-gray-500">{t("today")}</p>
<p className="pb-3 text-xs font-medium leading-4 text-gray-500 uppercase">{t("today")}</p>
*/}
<div className="overflow-hidden rounded-md border border-gray-200">

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import autoAnimate from "@formkit/auto-animate";
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
@ -17,7 +18,7 @@ import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc/react";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Form } from "@calcom/ui/form/fields";
import { Button, showToast } from "@calcom/ui/v2";
import { showToast } from "@calcom/ui/v2";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
@ -37,8 +38,6 @@ import EventWorkflowsTab from "@components/v2/eventtype/EventWorkfowsTab";
import { getTranslation } from "@server/lib/i18n";
const TABS_WITHOUT_ACTION_BUTTONS = ["workflows", "availability"];
export type FormValues = {
title: string;
eventTitle: string;
@ -245,16 +244,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div ref={animationParentRef} className="space-y-6">
{tabMap[tabName]}
</div>
{!TABS_WITHOUT_ACTION_BUTTONS.includes(tabName) && (
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
<Button href="/event-types" color="secondary" tabIndex={-1}>
{t("cancel")}
</Button>
<Button type="submit" data-testid="update-eventtype" disabled={updateMutation.isLoading}>
{t("update")}
</Button>
</div>
)}
</Form>
</EventTypeSingleLayout>
);

View File

@ -1,5 +1,4 @@
import { UserPlan } from "@prisma/client";
import { Trans } from "next-i18next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
@ -9,8 +8,7 @@ import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
import { Dialog, EmptyScreen, Badge, Button, Tooltip, Switch, showToast, ButtonGroup } from "@calcom/ui/v2";
import { Badge, Button, ButtonGroup, Dialog, EmptyScreen, showToast, Switch, Tooltip } from "@calcom/ui/v2";
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
import Dropdown, {
DropdownItem,
@ -20,18 +18,16 @@ import Dropdown, {
DropdownMenuTrigger,
} from "@calcom/ui/v2/core/Dropdown";
import Shell from "@calcom/ui/v2/core/Shell";
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
import CreateEventTypeButton from "@calcom/ui/v2/modules/event-types/CreateEventType";
import { withQuery } from "@lib/QueryCell";
import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import SkeletonLoader from "@components/v2/eventtype/SkeletonLoader";
import { TRPCClientError } from "@trpc/react";
@ -58,10 +54,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
return (
<Link href={`/event-types/${type.id}`}>
<a
className={classNames(
"flex-grow truncate text-sm ",
type.$disabled && "pointer-events-none cursor-not-allowed opacity-30"
)}
className="flex-grow truncate text-sm"
title={`${type.title} ${type.description ? ` ${type.description}` : ""}`}>
<div>
<span
@ -131,7 +124,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
return {
eventTypeGroups: [],
profiles: [],
viewer: { canAddEvents: false, plan: UserPlan.FREE },
viewer: { canAddEvents: true, plan: UserPlan.PRO },
};
return {
...data,
@ -204,6 +197,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}
}, []);
const firstItem = types[0];
const lastItem = types[types.length - 1];
return (
<div className="mb-16 flex overflow-hidden rounded-md border border-gray-200 bg-white">
<ul className="w-full divide-y divide-neutral-200" data-testid="event-types">
@ -211,34 +206,23 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${CAL_URL}/${embedLink}`;
return (
<li
key={type.id}
className={classNames(type.$disabled && "select-none")}
data-disabled={type.$disabled ? 1 : 0}>
<div
className={classNames(
"flex items-center justify-between hover:bg-neutral-50",
type.$disabled && "hover:bg-white"
)}>
<div
className={classNames(
"group flex w-full items-center justify-between px-4 py-4 pr-0 sm:px-6",
type.$disabled && "hover:bg-white"
)}>
{types.length > 1 && !type.$disabled && (
<>
<button
className="invisible absolute left-[5px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, -1)}>
<Icon.FiArrowUp className="h-5 w-5" />
</button>
<li key={type.id}>
<div className="flex items-center justify-between hover:bg-neutral-50">
<div className="group flex w-full items-center justify-between px-4 py-4 pr-0 sm:px-6">
{!(firstItem && firstItem.id === type.id) && (
<button
className="invisible absolute left-[5px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, -1)}>
<Icon.FiArrowUp className="h-5 w-5" />
</button>
)}
<button
className="invisible absolute left-[5px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, 1)}>
<Icon.FiArrowDown className="h-5 w-5" />
</button>
</>
{!(lastItem && lastItem.id === type.id) && (
<button
className="invisible absolute left-[5px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, 1)}>
<Icon.FiArrowDown className="h-5 w-5" />
</button>
)}
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
@ -246,7 +230,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{type.users?.length > 1 && (
<AvatarGroup
border="border-2 border-white"
className={classNames("relative top-1 right-3", type.$disabled && " opacity-30")}
className="relative top-1 right-3"
size={8}
truncateAfter={4}
items={type.users.map((organizer) => ({
@ -256,11 +240,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}))}
/>
)}
<div
className={classNames(
"flex items-center justify-between space-x-2 rtl:space-x-reverse ",
type.$disabled && "pointer-events-none cursor-not-allowed"
)}>
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
{type.hidden && (
<Badge variant="gray" size="lg">
{t("hidden")}
@ -286,7 +266,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
size="icon"
href={calLink}
StartIcon={Icon.FiExternalLink}
disabled={type.$disabled}
combined
/>
</Tooltip>
@ -296,7 +275,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="secondary"
size="icon"
StartIcon={Icon.FiLink}
disabled={type.$disabled}
onClick={() => {
showToast(t("link_copied"), "success");
navigator.clipboard.writeText(calLink);
@ -319,7 +297,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownItem
type="button"
href={"/event-types/" + type.id}
disabled={type.$disabled}
StartIcon={Icon.FiEdit2}>
{t("edit") as string}
</DropdownItem>
@ -328,7 +305,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownItem
type="button"
data-testid={"event-type-duplicate-" + type.id}
disabled={type.$disabled}
StartIcon={Icon.FiCopy}
onClick={() => openModal(group, type)}>
{t("duplicate") as string}
@ -339,10 +315,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
as={DropdownItem}
type="button"
StartIcon={Icon.FiCode}
className={classNames(
"w-full rounded-none",
type.$disabled && " pointer-events-none cursor-not-allowed opacity-30"
)}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
@ -357,7 +330,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
setDeleteDialogTypeId(type.id);
}}
StartIcon={Icon.FiTrash}
disabled={type.$disabled}
className="w-full rounded-none">
{t("delete") as string}
</DropdownItem>
@ -373,13 +345,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="mr-5 flex flex-shrink-0 sm:hidden">
<Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button
type="button"
size="icon"
color="secondary"
className={classNames(type.$disabled && " opacity-30")}
StartIcon={Icon.FiMoreHorizontal}
/>
<Button type="button" size="icon" color="secondary" StartIcon={Icon.FiMoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuContent portalled>
<DropdownMenuItem className="outline-none">
@ -548,9 +514,7 @@ const CTA = () => {
if (!query.data) return null;
return (
<CreateEventTypeButton canAddEvents={query.data.viewer.canAddEvents} options={query.data.profiles} />
);
return <CreateEventTypeButton canAddEvents={true} options={query.data.profiles} />;
};
const WithQuery = withQuery(["viewer.eventTypes"]);
@ -571,22 +535,6 @@ const EventTypesPage = () => {
customLoader={<SkeletonLoader />}
success={({ data }) => (
<>
{data.viewer.plan === "FREE" && !data.viewer.canAddEvents && (
<Alert
severity="warning"
title={<>{t("plan_upgrade")}</>}
message={
<Trans i18nKey="plan_upgrade_instructions">
You can
<a href="/api/upgrade" className="underline">
upgrade here
</a>
.
</Trans>
}
className="mb-4"
/>
)}
{data.eventTypeGroups.map((group, index) => (
<Fragment key={group.profile.slug}>
{/* hide list heading when there is only one (current user) */}

View File

@ -4,7 +4,7 @@ import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
function AdminAppsView() {
return (
<>
<Meta title="apps" description="apps_description" />
<Meta title="Apps" description="apps_description" />
<h1>App listing</h1>
</>
);

View File

@ -12,7 +12,7 @@ function AdminView() {
return (
<>
<Meta title="admin" description="impersonation" />
<Meta title="Admin" description="Impersonation" />
<form
className="mb-6 w-full sm:w-1/2"
onSubmit={(e) => {

View File

@ -4,7 +4,7 @@ import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
function AdminAppsView() {
return (
<>
<Meta title="admin" description="admin_description" />
<Meta title="Admin" description="admin_description" />
<h1>Admin index</h1>
</>
);

View File

@ -4,7 +4,7 @@ import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
function AdminUsersView() {
return (
<>
<Meta title="users" description="users_description" />
<Meta title="Users" description="users_description" />
<h1>Users listing</h1>
</>
);

View File

@ -7,10 +7,10 @@ import ApiKeyListItem from "@calcom/features/ee/api-keys/components/v2/ApiKeyLis
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
import { EmptyScreen, Button } from "@calcom/ui/v2";
import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog";
import Meta from "@calcom/ui/v2/core/Meta";
import SkeletonLoader from "@calcom/ui/v2/core/apps/SkeletonLoader";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
const ApiKeysView = () => {

View File

@ -1,27 +1,43 @@
import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import { Controller, useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc/react";
import Badge from "@calcom/ui/v2/core/Badge";
import { Button } from "@calcom/ui/v2/core/Button";
import Meta from "@calcom/ui/v2/core/Meta";
import Switch from "@calcom/ui/v2/core/Switch";
import ColorPicker from "@calcom/ui/v2/core/colorpicker";
import Select from "@calcom/ui/v2/core/form/Select";
import { Form } from "@calcom/ui/v2/core/form/fields";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import showToast from "@calcom/ui/v2/core/notifications";
import { SkeletonContainer, SkeletonText, SkeletonButton } from "@calcom/ui/v2/core/skeleton";
import { getSession } from "@lib/auth";
import { inferSSRProps } from "@lib/types/inferSSRProps";
const SkeletonLoader = () => {
return (
<SkeletonContainer>
<div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center">
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
</div>
<div className="flex justify-between">
<SkeletonText className="h-8 w-1/3" />
<SkeletonText className="h-8 w-1/3" />
</div>
const AppearanceView = (props: inferSSRProps<typeof getServerSideProps>) => {
<SkeletonText className="h-8 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</div>
</SkeletonContainer>
);
};
const AppearanceView = () => {
const { t } = useLocale();
const { user } = props;
const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
const mutation = trpc.useMutation("viewer.updateProfile", {
onSuccess: () => {
showToast(t("settings_updated_successfully"), "success");
@ -31,183 +47,164 @@ const AppearanceView = (props: inferSSRProps<typeof getServerSideProps>) => {
},
});
const themeOptions = [
{ value: "light", label: t("light") },
{ value: "dark", label: t("dark") },
];
const formMethods = useForm();
return (
<Form
form={formMethods}
handleSubmit={(values) => {
mutation.mutate({
...values,
theme: values.theme.value,
});
}}>
<Meta title="appearance" description="appearance_description" />
<Controller
name="theme"
control={formMethods.control}
defaultValue={user.theme}
render={({ field: { value } }) => (
<>
<div className="flex items-center text-sm ">
<div className="mr-1 flex-grow">
<p className="font-semibold ">{t("follow_system_preferences")}</p>
<p className="mt-0.5 leading-5 text-gray-600">
<Trans i18nKey="system_preference_description">
Automatically adjust theme based on invitee system preferences. Note: This only applies to
the booking pages.
</Trans>
</p>
</div>
<div className="flex-none">
<Switch
onCheckedChange={(checked) =>
formMethods.setValue("theme", checked ? null : themeOptions[0])
}
checked={!value}
if (isLoading) return <SkeletonLoader />;
if (user)
return (
<Form
form={formMethods}
handleSubmit={(values) => {
mutation.mutate({
...values,
// Radio values don't support null as values, therefore we convert an empty string
// back to null here.
theme: values.theme || null,
});
}}>
<Meta title="Appearance" description="Manage settings for your booking appearance" />
<div className="mb-6 flex items-center text-sm">
<div>
<p className="font-semibold">{t("theme")}</p>
<p className="text-gray-600">{t("theme_applies_note")}</p>
</div>
</div>
<div className="flex flex-col justify-between sm:flex-row">
<ThemeLabel
variant="system"
value={null}
label={t("theme_system")}
defaultChecked={user.theme === null}
register={formMethods.register}
/>
<ThemeLabel
variant="light"
value="light"
label={t("theme_light")}
defaultChecked={user.theme === "light"}
register={formMethods.register}
/>
<ThemeLabel
variant="dark"
value="dark"
label={t("theme_dark")}
defaultChecked={user.theme === "dark"}
register={formMethods.register}
/>
</div>
<hr className="border-1 my-8 border-neutral-200" />
<div className="mb-6 flex items-center text-sm">
<div>
<p className="font-semibold">{t("custom_brand_colors")}</p>
<p className="mt-0.5 leading-5 text-gray-600">{t("customize_your_brand_colors")}</p>
</div>
</div>
<div className="block justify-between sm:flex">
<Controller
name="brandColor"
control={formMethods.control}
defaultValue={user.brandColor}
render={() => (
<div>
<p className="mb-2 block text-sm font-medium text-gray-900">{t("light_brand_color")}</p>
<ColorPicker
defaultValue={user.brandColor}
onChange={(value) => formMethods.setValue("brandColor", value)}
/>
</div>
</div>
<div>
<Select
options={themeOptions}
onChange={(event) => {
if (event) formMethods.setValue("theme", { ...event });
}}
isDisabled={!value}
className="mt-2 w-full sm:w-56"
defaultValue={value || themeOptions[0]}
value={value || themeOptions[0]}
/>
</div>
</>
)}
/>
<hr className="border-1 my-8 border-neutral-200" />
<div className="mb-6 flex items-center text-sm">
<div>
<p className="font-semibold">{t("custom_brand_colors")}</p>
<p className="mt-0.5 leading-5 text-gray-600">{t("customize_your_brand_colors")}</p>
)}
/>
<Controller
name="darkBrandColor"
control={formMethods.control}
defaultValue={user.darkBrandColor}
render={() => (
<div className="mt-6 sm:mt-0">
<p className="mb-2 block text-sm font-medium text-gray-900">{t("dark_brand_color")}</p>
<ColorPicker
defaultValue={user.darkBrandColor}
onChange={(value) => formMethods.setValue("darkBrandColor", value)}
/>
</div>
)}
/>
</div>
</div>
<div className="block justify-between sm:flex">
<Controller
name="brandColor"
control={formMethods.control}
defaultValue={user.brandColor}
render={({ field: { value } }) => (
<div>
<p className="mb-2 block text-sm font-medium text-gray-900">{t("light_brand_color")}</p>
<ColorPicker
defaultValue={user.brandColor}
onChange={(value) => formMethods.setValue("brandColor", value)}
/>
</div>
)}
/>
<Controller
name="darkBrandColor"
control={formMethods.control}
defaultValue={user.darkBrandColor}
render={({ field: { value } }) => (
<div className="mt-6 sm:mt-0">
<p className="mb-2 block text-sm font-medium text-gray-900">{t("dark_brand_color")}</p>
<ColorPicker
defaultValue={user.darkBrandColor}
onChange={(value) => formMethods.setValue("darkBrandColor", value)}
/>
</div>
)}
/>
</div>
{/* TODO future PR to preview brandColors */}
{/* <Button
{/* TODO future PR to preview brandColors */}
{/* <Button
color="secondary"
EndIcon={Icon.FiExternalLink}
className="mt-6"
onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}>
Preview
</Button> */}
<hr className="border-1 my-8 border-neutral-200" />
<Controller
name="hideBranding"
control={formMethods.control}
defaultValue={user.hideBranding}
render={({ field: { value } }) => (
<>
<div className="flex w-full text-sm">
<div className="mr-1 flex-grow">
<div className="flex items-center">
<p className="mr-2 font-semibold">{t("disable_cal_branding")}</p>{" "}
<Badge variant="gray">{t("pro")}</Badge>
<hr className="border-1 my-8 border-neutral-200" />
<Controller
name="hideBranding"
control={formMethods.control}
defaultValue={user.hideBranding}
render={({ field: { value } }) => (
<>
<div className="flex w-full text-sm">
<div className="mr-1 flex-grow">
<div className="flex items-center">
<p className="mr-2 font-semibold">{t("disable_cal_branding")}</p>{" "}
<Badge variant="gray">{t("pro")}</Badge>
</div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding")}</p>
</div>
<div className="flex-none">
<Switch
onCheckedChange={(checked) => formMethods.setValue("hideBranding", checked)}
checked={value}
/>
</div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding")}</p>
</div>
<div className="flex-none">
<Switch
onCheckedChange={(checked) => formMethods.setValue("hideBranding", checked)}
checked={value}
/>
</div>
</div>
</>
)}
/>
<Button color="primary" className="mt-8">
{t("update")}
</Button>
</Form>
);
</>
)}
/>
<Button color="primary" className="mt-8">
{t("update")}
</Button>
</Form>
);
};
AppearanceView.getLayout = getLayout;
export default AppearanceView;
interface ThemeLabelProps {
variant: "light" | "dark" | "system";
value?: "light" | "dark" | null;
label: string;
defaultChecked?: boolean;
register: any;
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getSession(context);
if (!session?.user?.id) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
username: true,
timeZone: true,
timeFormat: true,
weekStart: true,
brandColor: true,
darkBrandColor: true,
hideBranding: true,
theme: true,
eventTypes: {
select: {
title: true,
},
},
},
});
if (!user) {
throw new Error("User seems logged in but cannot be found in the db");
}
return {
props: {
user,
},
};
const ThemeLabel = ({ variant, label, value, defaultChecked, register }: ThemeLabelProps) => {
return (
<label
className="relative mb-4 flex-1 cursor-pointer text-center last:mb-0 last:mr-0 sm:mr-4 sm:mb-0"
htmlFor={`theme-${variant}`}>
<input
className="peer absolute top-8 left-8"
type="radio"
value={value}
id={`theme-${variant}`}
defaultChecked={defaultChecked}
{...register("theme")}
/>
<div className="relative z-10 rounded-lg ring-black transition-all peer-checked:ring-2">
<img
aria-hidden="true"
className="cover w-full rounded-lg"
src={`/theme-${variant}.svg`}
alt={`theme ${variant}`}
/>
</div>
<p className="mt-2 text-sm font-medium text-gray-600 peer-checked:text-gray-900">{label}</p>
</label>
);
};

View File

@ -11,6 +11,7 @@ import Badge from "@calcom/ui/v2/core/Badge";
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
import Meta from "@calcom/ui/v2/core/Meta";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import { SkeletonContainer, SkeletonText, SkeletonButton } from "@calcom/ui/v2/core/skeleton";
import { List, ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/modules/List";
import DestinationCalendarSelector from "@calcom/ui/v2/modules/event-types/DestinationCalendarSelector";
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
@ -19,6 +20,21 @@ import { QueryCell } from "@lib/QueryCell";
import { CalendarSwitch } from "@components/v2/settings/CalendarSwitch";
const SkeletonLoader = () => {
return (
<SkeletonContainer>
<div className="mt-6 mb-8 space-y-6 divide-y">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</div>
</SkeletonContainer>
);
};
const CalendarsView = () => {
const { t } = useLocale();
const router = useRouter();
@ -34,36 +50,42 @@ const CalendarsView = () => {
return (
<>
<Meta title="calendars" description="calendars_description" />
<Meta title="Calendars" description="Configure how your event types interact with your calendars" />
<QueryCell
query={query}
customLoader={<SkeletonLoader />}
success={({ data }) => {
console.log("🚀 ~ file: calendars.tsx ~ line 28 ~ CalendarsView ~ data", data);
return data.connectedCalendars.length ? (
<div>
<div className="mt-4 rounded-md border-neutral-200 bg-white p-2 sm:mx-0 sm:p-10 md:border md:p-6 xl:mt-0">
<div className="mt-4 rounded-md border-neutral-200 bg-white p-2 sm:mx-0 sm:p-10 md:border md:p-2 xl:mt-0">
<Icon.FiCalendar className="h-5 w-5" />
<div className="mt-4 flex space-x-4 rounded-md border-gray-200 bg-gray-50 p-2 sm:mx-0 sm:p-10 md:border md:p-6 xl:mt-0">
<div className=" flex h-9 w-9 items-center justify-center rounded-md border-2 border-gray-200 bg-white p-[6px]">
<Icon.FiCalendar className="h-6 w-6" />
</div>
<h4 className="leading-20 mt-2 text-xl font-semibold text-black">{t("add_to_calendar")}</h4>
<p className="pb-2 text-sm text-gray-600">
<Trans i18nKey="add_to_calendar_description">
Where to add events when you re booked. You can override this on a per-event basis in
advanced settings in the event type.
</Trans>
</p>
<DestinationCalendarSelector
hidePlaceholder
value={data.destinationCalendar?.externalId}
onChange={mutation.mutate}
isLoading={mutation.isLoading}
/>
</div>
<h4 className="leading-20 mt-12 text-xl font-semibold text-black">
<div className="flex flex-col space-y-3">
<div>
<h4 className=" pb-2 text-base font-semibold leading-5 text-black">
{t("add_to_calendar")}
</h4>
<p className=" text-sm leading-5 text-gray-600">
<Trans i18nKey="add_to_calendar_description">
Where to add events when you re booked. You can override this on a per-event basis in
advanced settings in the event type.
</Trans>
</p>
</div>
<DestinationCalendarSelector
hidePlaceholder
value={data.destinationCalendar?.externalId}
onChange={mutation.mutate}
isLoading={mutation.isLoading}
/>
</div>
</div>
<h4 className="mt-12 text-base font-semibold leading-5 text-black">
{t("check_for_conflicts")}
</h4>
<p className="pb-2 text-sm text-gray-600">{t("select_calendars")}</p>
<p className="pb-2 text-sm leading-5 text-gray-600">{t("select_calendars")}</p>
<List>
{data.connectedCalendars.map((item) => (
<Fragment key={item.credentialId}>
@ -92,7 +114,11 @@ const CalendarsView = () => {
<ListItemText component="p">{item.integration.description}</ListItemText>
</div>
<div>
<DisconnectIntegration credentialId={item.credentialId} label={t("disconnect")} />
<DisconnectIntegration
trashIcon
credentialId={item.credentialId}
buttonProps={{ size: "icon", color: "secondary" }}
/>
</div>
</div>
<div className="w-full border-t border-gray-200">
@ -120,9 +146,9 @@ const CalendarsView = () => {
) : (
<EmptyScreen
Icon={Icon.FiCalendar}
headline="No calendar installed"
description="You have not yet connected any of your calendars"
buttonText="Add a calendar"
headline={t("no_calendar_installed")}
description={t("no_calendar_installed_description")}
buttonText={t("add_a_calendar")}
buttonOnClick={() => router.push(`${WEBAPP_URL}/apps/categories/calendar`)}
/>
);

View File

@ -1,92 +1,110 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import getApps from "@calcom/app-store/utils";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui";
import { Dropdown, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Button } from "@calcom/ui/v2";
import { Dialog, DialogContent } from "@calcom/ui/v2/core/Dialog";
import Meta from "@calcom/ui/v2/core/Meta";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import showToast from "@calcom/ui/v2/core/notifications";
import { SkeletonContainer, SkeletonText } from "@calcom/ui/v2/core/skeleton";
import { List, ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/modules/List";
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
import { inferSSRProps } from "@lib/types/inferSSRProps";
const SkeletonLoader = () => {
return (
<SkeletonContainer>
<div className="mt-6 mb-8 space-y-6 divide-y">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
</div>
</SkeletonContainer>
);
};
const ConferencingLayout = (props: inferSSRProps<typeof getServerSideProps>) => {
const ConferencingLayout = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { apps } = props;
const { data: apps, isLoading } = trpc.useQuery(
["viewer.integrations", { variant: "conferencing", onlyInstalled: true }],
{
suspense: true,
}
);
const deleteAppMutation = trpc.useMutation("viewer.deleteCredential", {
onSuccess: () => {
showToast("Integration deleted successfully", "success");
utils.invalidateQueries(["viewer.integrations", { variant: "conferencing", onlyInstalled: true }]);
setDeleteAppModal(false);
},
onError: () => {
showToast("Error deleting app", "error");
setDeleteAppModal(false);
},
});
// Error reason: getaddrinfo EAI_AGAIN http
// const query = trpc.useQuery(["viewer.integrations", { variant: "conferencing", onlyInstalled: true }], {
// suspense: true,
// });
const [deleteAppModal, setDeleteAppModal] = useState(false);
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
if (isLoading) return <SkeletonLoader />;
return (
<div className="w-full bg-white sm:mx-0 xl:mt-0">
<Meta title="conferencing" description="conferencing_description" />
<List roundContainer={true}>
{apps.map((app) => (
<ListItem rounded={false} className="flex-col border-0" key={app.title}>
<div className="flex w-full flex-1 items-center space-x-3 pl-1 pt-1 rtl:space-x-reverse">
{
// eslint-disable-next-line @next/next/no-img-element
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3" className="mb-1 space-x-2">
<h3 className="truncate text-sm font-medium text-neutral-900">{app.title}</h3>
</ListItemTitle>
<ListItemText component="p">{app.description}</ListItemText>
</div>
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button StartIcon={Icon.FiMoreHorizontal} size="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DisconnectIntegration
credentialId={app.credentialId}
label={t("remove_app")}
trashIcon
isGlobal={app.isGlobal}
/>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
{/* <div className="flex w-1/2 space-x-6">
<img className="h-10 w-10" src={app.logo} alt={app.title} />
<div className=" truncate pl-2">
<h3 className="truncate text-sm font-medium text-neutral-900">{app.title}</h3>
<p className="truncate text-sm text-gray-500">{app.description}</p>
{apps?.items &&
apps.items
.map((app) => ({ ...app, title: app.title || app.name }))
.map((app) => (
<ListItem rounded={false} className="flex-col border-0" key={app.title}>
<div className="flex w-full flex-1 items-center space-x-3 pl-1 pt-1 rtl:space-x-reverse">
{
// eslint-disable-next-line @next/next/no-img-element
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3" className="mb-1 space-x-2">
<h3 className="truncate text-sm font-medium text-neutral-900">{app.title}</h3>
</ListItemTitle>
<ListItemText component="p">{app.description}</ListItemText>
</div>
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button StartIcon={Icon.FiMoreHorizontal} size="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button
color="destructive"
StartIcon={Icon.FiTrash}
disabled={app.isGlobal}
onClick={() => {
setDeleteCredentialId(app.credentialIds[0]);
setDeleteAppModal(true);
}}>
{t("remove_app")}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
</div>
</div>
<div>
<Dropdown>
<DropdownMenuTrigger className="focus:ring-brand-900 block h-[36px] w-auto justify-center rounded-md border border-gray-200 bg-transparent text-gray-700 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-1">
<Icon.FiMoreHorizontal className="group-hover:text-gray-800" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DisconnectIntegration
credentialId={app.credentialId}
label={t("remove_app")}
trashIcon
isGlobal={app.isGlobal}
/>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div> */}
</div>
</ListItem>
))}
</ListItem>
))}
</List>
<Dialog open={deleteAppModal} onOpenChange={setDeleteAppModal}>
<DialogContent
title={t("Remove app")}
description={t("are_you_sure_you_want_to_remove_this_app")}
type="confirmation"
actionText={t("yes_remove_app")}
Icon={Icon.FiAlertCircle}
actionOnClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}
/>
</Dialog>
</div>
);
};
@ -94,39 +112,3 @@ const ConferencingLayout = (props: inferSSRProps<typeof getServerSideProps>) =>
ConferencingLayout.getLayout = getLayout;
export default ConferencingLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getSession(context);
if (!session?.user?.id) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const videoCredentials = await prisma.credential.findMany({
where: {
userId: session.user.id,
app: {
categories: {
has: "video",
},
},
},
});
const apps = getApps(videoCredentials)
.filter((app) => {
return app.variant === "conferencing" && app.credentials.length;
})
.map((app) => {
return {
slug: app.slug,
title: app.title,
logo: app.logo,
description: app.description,
credentialId: app.credentials[0].id,
isGlobal: app.isGlobal,
};
});
return { props: { apps } };
};

View File

@ -1,11 +1,8 @@
import { GetServerSidePropsContext } from "next";
import { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { useMemo } from "react";
import { useForm, Controller } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/v2/core/Button";
import Meta from "@calcom/ui/v2/core/Meta";
@ -14,34 +11,47 @@ import Select from "@calcom/ui/v2/core/form/Select";
import { Form, Label } from "@calcom/ui/v2/core/form/fields";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import showToast from "@calcom/ui/v2/core/notifications";
import { SkeletonContainer, SkeletonText, SkeletonButton } from "@calcom/ui/v2/core/skeleton";
import { withQuery } from "@lib/QueryCell";
import { getSession } from "@lib/auth";
import { nameOfDay } from "@lib/core/i18n/weekday";
import { inferSSRProps } from "@lib/types/inferSSRProps";
const SkeletonLoader = () => {
return (
<SkeletonContainer>
<div className="mt-6 mb-8 space-y-6 divide-y">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</div>
</SkeletonContainer>
);
};
interface GeneralViewProps {
localeProp: string;
t: TFunction;
user: {
timeZone: string;
timeFormat: number | null;
weekStart: string;
};
}
const WithQuery = withQuery(["viewer.public.i18n"], { context: { skipBatch: true } });
const GeneralQueryView = (props: inferSSRProps<typeof getServerSideProps>) => {
const { t } = useLocale();
return <WithQuery success={({ data }) => <GeneralView localeProp={data.locale} t={t} {...props} />} />;
const GeneralQueryView = () => {
return (
<WithQuery
success={({ data }) => <GeneralView localeProp={data.locale} />}
customLoader={<SkeletonLoader />}
/>
);
};
const GeneralView = ({ localeProp, t, user }: GeneralViewProps) => {
const GeneralView = ({ localeProp }: GeneralViewProps) => {
const router = useRouter();
const utils = trpc.useContext();
const { t } = useLocale();
// const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
const mutation = trpc.useMutation("viewer.updateProfile", {
onSuccess: () => {
showToast(t("settings_updated_successfully"), "success");
@ -49,6 +59,9 @@ const GeneralView = ({ localeProp, t, user }: GeneralViewProps) => {
onError: () => {
showToast(t("error_updating_settings"), "error");
},
onSettled: async () => {
await utils.invalidateQueries(["viewer.public.i18n"]);
},
});
const localeOptions = useMemo(() => {
@ -91,6 +104,8 @@ const GeneralView = ({ localeProp, t, user }: GeneralViewProps) => {
},
});
if (isLoading) return <SkeletonLoader />;
return (
<Form
form={formMethods}
@ -102,7 +117,7 @@ const GeneralView = ({ localeProp, t, user }: GeneralViewProps) => {
weekStart: values.weekStart.value,
});
}}>
<Meta title="general" description="general_description" />
<Meta title="General" description="Manage settings for your language and timezone" />
<Controller
name="locale"
render={({ field: { value, onChange } }) => (
@ -183,32 +198,3 @@ const GeneralView = ({ localeProp, t, user }: GeneralViewProps) => {
GeneralQueryView.getLayout = getLayout;
export default GeneralQueryView;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getSession(context);
if (!session?.user?.id) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
timeZone: true,
timeFormat: true,
weekStart: true,
},
});
if (!user) {
throw new Error("User seems logged in but cannot be found in the db");
}
return {
props: {
user,
},
};
};

View File

@ -1,12 +1,10 @@
import crypto from "crypto";
import { GetServerSidePropsContext } from "next";
import { signOut } from "next-auth/react";
import { useRef, useState, BaseSyntheticEvent } from "react";
import { Controller, useForm } from "react-hook-form";
import { ErrorCode, getSession } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
@ -20,21 +18,37 @@ import Meta from "@calcom/ui/v2/core/Meta";
import { Form, Label, TextField, PasswordField } from "@calcom/ui/v2/core/form/fields";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import showToast from "@calcom/ui/v2/core/notifications";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { SkeletonContainer, SkeletonText, SkeletonButton, SkeletonAvatar } from "@calcom/ui/v2/core/skeleton";
import TwoFactor from "@components/auth/TwoFactor";
const SkeletonLoader = () => {
return (
<SkeletonContainer>
<div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center">
<SkeletonAvatar className=" h-12 w-12 px-4" />
<SkeletonButton className=" h-6 w-32 rounded-md p-5" />
</div>
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</div>
</SkeletonContainer>
);
};
interface DeleteAccountValues {
totpCode: string;
}
const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
const ProfileView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { user } = props;
// const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
const mutation = trpc.useMutation("viewer.updateProfile", {
onSuccess: () => {
showToast(t("settings_updated_successfully"), "success");
@ -50,6 +64,11 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
const form = useForm<DeleteAccountValues>();
const emailMd5 = crypto
.createHash("md5")
.update(user?.email || "example@example.com")
.digest("hex");
const onDeleteMeSuccessMutation = async () => {
await utils.invalidateQueries(["viewer.me"]);
showToast(t("Your account was deleted"), "success");
@ -88,7 +107,7 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
const formMethods = useForm({
defaultValues: {
avatar: user.avatar || "",
avatar: user?.avatar || "",
username: user?.username || "",
name: user?.name || "",
bio: user?.bio || "",
@ -107,6 +126,8 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
};
if (isLoading) return <SkeletonLoader />;
return (
<>
<Form
@ -114,15 +135,14 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
handleSubmit={(values) => {
mutation.mutate(values);
}}>
<Meta title="profile" description="profile_description" />
<Meta title="Profile" description="Manage settings for your cal profile" />
<div className="flex items-center">
{/* TODO upload new avatar */}
<Controller
control={formMethods.control}
name="avatar"
render={({ field: { value } }) => (
<>
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={user.emailMd5} size="lg" />
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
<div className="ml-4">
<ImageUploader
target="avatar"
@ -144,9 +164,10 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
render={({ field: { value } }) => (
<div className="mt-8">
<TextField
data-testid="username-input"
name="username"
label={t("personal_cal_url")}
addOnLeading="https://"
addOnLeading="https://cal.com/"
value={value}
onChange={(e) => {
formMethods.setValue("username", e?.target.value);
@ -158,14 +179,13 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
<Controller
control={formMethods.control}
name="name"
render={({ field: { value } }) => (
render={({ field: { value, onChange } }) => (
<div className="mt-8">
<TextField
name="username"
label={t("full_name")}
value={value}
onChange={(e) => {
formMethods.setValue("name", e?.target.value);
onChange(e?.target.value);
}}
/>
</div>
@ -198,7 +218,11 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
{/* Delete account Dialog */}
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
<DialogTrigger asChild>
<Button color="destructive" className="mt-1 border-2" StartIcon={Icon.FiTrash2}>
<Button
data-testid="delete-account"
color="destructive"
className="mt-1 border-2"
StartIcon={Icon.FiTrash2}>
{t("delete_account")}
</Button>
</DialogTrigger>
@ -207,6 +231,10 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
description={t("confirm_delete_account_modal")}
type="creation"
actionText={t("delete_my_account")}
actionProps={{
// @ts-expect-error data attributes aren't typed
"data-testid": "delete-account-confirm",
}}
Icon={Icon.FiAlertTriangle}
actionOnClick={(e) => e && onConfirmButton(e)}>
<>
@ -222,7 +250,7 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
ref={passwordRef}
/>
{user.twoFactorEnabled && (
{user?.twoFactorEnabled && (
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
<TwoFactor center={false} />
</Form>
@ -240,39 +268,3 @@ const ProfileView = (props: inferSSRProps<typeof getServerSideProps>) => {
ProfileView.getLayout = getLayout;
export default ProfileView;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getSession(context);
if (!session?.user?.id) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
id: true,
username: true,
email: true,
name: true,
bio: true,
avatar: true,
twoFactorEnabled: true,
},
});
if (!user) {
throw new Error("User seems logged in but cannot be found in the db");
}
return {
props: {
user: {
...user,
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
},
};
};

View File

@ -1,16 +1,21 @@
import { IdentityProvider } from "@prisma/client";
import { Trans } from "next-i18next";
import { Controller, useForm } from "react-hook-form";
import { useForm } from "react-hook-form";
import { identityProviderNameMap } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/v2/core/Button";
import Button from "@calcom/ui/v2/core/Button";
import Meta from "@calcom/ui/v2/core/Meta";
import { Form, TextField } from "@calcom/ui/v2/core/form/fields";
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
import showToast from "@calcom/ui/v2/core/notifications";
type ChangePasswordFormValues = {
oldPassword: string;
newPassword: string;
};
const PasswordView = () => {
const { t } = useLocale();
const { data: user } = trpc.useQuery(["viewer.me"]);
@ -24,11 +29,26 @@ const PasswordView = () => {
},
});
const formMethods = useForm();
const formMethods = useForm<ChangePasswordFormValues>({
defaultValues: {
oldPassword: "",
newPassword: "",
},
});
const {
register,
formState: { isSubmitting },
} = formMethods;
const handleSubmit = (values: ChangePasswordFormValues) => {
const { oldPassword, newPassword } = values;
mutation.mutate({ oldPassword, newPassword });
};
return (
<>
<Meta title="password" description="password_description" />
<Meta title="Password" description="Manage settings for your account passwords" />
{user && user.identityProvider !== IdentityProvider.CAL ? (
<div>
<div className="mt-6">
@ -45,46 +65,17 @@ const PasswordView = () => {
</p>
</div>
) : (
<Form
form={formMethods}
handleSubmit={async (values) => {
const { oldPassword, newPassword } = values;
mutation.mutate({ oldPassword, newPassword });
}}>
<Form<ChangePasswordFormValues> form={formMethods} handleSubmit={handleSubmit}>
<div className="max-w-[38rem] sm:flex sm:space-x-4">
<div className="flex-grow">
<Controller
name="oldPassword"
control={formMethods.control}
render={({ field: { value } }) => (
<TextField
name="oldPassword"
label={t("old_password")}
value={value}
type="password"
onChange={(e) => {
formMethods.setValue("oldPassword", e?.target.value);
}}
/>
)}
/>
<TextField {...register("oldPassword")} label={t("old_password")} type="password" />
</div>
<div className="flex-grow">
<Controller
name="newPassword"
control={formMethods.control}
render={({ field: { value } }) => (
<TextField
name="newPassword"
label={t("new_password")}
value={value}
type="password"
placeholder={t("secure_password")}
onChange={(e) => {
formMethods.setValue("newPassword", e?.target.value);
}}
/>
)}
<TextField
{...register("newPassword")}
label={t("new_password")}
type="password"
placeholder={t("secure_password")}
/>
</div>
</div>
@ -94,7 +85,12 @@ const PasswordView = () => {
contain at least 1 number
</Trans>
</p>
<Button color="primary" className="mt-8" disabled={formMethods.formState.isSubmitting}>
{/* TODO: Why is this Form not submitting? Hacky fix but works */}
<Button
color="primary"
className="mt-8"
disabled={isSubmitting || mutation.isLoading}
onClick={() => handleSubmit(formMethods.getValues())}>
{t("update")}
</Button>
</Form>

View File

@ -24,7 +24,7 @@ const TwoFactorAuthView = () => {
return (
<>
<Meta title="2fa" description="2fa_description" />
<Meta title="Two-Factor Authentication" description="Manage settings for your account passwords" />
<div className="mt-6 flex items-start space-x-4">
<Switch
checked={user?.twoFactorEnabled}

View File

@ -0,0 +1,114 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useState } from "react";
import { z } from "zod";
// import TeamGeneralSettings from "@calcom/features/teams/createNewTeam/TeamGeneralSettings";
import AddNewTeamMembers from "@calcom/features/ee/teams/components/v2/AddNewTeamMembers";
import CreateNewTeam from "@calcom/features/ee/teams/components/v2/CreateNewTeam";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { StepCard } from "@components/getting-started/components/StepCard";
import { Steps } from "@components/getting-started/components/Steps";
const INITIAL_STEP = "create-a-new-team";
// TODO: Add teams general settings "general-settings"
const steps = ["create-a-new-team", "add-team-members"] as const;
const stepTransform = (step: typeof steps[number]) => {
const stepIndex = steps.indexOf(step);
if (stepIndex > -1) {
return steps[stepIndex];
}
return INITIAL_STEP;
};
const stepRouteSchema = z.object({
step: z.array(z.enum(steps)).default([INITIAL_STEP]),
});
const CreateNewTeamPage = () => {
const router = useRouter();
const { t } = useLocale();
const [teamId, setTeamId] = useState<number>();
const result = stepRouteSchema.safeParse(router.query);
const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
const headers = [
{
title: `${t("create_new_team")}`,
subtitle: [`${t("create_new_team_description")}`],
},
// {
// title: `${t("general_settings")}`,
// subtitle: [`${t("general_settings_description")}`],
// },
{
title: `${t("add_team_members")}`,
subtitle: [`${t("add_team_members_description")}`],
},
];
const goToIndex = (index: number) => {
const newStep = steps[index];
router.push(
{
pathname: `/settings/teams/new/${stepTransform(newStep)}`,
},
undefined
);
};
const currentStepIndex = steps.indexOf(currentStep);
return (
<div
className="dark:bg-brand dark:text-brand-contrast min-h-screen text-black"
data-testid="onboarding"
key={router.asPath}>
<Head>
<title>{t("create_new_team")}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="mx-auto px-4 py-24">
<div className="relative">
<div className="sm:mx-auto sm:w-full sm:max-w-[600px]">
<div className="mx-auto sm:max-w-[520px]">
<header>
<p className="font-cal mb-3 text-[28px] font-medium leading-7">
{headers[currentStepIndex]?.title || "Undefined title"}
</p>
<p className="font-sans text-sm font-normal text-gray-500">
{headers[currentStepIndex]?.subtitle}
</p>
</header>
<Steps maxSteps={steps.length} currentStep={currentStepIndex} navigateToStep={goToIndex} />
</div>
<StepCard>
{currentStep === "create-a-new-team" && (
<CreateNewTeam
nextStep={() => {
goToIndex(1);
}}
setTeamId={(teamId: number) => setTeamId(teamId)}
/>
)}
{/* {currentStep === "general-settings" && (
<TeamGeneralSettings teamId={teamId} nextStep={() => goToIndex(2)} />
)} */}
{currentStep === "add-team-members" && teamId && <AddNewTeamMembers teamId={teamId} />}
</StepCard>
</div>
</div>
</div>
</div>
);
};
export default CreateNewTeamPage;

View File

@ -34,8 +34,6 @@ import { Icon } from "@calcom/ui/Icon";
import { EmailInput } from "@calcom/ui/form/fields";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking";
@ -275,9 +273,7 @@ export default function Success(props: SuccessProps) {
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
{isSuccessRedirectAvailable(eventType) && eventType.successRedirectUrl ? (
<RedirectionToast url={eventType.successRedirectUrl} />
) : null}{" "}
{eventType.successRedirectUrl ? <RedirectionToast url={eventType.successRedirectUrl} /> : null}{" "}
<div
className={classNames(
shouldAlignCentrally ? "text-center" : "",
@ -868,7 +864,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
hideBranding: eventType.team ? eventType.team.hideBranding : isBrandingHidden(eventType.users[0]),
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
profile,
eventType,
recurringBookings: recurringBookings ? recurringBookings.map((obj) => obj.startTime.toString()) : null,

View File

@ -8,8 +8,7 @@ test.describe("App Store - Authed", () => {
test("Browse apple-calendar and try to install", async ({ page, users }) => {
const pro = await users.create();
await pro.login();
await page.goto("/apps");
await page.click('[data-testid="app-store-category-calendar"]');
await page.goto("/apps/categories/calendar");
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
await page.click('[data-testid="install-app-button"]');
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
@ -19,9 +18,7 @@ test.describe("App Store - Authed", () => {
test.describe("App Store - Unauthed", () => {
test("Browse apple-calendar and try to install", async ({ page }) => {
await page.goto("/apps");
await page.waitForSelector("[data-testid=dashboard-shell]");
await page.click('[data-testid="app-store-category-calendar"]');
await page.goto("/apps/categories/calendar");
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
await page.click('[data-testid="install-app-button"]');
await expect(page.locator(`[data-testid="login-form"]`)).toBeVisible();

View File

@ -20,21 +20,23 @@ test.describe("Can signup from a team invite", async () => {
password: `${proUser.username}-member`,
email: `${proUser.username}-member@example.com`,
};
await page.goto("/settings/teams");
await page.goto("/settings/teams/new");
await page.waitForLoadState("networkidle");
// Create a new team
await page.click("text=New Team");
await page.fill('input[id="name"]', teamName);
await page.click('[data-testid="create-new-team-button"]');
// Go to new team page
await page.click(`a[title="${teamName}"]`);
await page.locator('input[name="name"]').fill(teamName);
await page.locator('input[name="slug"]').fill(teamName);
await page.locator('button[type="submit"]').click();
// Add new member to team
await page.click('[data-testid="new-member-button"]');
await page.fill('input[id="inviteUser"]', testUser.email);
await page.click('[data-testid="invite-new-member-button"]');
// TODO: Adapt to new flow
// Wait for the invite to be sent
await page.waitForSelector(`[data-testid="member-email"][data-email="${testUser.email}"]`);
/*await page.waitForSelector(`[data-testid="member-email"][data-email="${testUser.email}"]`);
const tokenObj = await prisma.verificationToken.findFirstOrThrow({
where: { identifier: testUser.email },
@ -95,7 +97,7 @@ test.describe("Can signup from a team invite", async () => {
expect(createdUser.teams).toHaveLength(1);
expect(createdUser.teams[0].team.name).toBe(teamName);
expect(createdUser.teams[0].role).toBe("MEMBER");
expect(createdUser.teams[0].accepted).toBe(true);
expect(createdUser.teams[0].accepted).toBe(true);*/
});
});

View File

@ -12,14 +12,12 @@ test("Can delete user account", async ({ page, users }) => {
await page.goto(`/settings/profile`);
await page.click("[data-testid=delete-account]");
await expect(page.locator(`[data-testid=delete-account-confirm]`)).toBeVisible();
if (!user.username) throw Error(`Test user doesn't have a username`);
await page.fill("[data-testid=password]", user.username);
await Promise.all([
page.waitForNavigation({ url: "/auth/logout" }),
page.click("[data-testid=delete-account-confirm]"),
]);
const $passwordField = page.locator("[data-testid=password]");
await $passwordField.fill(user.username);
await Promise.all([page.waitForNavigation({ url: "/auth/logout" }), page.click("text=Delete my account")]);
await expect(page.locator(`[id="modal-title"]`)).toHaveText("You've been logged out");
});

View File

@ -19,12 +19,6 @@ test.describe("free user", () => {
await users.deleteAll();
});
test("only one visible event", async ({ page, users }) => {
const [free] = users.get();
await expect(page.locator(`[href="/${free.username}/${free.eventTypes[0].slug}"]`)).toBeVisible();
await expect(page.locator(`[href="/${free.username}/${free.eventTypes[1].slug}"]`)).not.toBeVisible();
});
test("cannot book same slot multiple times", async ({ page }) => {
// Click first event type
await page.click('[data-testid="event-type-link"]');
@ -56,16 +50,6 @@ test.describe("free user", () => {
// check for error message
await expect(page.locator("[data-testid=booking-fail]")).toBeVisible();
});
test("Second event type is not bookable", async ({ page, users }) => {
const [free] = users.get();
// Not available in listing
await expect(page.locator(`[href="/${free.username}/${free.eventTypes[1].slug}"]`)).toHaveCount(0);
await page.goto(`/${free.username}/${free.eventTypes[1].slug}`);
// Not available on a direct visit to event type page
await expect(page.locator('[data-testid="404-page"]')).toBeVisible();
});
});
test.describe("pro user", () => {

View File

@ -9,14 +9,18 @@ test.describe("Change Password Test", () => {
const pro = await users.create();
await pro.login();
// Go to http://localhost:3000/settings/security
await page.goto("/settings/security");
await page.goto("/settings/security/password");
if (!pro.username) throw Error("Test user doesn't have a username");
await page.waitForLoadState("networkidle");
// Fill form
await page.waitForSelector('[name="current_password"]');
await page.fill('[name="current_password"]', pro.username);
await page.fill('[name="new_password"]', `${pro.username}1111`);
await page.press('[name="new_password"]', "Enter");
await page.locator('[name="oldPassword"]').fill(pro.username);
const $newPasswordField = page.locator('[name="newPassword"]');
$newPasswordField.fill(`${pro.username}Aa1111`);
await page.locator("text=Update").click();
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");

View File

@ -30,18 +30,15 @@ test.describe("Change username on settings", () => {
await user.login();
// Try to go homepage
await page.goto("/settings/profile");
await page.goto("/settings/my-account/profile");
// Change username from normal to normal
const usernameInput = page.locator("[data-testid=username-input]");
await usernameInput.fill("demousernamex");
// Click on save button
await page.click("[data-testid=update-username-btn-desktop]");
await Promise.all([
page.waitForResponse("**/viewer.updateProfile*"),
page.click("[data-testid=save-username]"),
page.click('button[type="submit"]'),
]);
const newUpdatedUser = await prisma.user.findUniqueOrThrow({
@ -76,7 +73,7 @@ test.describe("Change username on settings", () => {
});
await user.login();
await page.goto("/settings/profile");
await page.goto("/settings/my-account/profile");
// Change username from normal to premium
const usernameInput = page.locator("[data-testid=username-input]");
@ -84,7 +81,7 @@ test.describe("Change username on settings", () => {
await usernameInput.fill(`xx${testInfo.workerIndex}`);
// Click on save button
await page.click("[data-testid=update-username-btn-desktop]");
await page.click('button[type="submit"]');
// Validate modal text fields
const currentUsernameText = page.locator("[data-testid=current-username]").innerText();
@ -130,7 +127,7 @@ test.describe("Change username on settings", () => {
await usernameInput.fill(`xx${testInfo.workerIndex}`);
// Click on save button
await page.click("[data-testid=update-username-btn-desktop]");
await page.click('button[type="submit"]');
// Validate modal text fields
const currentUsernameText = page.locator("[data-testid=current-username]").innerText();

View File

@ -2,8 +2,13 @@ import { expect, Page } from "@playwright/test";
import { test } from "./lib/fixtures";
function chooseEmbedType(page: Page, embedType: string) {
page.locator(`[data-testid=${embedType}]`).click();
// these tests need rewrite
// eslint-disable-next-line playwright/no-skipped-test
test.skip();
async function chooseEmbedType(page: Page, embedType: string) {
const $embedTypeSelector = await page.locator(`[data-testid=${embedType}]`);
$embedTypeSelector.click();
}
async function gotToPreviewTab(page: Page) {
@ -60,7 +65,7 @@ async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
url.searchParams.get("dialog") === "embed" &&
url.searchParams.get("embedUrl") === embedUrl &&
url.searchParams.get("embedType") === embedType &&
url.searchParams.get("tabName") === "embed-code"
url.searchParams.get("embedTabName") === "embed-code"
);
},
});
@ -106,7 +111,7 @@ test.describe("Embed Code Generator Tests", () => {
basePage: "/event-types",
});
chooseEmbedType(page, "inline");
await chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
@ -114,14 +119,14 @@ test.describe("Embed Code Generator Tests", () => {
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "inline" });
/*await expectToContainValidCode(page, { embedType: "inline" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: `${pro.username}/30-min`,
});
});*/
});
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
@ -134,20 +139,20 @@ test.describe("Embed Code Generator Tests", () => {
basePage: "/event-types",
});
chooseEmbedType(page, "floating-popup");
await chooseEmbedType(page, "floating-popup");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "floating-popup",
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "floating-popup" });
/*await expectToContainValidCode(page, { embedType: "floating-popup" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "floating-popup",
calLink: `${pro.username}/30-min`,
});
});*/
});
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
@ -159,20 +164,20 @@ test.describe("Embed Code Generator Tests", () => {
basePage: "/event-types",
});
chooseEmbedType(page, "element-click");
await chooseEmbedType(page, "element-click");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "element-click",
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "element-click" });
/*await expectToContainValidCode(page, { embedType: "element-click" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "element-click",
calLink: `${pro.username}/30-min`,
});
});*/
});
});
@ -195,7 +200,7 @@ test.describe("Embed Code Generator Tests", () => {
basePage,
});
chooseEmbedType(page, "inline");
await chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
@ -203,7 +208,7 @@ test.describe("Embed Code Generator Tests", () => {
embedType: "inline",
});
await expectToContainValidCode(page, {
/*await expectToContainValidCode(page, {
embedType: "inline",
});
@ -212,7 +217,7 @@ test.describe("Embed Code Generator Tests", () => {
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: decodeURIComponent(embedUrl),
});
});*/
});
});
});

View File

@ -23,10 +23,6 @@ test.describe("Event Types tests", () => {
const $eventTypes = page.locator("[data-testid=event-types] > *");
const count = await $eventTypes.count();
expect(count).toBeGreaterThanOrEqual(2);
for (let i = 0; i < count; i++) {
expect(await $eventTypes.nth(i).getAttribute("data-disabled")).toBe("0");
}
});
test("can add new event type", async ({ page }) => {
@ -63,7 +59,7 @@ test.describe("Event Types tests", () => {
},
});
await page.click("[data-testid=show-advanced-settings]");
await page.click("[data-testid=vertical-tab-recurring]");
await expect(page.locator("[data-testid=recurring-event-collapsible]")).not.toBeVisible();
await page.click("[data-testid=recurring-event-check]");
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeVisible();
@ -120,15 +116,9 @@ test.describe("Event Types tests", () => {
return !!url.pathname.match(/\/event-types\/.+/);
},
});
await expect(page.locator("[data-testid=advanced-settings-content]")).not.toBeVisible();
await page.locator("[data-testid=show-advanced-settings]").click();
await expect(page.locator("[data-testid=advanced-settings-content]")).toBeVisible();
await page.locator("[data-testid=update-eventtype]").click();
await page.waitForNavigation({
url: (url) => {
return url.pathname.endsWith("/event-types");
},
});
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
await expect(toast).toBeTruthy();
});
});
@ -145,15 +135,6 @@ test.describe("Event Types tests", () => {
const $eventTypes = page.locator("[data-testid=event-types] > *");
const count = await $eventTypes.count();
expect(count).toBeGreaterThanOrEqual(2);
const $first = $eventTypes.first();
const $last = $eventTypes.last()!;
expect(await $first.getAttribute("data-disabled")).toBe("0");
expect(await $last.getAttribute("data-disabled")).toBe("1");
});
test("can not add new event type", async ({ page }) => {
await expect(page.locator("[data-testid=new-event-type]")).toBeDisabled();
});
test("edit first event", async ({ page }) => {
@ -165,15 +146,9 @@ test.describe("Event Types tests", () => {
return !!url.pathname.match(/\/event-types\/.+/);
},
});
await expect(page.locator("[data-testid=advanced-settings-content]")).not.toBeVisible();
await page.locator("[data-testid=show-advanced-settings]").click();
await expect(page.locator("[data-testid=advanced-settings-content]")).toBeVisible();
await page.locator("[data-testid=update-eventtype]").click();
await page.waitForNavigation({
url: (url) => {
return url.pathname.endsWith("/event-types");
},
});
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
await expect(toast).toBeTruthy();
});
});
});

View File

@ -20,18 +20,19 @@ test.describe("hash my url", () => {
await page.waitForSelector('[data-testid="event-types"]');
await page.locator('//ul[@data-testid="event-types"]/li[1]').click();
// We wait for the page to load
await page.locator('//*[@data-testid="show-advanced-settings"]').click();
await page.locator(".primary-navigation >> text=Advanced").click();
// ignore if it is already checked, and click if unchecked
const hashedLinkCheck = await page.locator('[id="hashedLinkCheck"]');
const hashedLinkCheck = await page.locator('[data-testid="hashedLinkCheck"]');
!(await hashedLinkCheck.isChecked()) && (await hashedLinkCheck.click());
await hashedLinkCheck.click();
// we wait for the hashedLink setting to load
const $url = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
// click update
await page.locator('[data-testid="update-eventtype"]').press("Enter");
await page.waitForURL("/event-types");
await page.waitForLoadState("networkidle");
// book using generated url hash
await page.goto($url);
@ -46,7 +47,7 @@ test.describe("hash my url", () => {
await page.waitForSelector('[data-testid="event-types"]');
await page.click('//ul[@data-testid="event-types"]/li[1]');
// We wait for the page to load
await page.locator('//*[@data-testid="show-advanced-settings"]').click();
await page.locator(".primary-navigation >> text=Advanced").click();
// we wait for the hashedLink setting to load
const $newUrl = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
expect($url !== $newUrl).toBeTruthy();

View File

@ -29,8 +29,6 @@ test.describe("user can login & logout succesfully", async () => {
const planLocator = shellLocator.locator(`[data-testid=plan-trial]`);
await expect(planLocator).toBeVisible();
await expect(planLocator).toHaveText("-TRIAL");
await expect(page.locator(`[data-testid=trial-banner]`)).toBeVisible();
});
//

View File

@ -12,6 +12,6 @@ test.describe("SAML tests", () => {
// Try to go Security page
await page.goto("/settings/security");
// It should redirect you to the event-types page
await page.waitForSelector("[data-testid=saml_config]");
// await page.waitForSelector("[data-testid=saml_config]");
});
});

View File

@ -35,26 +35,26 @@ test.describe("Wipe my Cal App Test", () => {
await bookings.create(pro.id, pro.username, eventType.id, {});
await pro.login();
await page.goto("/bookings/upcoming");
await expect(page.locator("data-testid=wipe-today-button")).toBeVisible();
const totalUserBookings = await prisma.booking.findMany({
where: {
userId: pro.id,
},
});
expect(totalUserBookings.length).toBe(3);
const $openBookingCount = await page.locator('[data-testid="bookings"] > *').count();
await expect($openBookingCount).toBe(3);
await page.locator("data-testid=wipe-today-button").click();
await page.locator("data-testid=send_request").click();
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(250);
const totalUserBookingsCancelled = await prisma.booking.findMany({
where: {
userId: pro.id,
status: "CANCELLED",
},
const $openBookings = await page.locator('[data-testid="bookings"]');
await $openBookings.evaluate((ul) => {
return new Promise<void>((resolve) =>
new window.MutationObserver(() => {
if (ul.childElementCount === 2) {
resolve();
}
}).observe(ul, { childList: true })
);
});
expect(totalUserBookingsCancelled.length).toBe(1);
await users.deleteAll();
});
});

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "ألغى {{organizer}} الحجز وطلب منك اختيار حجز في وقت آخر.",
"request_reschedule_title_organizer": "لقد طلبت من {{attendee}} إعادة جدولة موعد",
"request_reschedule_subtitle_organizer": "لقد ألغيت الحجز ويجب أن يختار {{attendee}} وقتًا جديداً للحجز معك.",
"rescheduled_event_type_subject": "تم إرسال طلب لإعادة الجدولة: {{eventType}} مع {{name}} في {{date}}",
"requested_to_reschedule_subject_attendee": "الإجراء المطلوب إعادة الجدولة: يُرجى حجز وقت جديد لمدة {{eventType}} مع {{name}}",
"reschedule_reason": "سبب إعادة الجدولة",
"hi_user_name": "مرحبا {{name}}",
"ics_event_title": "{{eventType}} مع {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "كلمة سر الاجتماع",
"meeting_url": "رابط الاجتماع",
"meeting_request_rejected": "تم رفض طلبك للاجتماع",
"rescheduled_event_type_subject": "تم إرسال طلب لإعادة الجدولة: {{eventType}} مع {{name}} في {{date}}",
"requested_to_reschedule_subject_attendee": "الإجراء المطلوب إعادة الجدولة: يُرجى حجز وقت جديد لمدة {{eventType}} مع {{name}}",
"rejected_event_type_with_organizer": "تم الرفض: {{eventType}} مع {{organizer}} في {{date}}",
"hi": "مرحبا",
"join_team": "انضم إلى للفريق",
@ -1022,8 +1022,6 @@
"navigate": "تنقّل",
"open": "فتح",
"close": "إغلاق",
"pro_feature_teams": "هذه ميزة في Pro. قم بالترقية إلى Pro لرؤية وجود فريقك.",
"pro_feature_workflows": "هذه ميزة في Pro. قم بالترقية إلى Pro لأتمتة إشعارات الحدث والتذكير مع سير العمل.",
"embed": "مُضمَن",
"new_username": "اسم مستخدم جديد",
"current_username": "اسم المستخدم الحالي",
@ -1037,5 +1035,7 @@
"download_desktop_app": "تحميل تطبيق سطح المكتب",
"set_ping_link": "تعيين رابط Ping",
"missing_connected_calendar": "لا يوجد تقويم افتراضي متصل",
"organizer_name_info": "اسمك"
"organizer_name_info": "اسمك",
"theme_light": "فاتح",
"theme_dark": "داكن"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "Organizátor {{organizer}} zrušil rezervaci a požádal vás, abyste si vybrali jiný čas.",
"request_reschedule_title_organizer": "Požádali jste uživatele {{attendee}} o změnu časového plánu",
"request_reschedule_subtitle_organizer": "Zrušili jste rezervaci a uživatel {{attendee}} by si s vámi měl zarezervovat nový čas.",
"rescheduled_event_type_subject": "Žádost o přesunutí schůzky: {{eventType}} s {{name}} dne {{date}}",
"requested_to_reschedule_subject_attendee": "Akce vyžadovaná k přeplánování: zarezervujte si nový čas pro událost {{eventType}} s osobou {{name}}",
"reschedule_reason": "Důvod přeplánování",
"hi_user_name": "Hezký den, {{name}}",
"ics_event_title": "{{eventType}} s {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Heslo ke schůzce",
"meeting_url": "URL ke schůzce",
"meeting_request_rejected": "Vaše žádost o schůzku byla zamítnuta",
"rescheduled_event_type_subject": "Žádost o přesunutí schůzky: {{eventType}} s {{name}} dne {{date}}",
"requested_to_reschedule_subject_attendee": "Akce vyžadovaná k přeplánování: zarezervujte si nový čas pro událost {{eventType}} s osobou {{name}}",
"rejected_event_type_with_organizer": "Zamítnuto: {{eventType}} s {{organizer}} během {{date}}",
"hi": "Zdravíme",
"join_team": "Připojit se k týmu",
@ -1022,8 +1022,6 @@
"navigate": "Navigace",
"open": "Otevřít",
"close": "Zavřít",
"pro_feature_teams": "Toto je funkce Pro. Pokud chcete zobrazit dostupnost svého týmu, upgradujte na verzi Pro.",
"pro_feature_workflows": "Toto je funkce Pro. Upgradujte na verzi Pro, abyste mohli automatizovat upozornění na události a upomínky pomocí pracovních postupů.",
"embed": "Vložit",
"new_username": "Nové uživatelské jméno",
"current_username": "Současné uživatelské jméno",
@ -1035,5 +1033,7 @@
"using_additional_inputs_as_variables": "Jak používat doplňkové vstupy jako proměnné?",
"download_desktop_app": "Stáhněte si aplikaci pro stolní počítače",
"set_ping_link": "Nastavení odkazu Ping",
"organizer_name_info": "Vaše jméno"
"organizer_name_info": "Vaše jméno",
"theme_light": "Světlý",
"theme_dark": "Tmavý"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} hat die Buchung storniert und Sie gebeten, eine andere Zeit auszuwählen.",
"request_reschedule_title_organizer": "Sie haben {{attendee}} zur Neuplanung aufgefordert",
"request_reschedule_subtitle_organizer": "Sie haben die Buchung storniert und {{attendee}} muss eine neue Buchungszeit mit Ihnen vereinbaren.",
"rescheduled_event_type_subject": "Anfrage für Neuplanung gesendet: {{eventType}} mit {{name}} am {{date}}",
"requested_to_reschedule_subject_attendee": "Aktion erforderlich - Neuplanung: Bitte buchen Sie eine neue Zeit für „{{eventType}}“ mit {{name}}",
"reschedule_reason": "Grund für die Neuplanung",
"hi_user_name": "Hi {{userName}}",
"ics_event_title": "{{eventType}} mit {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Termin-Passwort",
"meeting_url": "Termin-URL",
"meeting_request_rejected": "Ihre Buchungsanfrage wurde abgelehnt",
"rescheduled_event_type_subject": "Anfrage für Neuplanung gesendet: {{eventType}} mit {{name}} am {{date}}",
"requested_to_reschedule_subject_attendee": "Aktion erforderlich - Neuplanung: Bitte buchen Sie eine neue Zeit für „{{eventType}}“ mit {{name}}",
"rejected_event_type_with_organizer": "Abgelehnt: {{eventType}} mit {{organizer}} auf {{date}}",
"hi": "Hi",
"join_team": "Team beitreten",
@ -355,7 +355,6 @@
"meeting_ended": "Meeting beendet",
"form_submitted": "Formular gesendet",
"event_triggers": "Ereignis-Auslöser",
"subscriber_url": "Subscriber URL",
"create_new_webhook": "Einen neuen Webhook erstellen",
"webhooks": "Webhooks",
"team_webhooks": "Team Webhooks",
@ -628,6 +627,7 @@
"billing": "Abrechnung",
"manage_your_billing_info": "Verwalten Sie Ihre Rechnungsinformationen und kündigen Sie Ihr Abonnement.",
"availability": "Verfügbarkeit",
"edit_availability": "Verfügbarkeit editieren",
"configure_availability": "Konfigurieren Sie die Zeiten, in denen Sie für Buchungen zur Verfügung stehen.",
"change_weekly_schedule": "Wöchentliche Termine ändern",
"logo": "Logo",
@ -1043,8 +1043,6 @@
"navigate": "Navigieren",
"open": "Öffnen",
"close": "Schließen",
"pro_feature_teams": "Dies ist eine Pro-Funktion. Sie müssen auf die Pro-Version upgraden, um die Verfügbarkeit Ihres Teams zu sehen.",
"pro_feature_workflows": "Dies ist eine Pro-Funktion. Sie müssen auf die Pro-Version upgraden, um Ihre Termin-Benachrichtigungen und Erinnerungen mit Workflows zu automatisieren.",
"show_eventtype_on_profile": "Im Profil anzeigen",
"embed": "Einbetten",
"new_username": "Neuer Benutzername",
@ -1129,7 +1127,6 @@
"set_availability_getting_started_subtitle_1": "Zeiträume definieren, in denen Sie verfügbar sind",
"set_availability_getting_started_subtitle_2": "Sie können dies alles später auf der Verfügbarkeitsseite anpassen.",
"connect_calendars_from_app_store": "Sie können weitere Kalender aus dem App-Store hinzufügen",
"copy_all": "Alles kopieren",
"add_variable": "Variable hinzufügen",
"custom_phone_number": "Benutzerdefinierte Telefonnummer",
"message_template": "Nachrichtenvorlage",
@ -1159,5 +1156,15 @@
"connect_calendar_later": "Ich verbinde meinen Kalender später",
"set_my_availability_later": "Ich werde meine Verfügbarkeit später festlegen",
"problem_saving_user_profile": "Beim Speichern Ihrer Daten ist ein Problem aufgetreten. Bitte versuchen Sie es erneut oder wenden Sie sich an den Support.",
"token_invalid_expired": "Token ist entweder ungültig oder abgelaufen."
"token_invalid_expired": "Token ist entweder ungültig oder abgelaufen.",
"exchange_add": "Mit Microsoft Exchange verbinden",
"exchange_authentication": "Authentifizierungsmethode",
"exchange_authentication_standard": "Basisauthentifizierung",
"exchange_authentication_ntlm": "NTLM-Authentifizierung",
"exchange_compression": "GZip-Kompression",
"theme_light": "Hell",
"theme_dark": "Dunkel",
"back_to_signin": "Zurück zur Anmeldung",
"password_updated": "Passwort aktualisiert!",
"pending_payment": "Zahlung ausstehend"
}

View File

@ -67,12 +67,14 @@
"event_still_awaiting_approval": "An event is still waiting for your approval",
"booking_submitted_subject": "Booking Submitted: {{eventType}} with {{name}} at {{date}}",
"your_meeting_has_been_booked": "Your meeting has been booked",
"event_type_has_been_rescheduled_on_time_date": "Your {{eventType}} with {{name}} has been rescheduled to {{time}} ({{timeZone}}) on {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Your {{eventType}} with {{name}} has been rescheduled to {{date}}.",
"event_has_been_rescheduled": "Updated - Your event has been rescheduled",
"request_reschedule_title_attendee": "Request to reschedule your booking",
"request_reschedule_subtitle": "{{organizer}} has cancelled the booking and requested you to pick another time.",
"request_reschedule_title_organizer": "You have requested {{attendee}} to reschedule",
"request_reschedule_subtitle_organizer": "You have cancelled the booking and {{attendee}} should pick a new booking time with you.",
"rescheduled_event_type_subject": "Request for reschedule sent: {{eventType}} with {{name}} at {{date}}",
"requested_to_reschedule_subject_attendee": "Action Required Reschedule: Please book a new time for {{eventType}} with {{name}}",
"reschedule_reason": "Reason for reschedule",
"hi_user_name": "Hi {{name}}",
"ics_event_title": "{{eventType}} with {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Meeting Password",
"meeting_url": "Meeting URL",
"meeting_request_rejected": "Your meeting request has been rejected",
"rescheduled_event_type_subject": "Request for reschedule sent: {{eventType}} with {{name}} at {{date}}",
"requested_to_reschedule_subject_attendee": "Action Required Reschedule: Please book a new time for {{eventType}} with {{name}}",
"rejected_event_type_with_organizer": "Rejected: {{eventType}} with {{organizer}} on {{date}}",
"hi": "Hi",
"join_team": "Join team",
@ -206,6 +206,7 @@
"2fa_enter_six_digit_code": "Enter the six-digit code from your authenticator app below.",
"create_an_account": "Create an account",
"dont_have_an_account": "Don't have an account?",
"i_dont_have_an_account": "I don't have an account",
"2fa_code": "Two-Factor Code",
"sign_in_account": "Sign in to your account",
"sign_in": "Sign in",
@ -266,6 +267,8 @@
"enter_new_password": "Enter the new password you'd like for your account.",
"reset_password": "Reset Password",
"change_your_password": "Change your password",
"show_password": "Show password",
"hide_password": "Hide password",
"try_again": "Try Again",
"request_is_expired": "That Request is Expired.",
"reset_instructions": "Enter the email address associated with your account and we will send you a link to reset your password.",
@ -444,7 +447,7 @@
"sunday": "Sunday",
"all_booked_today": "All booked today.",
"slots_load_fail": "Could not load the available time slots.",
"additional_guests": "+ Additional Guests",
"additional_guests": "Add guest",
"your_name": "Your name",
"email_address": "Email address",
"enter_valid_email": "Please enter a valid email",
@ -495,7 +498,10 @@
"lets_create_first_administrator_user": "Let's create the first administrator user.",
"new_member": "New Member",
"invite": "Invite",
"invite_new_member": "Invite a new member",
"add_team_members": "Add team members",
"add_team_members_description": "Invite others to join your team",
"add_team_member": "Add team member",
"invite_new_member": "Invite a new team member",
"invite_new_team_member": "Invite someone to your team.",
"change_member_role": "Change team member role",
"disable_cal_branding": "Disable Cal branding",
@ -628,8 +634,10 @@
"billing": "Billing",
"manage_your_billing_info": "Manage your billing information and cancel your subscription.",
"availability": "Availability",
"availability_title": "{{availabilityTitle}} | Availability",
"edit_availability": "Edit availability",
"configure_availability": "Configure times when you are available for bookings.",
"copy_times_to": "Copy times to",
"change_weekly_schedule": "Change your weekly schedule",
"logo": "Logo",
"error": "Error",
@ -1046,8 +1054,8 @@
"navigate": "Navigate",
"open": "Open",
"close": "Close",
"pro_feature_teams": "This is a Pro feature. Upgrade to Pro to see your team's availability.",
"pro_feature_workflows": "This is a Pro feature. Upgrade to Pro to automate your event notifications and reminders with Workflows.",
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
"show_eventtype_on_profile": "Show on Profile",
"embed": "Embed",
"new_username": "New username",
@ -1147,7 +1155,6 @@
"set_availability_getting_started_subtitle_2": "You can customise all of this later in the availability page.",
"connect_calendars_from_app_store": "You can add more calendars from the app store",
"current_step_of_total": "Step {{currentStep}} of {{maxSteps}}",
"copy_all": "Copy All",
"add_variable": "Add variable",
"custom_phone_number": "Custom phone number",
"message_template": "Message template",
@ -1178,12 +1185,12 @@
"team_members": "Team members",
"more": "More",
"more_page_footer": "We view the mobile application as an extension of the web application. If you are performing any complication actions, please refer back to the web application.",
"workflow_example_1": "Send email reminder 24 hours before event starts to host",
"workflow_example_2": "Send SMS reminder 1 hour before event starts to host",
"workflow_example_3": "Send custom email when event is cancelled to host",
"workflow_example_4": "Send email reminder 24 hours before event starts to attendee",
"workflow_example_5": "Send SMS reminder 1 hour before event starts to attendee",
"workflow_example_6": "Send custom SMS when event is rescheduled to attendee",
"workflow_example_1": "Send SMS reminder 24 hours before event starts to attendee",
"workflow_example_2": "Send custom SMS when event is rescheduled to attendee",
"workflow_example_3": "Send custom email when new event is booked to host",
"workflow_example_4": "Send email reminder 1 hour before events starts to attendee",
"workflow_example_5": "Send custom email when event is rescheduled to host",
"workflow_example_6": "Send custom SMS when new event is booked to host",
"welcome_to_cal_header": "Welcome to Cal.com!",
"edit_form_later_subtitle": "Youll be able to edit this later.",
"connect_calendar_later": "I'll connect my calendar later",
@ -1197,10 +1204,20 @@
"team_disable_cal_branding_description": "Removes any Cal related brandings, i.e. 'Powered by Cal'",
"invited_by_team": "{{teamName}} has invited you to join their team as a {{role}}",
"token_invalid_expired": "Token is either invalid or expired.",
"exchange_add": "Connect to Microsoft Exchange",
"exchange_authentication": "Authentication method",
"exchange_authentication_standard": "Basic authentication",
"exchange_authentication_ntlm": "NTLM authentication",
"exchange_compression": "GZip compression",
"routing_forms_description": "You can see all forms and routes you have created here.",
"add_new_form": "Add new form",
"form_description": "Create your form to route a booker",
"copy_link_to_form": "Copy link to form",
"theme": "Theme",
"theme_applies_note": "This only applies to your public booking pages",
"theme_light": "Light",
"theme_dark": "Dark",
"theme_system": "System default",
"add_a_team": "Add a team",
"saml_config": "SAML Config",
"add_webhook_description": "Receive meeting data in real-time when something happens in Cal.com",
@ -1225,5 +1242,9 @@
"password_reset_leading":"If you did not receive the email soon, check that the email address you entered is correct, check your spam folder or reach out to support if the issue persists.",
"password_updated":"Password updated!",
"pending_payment": "Pending payment",
"confirmation_page_rainbow": "Token gate your event with tokens or NFTs on Ethereum, Polygon, and more."
"confirmation_page_rainbow": "Token gate your event with tokens or NFTs on Ethereum, Polygon, and more.",
"not_on_cal": "Not on Cal.com",
"no_calendar_installed": "No calendar installed",
"no_calendar_installed_description": "You have not yet connected any of your calendars",
"add_a_calendar": "Add a calendar"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} ha cancelado la reserva y le ha solicitado que elija otra hora.",
"request_reschedule_title_organizer": "Le ha solicitado a {{attendee}} que reprograme",
"request_reschedule_subtitle_organizer": "Ha cancelado la reserva y {{attendee}} deberá seleccionar una nueva hora de reserva con usted.",
"rescheduled_event_type_subject": "Solicitud de reprogramación enviada: {{eventType}} con {{name}} el {{date}}",
"requested_to_reschedule_subject_attendee": "Reprogramar acción requerida: reserva una nueva hora para {{eventType}} con {{name}}",
"reschedule_reason": "Motivo de la reprogramación",
"hi_user_name": "Hola {{userName}}",
"ics_event_title": "{{eventType}} con {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Contraseña de la reunión",
"meeting_url": "URL de la reunión",
"meeting_request_rejected": "Su solicitud de reunión ha sido rechazada",
"rescheduled_event_type_subject": "Solicitud de reprogramación enviada: {{eventType}} con {{name}} el {{date}}",
"requested_to_reschedule_subject_attendee": "Reprogramar acción requerida: reserva una nueva hora para {{eventType}} con {{name}}",
"rejected_event_type_with_organizer": "Rechazado: {{eventType}} con {{organizer}} en {{date}}",
"hi": "Hola",
"join_team": "Unirse al Equipo",
@ -1022,8 +1022,6 @@
"navigate": "Navegar",
"open": "Abrir",
"close": "Cerrar",
"pro_feature_teams": "Esta es una característica Pro. Actualice a Pro para ver la disponibilidad de su equipo.",
"pro_feature_workflows": "Esta es una característica profesional. Actualice a Pro para automatizar sus notificaciones de eventos y recordatorios con flujos de trabajo.",
"embed": "Insertar",
"new_username": "Nombre de usuario nuevo",
"current_username": "Nombre de usuario actual",
@ -1035,5 +1033,7 @@
"using_additional_inputs_as_variables": "¿Cómo usar entradas adicionales como variables?",
"download_desktop_app": "Descargar la aplicación de escritorio",
"set_ping_link": "Establecer enlace Ping",
"organizer_name_info": "Tu Nombre"
"organizer_name_info": "Tu Nombre",
"theme_light": "Claro",
"theme_dark": "Oscuro"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} a annulé la réservation et vous a demandé de choisir un autre moment.",
"request_reschedule_title_organizer": "Vous avez demandé à {{attendee}} de reporter",
"request_reschedule_subtitle_organizer": "Vous avez annulé la réservation et {{attendee}} doit choisir un nouvel horaire de réservation avec vous.",
"rescheduled_event_type_subject": "Demande de report envoyée : {{eventType}} avec {{name}} le {{date}}",
"requested_to_reschedule_subject_attendee": "Action requise - Report : veuillez réserver une nouvelle heure pour {{eventType}} avec {{name}}",
"reschedule_reason": "Raison du report",
"hi_user_name": "Bonjour {{userName}}",
"ics_event_title": "{{eventType}} avec {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Mot de passe de la réunion",
"meeting_url": "URL de la réunion",
"meeting_request_rejected": "Votre demande de réunion a été refusée",
"rescheduled_event_type_subject": "Demande de report envoyée : {{eventType}} avec {{name}} le {{date}}",
"requested_to_reschedule_subject_attendee": "Action requise - Report : veuillez réserver une nouvelle heure pour {{eventType}} avec {{name}}",
"rejected_event_type_with_organizer": "Refusé : {{eventType}} avec {{organizer}} le {{date}}",
"hi": "Bonjour",
"join_team": "Rejoindre l'équipe",
@ -1022,8 +1022,6 @@
"navigate": "Naviguer",
"open": "Ouvrir",
"close": "Fermer",
"pro_feature_teams": "Cette fonctionnalité est une fonctionnalité Pro. Passez en version Pro pour voir la disponibilité de votre équipe.",
"pro_feature_workflows": "Cette fonctionnalité est une fonctionnalité Pro. Passez en version Pro pour automatiser vos notifications et rappels d'événements avec les workflows.",
"embed": "Intégré",
"new_username": "Nouveau nom d'utilisateur",
"current_username": "Nom d'utilisateur actuel",
@ -1035,5 +1033,7 @@
"using_additional_inputs_as_variables": "Comment utiliser des entrées supplémentaires en tant que variables ?",
"download_desktop_app": "Télécharger l'application de bureau",
"set_ping_link": "Définir le lien Ping",
"organizer_name_info": "Votre nom"
"organizer_name_info": "Votre nom",
"theme_light": "Clair",
"theme_dark": "Sombre"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} ביטל/ה את ההזמנה וביקש/ה שתבחר/י מועד אחר.",
"request_reschedule_title_organizer": "ביקשת מ{{attendee}} לקבוע מועד חדש",
"request_reschedule_subtitle_organizer": "ביטלת את ההזמנה ו-{{attendee}} צריך/ה לתאם איתך מועד חדש.",
"rescheduled_event_type_subject": "נשלחה בקשה לקביעת מועד חדש: {{eventType}} עם {{name}} בתאריך {{date}}",
"requested_to_reschedule_subject_attendee": "הפעולה חייבה קביעת מועד חדש: יש לקבוע מועד חדש עבור {{eventType}} עם {{name}}",
"reschedule_reason": "הסיבה לשינוי המועד",
"hi_user_name": "שלום {{name}}",
"ics_event_title": "{{eventType}} עם {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "סיסמת הפגישה",
"meeting_url": "כתובת ה-URL של הפגישה",
"meeting_request_rejected": "בקשת הפגישה שלך נדחתה",
"rescheduled_event_type_subject": "נשלחה בקשה לקביעת מועד חדש: {{eventType}} עם {{name}} בתאריך {{date}}",
"requested_to_reschedule_subject_attendee": "הפעולה חייבה קביעת מועד חדש: יש לקבוע מועד חדש עבור {{eventType}} עם {{name}}",
"rejected_event_type_with_organizer": "נדחה: {{eventType}} עם {{organizer}} בתאריך {{date}}",
"hi": "שלום",
"join_team": "להצטרף לצוות",
@ -1022,8 +1022,6 @@
"navigate": "ניווט",
"open": "פתיחה",
"close": "סגירה",
"pro_feature_teams": "זוהי תכונה של גרסת Pro. כדאי לשדרג ל-Pro כדי לראות את זמינות חברי הצוות.",
"pro_feature_workflows": "זוהי תכונה של גרסת Pro. כדאי לשדרג ל-Pro כדי להפוך את התזכורות והעדכונים לגבי אירועים לאוטומטיים באמצעות תהליכי עבודה.",
"embed": "הטמעה",
"new_username": "שם משתמש חדש",
"current_username": "שם משתמש נוכחי",
@ -1035,5 +1033,7 @@
"using_additional_inputs_as_variables": "איך ניתן להשתמש בסוגי קלט נוספים כמשתנים?",
"download_desktop_app": "הורדת האפליקציה למחשבים שולחניים",
"set_ping_link": "הגדרת קישור Ping",
"organizer_name_info": "שמך"
"organizer_name_info": "שמך",
"theme_light": "בהיר",
"theme_dark": "כהה"
}

View File

@ -214,5 +214,7 @@
"organizer_name_workflow": "Szervező",
"location_workflow": "Helyszín",
"additional_notes_workflow": "Egyéb jegyzetek",
"organizer_name_info": "Neved"
"organizer_name_info": "Neved",
"theme_light": "Világos",
"theme_dark": "Sötét"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} ha annullato la prenotazione e ti ha chiesto di scegliere un altro orario.",
"request_reschedule_title_organizer": "Hai richiesto a {{attendee}} di riprogrammare",
"request_reschedule_subtitle_organizer": "Hai annullato la prenotazione e {{attendee}} deve selezionare un nuovo orario per la prenotazione.",
"rescheduled_event_type_subject": "Richiesta di riprogrammazione inviata: {{eventType}} con {{name}} il {{date}}",
"requested_to_reschedule_subject_attendee": "Azione richiesta per la riprogrammazione: prenota un nuovo orario per {{eventType}} con {{name}}",
"reschedule_reason": "Motivo della riprogrammazione",
"hi_user_name": "Ciao {{name}}",
"ics_event_title": "{{eventType}} con {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Password del meeting",
"meeting_url": "URL del Meeting",
"meeting_request_rejected": "La tua richiesta di riunione è stata rifiutata",
"rescheduled_event_type_subject": "Richiesta di riprogrammazione inviata: {{eventType}} con {{name}} il {{date}}",
"requested_to_reschedule_subject_attendee": "Azione richiesta per la riprogrammazione: prenota un nuovo orario per {{eventType}} con {{name}}",
"rejected_event_type_with_organizer": "Rifiutato: {{eventType}} con {{organizer}} il {{date}}",
"hi": "Ciao",
"join_team": "Unisciti al team",
@ -1022,8 +1022,6 @@
"navigate": "Esplora",
"open": "Apri",
"close": "Chiudi",
"pro_feature_teams": "Questa è una funzionalità Pro. Effettua l'upgrade a Pro per vedere la disponibilità del team.",
"pro_feature_workflows": "Questa è una funzionalità Pro. Effettua l'upgrade a Pro per automatizzare le notifiche e i promemoria degli eventi per i flussi di lavoro.",
"embed": "Incorpora",
"new_username": "Nuovo nome utente",
"current_username": "Nome utente corrente",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Scarica app desktop",
"set_ping_link": "Imposta link di Ping",
"missing_connected_calendar": "Nessun calendario predefinito collegato",
"organizer_name_info": "Il tuo nome"
"organizer_name_info": "Il tuo nome",
"theme_light": "Chiaro",
"theme_dark": "Scuro"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} が予約をキャンセルしました。別の時間を選択することをあなたにリクエストしています。",
"request_reschedule_title_organizer": "{{attendee}} にスケジュールの変更をリクエストしました",
"request_reschedule_subtitle_organizer": "予約をキャンセルしました。{{attendee}} はあなたとの予約の時間を新しく選択する必要があります。",
"rescheduled_event_type_subject": "スケジュール変更のリクエストを送信しました: {{date}} の {{name}} との {{eventType}}",
"requested_to_reschedule_subject_attendee": "スケジュール変更のアクションが必要です: {{name}} との {{eventType}} に新しい時間を予約してください",
"reschedule_reason": "スケジュール変更の理由",
"hi_user_name": "こんにちは、{{name}} さん",
"ics_event_title": "{{name}} との {{eventType}}",
@ -92,8 +94,6 @@
"meeting_password": "ミーティングパスワード",
"meeting_url": "ミーティング URL",
"meeting_request_rejected": "ミーティングリクエストが拒否されました",
"rescheduled_event_type_subject": "スケジュール変更のリクエストを送信しました: {{date}} の {{name}} との {{eventType}}",
"requested_to_reschedule_subject_attendee": "スケジュール変更のアクションが必要です: {{name}} との {{eventType}} に新しい時間を予約してください",
"rejected_event_type_with_organizer": "拒否されたリクエスト: {{date}} の {{organizer}} による{{eventType}}",
"hi": "こんにちは",
"join_team": "チームに参加",
@ -286,10 +286,10 @@
"cancellation_successful": "キャンセルしました",
"really_cancel_booking": "本当に予約をキャンセルしますか?",
"cannot_cancel_booking": "この予約はキャンセルできません",
"reschedule_instead": "代わりに、そのスケジュールを変更することもできます。",
"reschedule_instead": "代わりに、そのスケジュールを再設定することも可能です。",
"event_is_in_the_past": "イベントは過去のものです",
"cancelling_event_recurring": "このイベントは繰り返しイベントに含まれる 1 イベントです。",
"cancelling_all_recurring": "これらはすべて、繰り返しイベントに含まれる残りのイベントです。",
"cancelling_event_recurring": "このイベントは、繰り返しイベントに含まれている 1 つのインスタンスです。",
"cancelling_all_recurring": "これらはすべて、繰り返しイベントに含まれているインスタンスの残りです。",
"error_with_status_code_occured": "ステータスコード {{status}} に関するエラーが発生しました。",
"booking_already_cancelled": "この予約はすでにキャンセルされています",
"go_back_home": "ホームに戻る",
@ -346,7 +346,7 @@
"booking_cancelled": "予約がキャンセルされました",
"booking_rescheduled": "予約の再スケジュール済",
"booking_created": "予約を作成しました",
"form_submitted": "送信されたフォーム",
"form_submitted": "フォームが送信されました",
"event_triggers": "イベントトリガー",
"subscriber_url": "購読者の URL",
"create_new_webhook": "新しいウェブフックを作成",
@ -380,7 +380,7 @@
"enable": "有効にする",
"code": "コード",
"code_is_incorrect": "コードが正しくありません。",
"add_time_availability": "新しいタイムスロットを追加",
"add_time_availability": "新しい時間帯を追加",
"add_an_extra_layer_of_security": "パスワードが盗まれる場合に備えて、アカウントのセキュリティを強化します。",
"2fa": "二要素認証",
"enable_2fa": "二要素認証を有効にする",
@ -406,10 +406,10 @@
"new_password_matches_old_password": "新しいパスワードは古いパスワードと同じです。別のパスワードを選択してください。",
"forgotten_secret_description": "このシークレットを紛失または忘れた場合は変更が可能です。しかしこのシークレットを使っている連携はすべて最新のものにする必要があります",
"current_incorrect_password": "現在のパスワードが正しくありません",
"password_hint_caplow": "大文字と小文字を混ぜる",
"password_hint_min": "7 文字以上",
"password_hint_num": "少なくとも 1 つの数字を含める",
"invalid_password_hint": "パスワードは少なくとも1つの数字を含み、大文字と小文字を混在させた7文字以上のものでなければなりません",
"password_hint_caplow": "アルファベットの大文字と小文字を両方使用してください",
"password_hint_min": "7 文字以上入力してください",
"password_hint_num": "少なくとも 1 文字は数字を含めてください",
"invalid_password_hint": "パスワードには少なくとも 1 文字以上数字を含め、アルファベットの大文字と小文字を両方使用した上で 7 文字以上の長さに設定してください",
"incorrect_password": "パスワードが正しくありません。",
"1_on_1": "1対1",
"24_h": "24時間",
@ -452,7 +452,7 @@
"booking_reschedule_confirmation": "{{profileName}} の {{eventTypeTitle}} をスケジュール変更する",
"in_person_meeting": "リンクまたは対面ミーティング",
"link_meeting": "ミーティングをリンク",
"phone_call": "電話番号",
"phone_call": "出席者の電話番号",
"your_number": "あなたの電話番号",
"phone_number": "電話番号",
"attendee_phone_number": "出席者の電話番号",
@ -624,8 +624,8 @@
"change_weekly_schedule": "ウィークリースケジュールを変更する",
"logo": "ロゴ",
"error": "エラー",
"at_least_characters_one": "少なくとも1文字を入力してください",
"at_least_characters_other": "少なくとも {{count}} 文字以上入力してください",
"at_least_characters_one": "少なくとも 1 文字以上入力してください",
"at_least_characters_other": "少なくとも {{count}} 文字以上入力してください",
"team_logo": "チームロゴ",
"add_location": "場所を追加",
"attendees": "出席者",
@ -648,7 +648,7 @@
"starting": "開始",
"disable_guests": "ゲストを無効化",
"disable_guests_description": "予約中のゲストの追加を無効にします。",
"private_link": "プライベート URL を生成",
"private_link": "プライベートリンクを生成",
"copy_private_link": "プライベートリンクをコピー",
"private_link_description": "Cal ユーザー名を公開せずに共有するプライベートURLを生成",
"invitees_can_schedule": "招待されるメンバーはスケジュールできる",
@ -679,7 +679,7 @@
"add_new_event_type": "新しいイベントタイプを追加する",
"new_event_type_to_book_description": "ユーザーが時間を予約する際に使う新しいイベントタイプを作成する。",
"length": "長さ",
"minimum_booking_notice": "最低限の予約通知",
"minimum_booking_notice": "最低限の通知",
"slot_interval": "時間帯の間隔",
"slot_interval_default": "イベントの長さを使用する (デフォルト)",
"delete_event_type_description": "このイベントタイプを削除してもよろしいですか? あなたがこのリンクを共有した人は、それを使って予約することができなくなります。",
@ -809,9 +809,9 @@
"booking_full": "もう空席はありません",
"api_keys": "API キー",
"api_key": "API キー",
"test_api_key": "API キーをテストする",
"test_api_key": "API キーをテスト",
"test_passed": "テストに合格しました!",
"test_failed": "テストに合格しませんでした",
"test_failed": "テストに失敗しました",
"provide_api_key": "API キーを提供する",
"api_key_modal_subtitle": "API キーを使用すると、自分のアカウントを API で呼び出すことができます。",
"api_keys_subtitle": "自分のアカウントへアクセスする際に使用する API キーを生成します。",
@ -850,7 +850,7 @@
"edit_booking": "予約を編集",
"reschedule_booking": "予約のスケジュールを変更",
"former_time": "以前の時間",
"confirmation_page_gif": "確認ページ用の GIF",
"confirmation_page_gif": "確認ページに GIF を追加する",
"search": "検索",
"impersonate": "なりすます",
"user_impersonation_heading": "ユーザーなりすまし",
@ -877,7 +877,7 @@
"choose_ways_put_cal_site": "サイトに Cal を設置する方法を、次の中から 1 つ選んでください。",
"setting_up_zapier": "Zapier 連携の設定",
"generate_api_key": "API キーを生成",
"generate_api_key_description": "Cal.com で使用する API キーを生成する",
"generate_api_key_description": "以下から Cal.com で使用する API キーを生成する",
"your_unique_api_key": "あなた専用の API キー",
"copy_safe_api_key": "この API キーをコピーして安全な場所に保存してください。このキーを紛失した場合は、新しいキーを生成する必要があります。",
"zapier_setup_instructions": "<0>Zapier アカウントにログインし、新しい Zap を作成します。</0><1>Cal.com を Trigger アプリとして選択します。Trigger イベントも選択します。</1><2>アカウントを選択し、あなた専用の API キーを入力します。</2><3>トリガーをテストします。</3><4>設定完了!</4>",
@ -908,7 +908,7 @@
"feedback": "フィードバック",
"submitted_feedback": "フィードバックをありがとうございます!",
"feedback_error": "フィードバックの送信エラー",
"comments": "コメント",
"comments": "こちらでコメントを共有してください:",
"booking_details": "予約の詳細",
"or_lowercase": "または",
"nevermind": "無視する",
@ -952,7 +952,7 @@
"hour_timeUnit": "時間",
"minute_timeUnit": "分",
"new_workflow_heading": "最初のワークフローを作成",
"new_workflow_description": "ワークフローにより、リマインダーと通知の送信を自動化できます。",
"new_workflow_description": "ワークフローを使用することにより、リマインダーと通知の送信を自動化できます。",
"active_on": "有効日時",
"workflow_updated_successfully": "{{workflowName}} が正常に更新されました",
"premium_to_standard_username_description": "これはスタンダードユーザー名で、更新するとダウングレードするための請求画面に移動します。",
@ -970,8 +970,8 @@
"web_conference": "ウェブ会議",
"number_for_sms_reminders": "電話番号SMS リマインダー用)",
"requires_confirmation": "確認が必要です",
"nr_event_type_one": "{{count}} イベントタイプ",
"nr_event_type_other": "{{count}} イベントタイプ",
"nr_event_type_one": "{{count}} 種類のイベントタイプ",
"nr_event_type_other": "{{count}} 種類のイベントタイプ",
"add_action": "アクションを追加",
"set_whereby_link": "Wherebyリンクを設定",
"invalid_whereby_link": "有効なWherebyリンクを入力してください",
@ -979,14 +979,14 @@
"invalid_around_link": "有効なAroundリンクを入力してください",
"set_riverside_link": "Riversideリンクを設定",
"invalid_riverside_link": "有効なRiversideリンクを入力してください",
"invalid_ping_link": "有効な Ping.gg リンクを入力してください",
"invalid_ping_link": "有効な Ping.gg リンクを入力してください",
"add_exchange2013": "Exchange 2013 のサーバーに接続",
"add_exchange2016": "Exchange 2016 のサーバーに接続",
"custom_template": "カスタムテンプレート",
"email_body": "本文",
"email_body": "メールの本文",
"subject": "件名",
"text_message": "テキストメッセージ",
"specific_issue": "特定の問題があります",
"specific_issue": "特定の問題がおありですか?",
"browse_our_docs": "ドキュメントを閲覧",
"choose_template": "テンプレートを選択する",
"reminder": "リマインダー",
@ -1010,32 +1010,32 @@
"new_seat_subject": "{{date}} の {{eventType}} への新規参加者 {{name}}",
"new_seat_title": "別のユーザーがイベントに自身を追加しました",
"variable": "変数",
"event_name_workflow": "イベント名",
"organizer_name_workflow": "主催者名",
"attendee_name_workflow": "出席者名",
"event_name_workflow": "イベント",
"organizer_name_workflow": "主催者",
"attendee_name_workflow": "出席者",
"event_date_workflow": "イベントの日付",
"event_time_workflow": "イベントの時間",
"location_workflow": "在地",
"additional_notes_workflow": "追加メモ",
"location_workflow": "所",
"additional_notes_workflow": "備考",
"app_upgrade_description": "この機能を利用するには、Pro アカウントへのアップグレードが必要です。",
"invalid_number": "電話番号が無効です",
"navigate": "ナビゲート",
"open": "開く",
"close": "閉じる",
"pro_feature_teams": "これは Pro 機能です。Pro にアップグレードすると、チームの参加可能状況を確認することができます。",
"pro_feature_workflows": "これは Pro 機能です。Pro にアップグレードすると、ワークフローを使って、イベントの通知やリマインダーを自動化できます。",
"embed": "埋め込み",
"embed": "埋め込む",
"new_username": "新しいユーザー名",
"current_username": "現在のユーザー名",
"example_1": "例 1",
"example_2": "例 2",
"company_size": "会社規模",
"what_help_needed": "お困りのことは何ですか?",
"company_size": "会社規模",
"what_help_needed": "何かお困りですか?",
"variable_format": "変数の形式",
"custom_input_as_variable_info": "追加入力ラベルの特殊文字はすべて無視し(文字と数字のみ使用)、文字はすべて大文字、空白はアンダースコアに置き換えます。",
"using_additional_inputs_as_variables": "変数として追加の入力を使用する方法は",
"download_desktop_app": "デスクトップアプリをダウンロードする",
"custom_input_as_variable_info": "追加の入力ラベルに含まれている特殊文字はすべて無視し (アルファベットと数字のみを使用)、アルファベットをすべて大文字に、そして空白をアンダースコアへと変換します。",
"using_additional_inputs_as_variables": "追加の入力内容を変数として使用するにはどうすればよいですか",
"download_desktop_app": "デスクトップアプリをダウンロード",
"set_ping_link": "Ping リンクを設定",
"missing_connected_calendar": "カレンダーが接続されていません",
"organizer_name_info": "あなたの名前"
"missing_connected_calendar": "デフォルトのカレンダーが接続されていません",
"organizer_name_info": "あなたの名前",
"theme_light": "明るさ",
"theme_dark": "暗め"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}}에서 예약을 취소했고 다른 시간을 선택할 것을 요청했습니다.",
"request_reschedule_title_organizer": "{{attendee}}에게 일정 변경을 요청하셨습니다",
"request_reschedule_subtitle_organizer": "예약을 취소하셨기 때문에 {{attendee}} 님이 새 예약 시간을 선택해야 합니다.",
"rescheduled_event_type_subject": "일정 변경 요청이 보내짐: {{date}}에 {{name}}(으)로 {{eventType}}",
"requested_to_reschedule_subject_attendee": "일정 변경 조치 필요: {{name}} 님과 함께 새로운 {{eventType}} 시간을 예약하십시오",
"reschedule_reason": "일정 변경 사유",
"hi_user_name": "안녕하세요 {{name}}",
"ics_event_title": "{{eventType}}과 {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "회의 비밀번호",
"meeting_url": "회의 URL",
"meeting_request_rejected": "회의 요청이 거절되었습니다.",
"rescheduled_event_type_subject": "일정 변경 요청이 보내짐: {{date}}에 {{name}}(으)로 {{eventType}}",
"requested_to_reschedule_subject_attendee": "일정 변경 조치 필요: {{name}} 님과 함께 새로운 {{eventType}} 시간을 예약하십시오",
"rejected_event_type_with_organizer": "거절됨: {{date}} {{organizer}} 의 {{eventType}}",
"hi": "안녕하세요",
"join_team": "팀에 합류하세요",
@ -1022,8 +1022,6 @@
"navigate": "탐색하기",
"open": "열기",
"close": "닫기",
"pro_feature_teams": "이것은 Pro 기능입니다. 팀의 가용성을 확인하려면 Pro로 업그레이드하세요.",
"pro_feature_workflows": "이것은 Pro 기능입니다. 워크플로로 이벤트 알림 및 미리 알림을 자동화하려면 Pro로 업그레이드하세요.",
"embed": "내장",
"new_username": "새 사용자 이름",
"current_username": "현재 사용자 이름",
@ -1037,5 +1035,7 @@
"download_desktop_app": "데스크탑 앱 다운로드",
"set_ping_link": "Ping 링크 설정",
"missing_connected_calendar": "연결된 캘린더 없음",
"organizer_name_info": "당신의 이름"
"organizer_name_info": "당신의 이름",
"theme_light": "밝은",
"theme_dark": "어두움"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} heeft de boeking geannuleerd en u verzocht een andere tijd te kiezen.",
"request_reschedule_title_organizer": "U heeft {{attendee}} gevraagd om te verplaatsen",
"request_reschedule_subtitle_organizer": "U heeft de boeking geannuleerd en {{attendee}} moet samen met u een nieuwe boekingstijd kiezen.",
"rescheduled_event_type_subject": "Verplaatst: {{eventType}} met {{name}} op {{date}}",
"requested_to_reschedule_subject_attendee": "Actie vereist voor verplaatsing: boek een nieuwe tijd voor {{eventType}} met {{name}}",
"reschedule_reason": "Reden voor verplaatsen",
"hi_user_name": "Hallo {{name}}",
"ics_event_title": "{{eventType}} met {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Deelneming wachtwoord",
"meeting_url": "URL afspraak",
"meeting_request_rejected": "Uw afspraak is helaas afgewezen",
"rescheduled_event_type_subject": "Verplaatst: {{eventType}} met {{name}} op {{date}}",
"requested_to_reschedule_subject_attendee": "Actie vereist voor verplaatsing: boek een nieuwe tijd voor {{eventType}} met {{name}}",
"rejected_event_type_with_organizer": "Afgewezen: {{eventType}} met {{organizer}} op {{date}}",
"hi": "Hallo",
"join_team": "Lidverzoek accepteren",
@ -1022,8 +1022,6 @@
"navigate": "Navigeren",
"open": "Openen",
"close": "Sluiten",
"pro_feature_teams": "Dit is een Pro-functie. Upgrade naar Pro om de beschikbaarheid van uw team te zien.",
"pro_feature_workflows": "Dit is een Pro-functie. Upgrade naar Pro om uw meldingen en herinneringen van gebeurtenissen te automatiseren met werkstromen.",
"embed": "Insluiten",
"new_username": "Nieuwe gebruikersnaam",
"current_username": "Huidige gebruikersnaam",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Desktop-app downloaden",
"set_ping_link": "Ping-link instellen",
"missing_connected_calendar": "Geen standaard agenda verbonden",
"organizer_name_info": "Uw naam"
"organizer_name_info": "Uw naam",
"theme_light": "Licht",
"theme_dark": "Donker"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} anulował(a) rezerwację i prosi Cię o wybranie innego terminu.",
"request_reschedule_title_organizer": "{{attendee}} poproszono o zmianę harmonogramu",
"request_reschedule_subtitle_organizer": "Rezerwacja została przez Ciebie anulowana i {{attendee}} musi wybrać z Tobą nowy czas rezerwacji.",
"rescheduled_event_type_subject": "Wysłano prośbę o zmianę terminu: {{eventType}} o nazwie {{name}}, z {{date}}",
"requested_to_reschedule_subject_attendee": "Działanie wymagało zmiany harmonogramu: Zarezerwuj nową godzinę na wydarzenie {{eventType}} z {{name}}",
"reschedule_reason": "Powód zmiany harmonogramu",
"hi_user_name": "Cześć {{name}}",
"ics_event_title": "{{eventType}} z {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Hasło Spotkania",
"meeting_url": "URL Spotkania",
"meeting_request_rejected": "Twoja prośba o spotkanie została odrzucona",
"rescheduled_event_type_subject": "Wysłano prośbę o zmianę terminu: {{eventType}} o nazwie {{name}}, z {{date}}",
"requested_to_reschedule_subject_attendee": "Działanie wymagało zmiany harmonogramu: Zarezerwuj nową godzinę na wydarzenie {{eventType}} z {{name}}",
"rejected_event_type_with_organizer": "Odrzucono: {{eventType}} z {{organizer}} w {{date}}",
"hi": "Cześć",
"join_team": "Dołącz do zespołu",
@ -1022,8 +1022,6 @@
"navigate": "Nawigacja",
"open": "Otwórz",
"close": "Zamknij",
"pro_feature_teams": "To jest funkcja Pro. Ulepsz do wersji Pro, aby zobaczyć dostępność Twojego zespołu.",
"pro_feature_workflows": "To jest funkcja Pro. Ulepsz do wersji Pro, aby za pomocą funkcji Przepływy pracy zautomatyzować powiadomienia o wydarzeniach i przypomnienia.",
"embed": "Osadź",
"new_username": "Nowa nazwa użytkownika",
"current_username": "Bieżąca nazwa użytkownika",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Pobierz aplikację komputerową",
"set_ping_link": "Ustaw link do aplikacji Ping",
"missing_connected_calendar": "Nie podłączono kalendarza domyślnego",
"organizer_name_info": "Twoje imię"
"organizer_name_info": "Twoje imię",
"theme_light": "Jasny",
"theme_dark": "Ciemny"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} cancelou a reserva e solicitou que você escolhesse outro horário.",
"request_reschedule_title_organizer": "Você solicitou que {{attendee}} reagendasse",
"request_reschedule_subtitle_organizer": "Você cancelou a reserva e {{attendee}} deve escolher outro horário de reserva com você.",
"rescheduled_event_type_subject": "Solicitação de reagendamento enviado: {{eventType}} com {{name}} às {{date}}",
"requested_to_reschedule_subject_attendee": "Ação de reagendamento necessária: reserve outro horário para {{eventType}} com {{name}}",
"reschedule_reason": "Motivo do reagendamento",
"hi_user_name": "Olá {{name}}",
"ics_event_title": "{{eventType}} com {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Senha da reunião",
"meeting_url": "URL da reunião",
"meeting_request_rejected": "A sua solicitação de reunião foi rejeitada",
"rescheduled_event_type_subject": "Solicitação de reagendamento enviado: {{eventType}} com {{name}} às {{date}}",
"requested_to_reschedule_subject_attendee": "Ação de reagendamento necessária: reserve outro horário para {{eventType}} com {{name}}",
"rejected_event_type_with_organizer": "Rejeitado: {{eventType}} com {{organizer}} em {{date}}",
"hi": "Olá",
"join_team": "Entrar no time",
@ -1022,8 +1022,6 @@
"navigate": "Navegar",
"open": "Abrir",
"close": "Fechar",
"pro_feature_teams": "Este é um recurso Pro. Atualize para o Pro para ver a disponibilidade da sua equipe.",
"pro_feature_workflows": "Este é um recurso Pro. Atualize para o Pro para automatizar suas notificações de eventos e lembretes com fluxos de trabalho.",
"embed": "Inserir",
"new_username": "Novo nome de usuário",
"current_username": "Nome de usuário atual",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Baixar app para desktop",
"set_ping_link": "Definir link do Ping",
"missing_connected_calendar": "Nenhum calendário padrão conectado",
"organizer_name_info": "Seu nome"
"organizer_name_info": "Seu nome",
"theme_light": "Claro",
"theme_dark": "Escuro"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} cancelou a reserva e pediu para que escolhesse outro horário.",
"request_reschedule_title_organizer": "Solicitou um reagendamento a {{attendee}}",
"request_reschedule_subtitle_organizer": "Cancelou a reserva e {{attendee}} deve escolher um novo horário para a reserva consigo.",
"rescheduled_event_type_subject": "Pedido de reagendamento enviado: {{eventType}} com {{name}} em {{date}}",
"requested_to_reschedule_subject_attendee": "Acção necessária para reagendamento: Por favor reserve uma nova hora para {{eventType}} com {{name}}",
"reschedule_reason": "Motivo do reagendamento",
"hi_user_name": "Olá {{userName}}",
"ics_event_title": "{{eventType}} com {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Senha da reunião",
"meeting_url": "URL da reunião",
"meeting_request_rejected": "O seu pedido de reunião foi rejeitado",
"rescheduled_event_type_subject": "Pedido de reagendamento enviado: {{eventType}} com {{name}} em {{date}}",
"requested_to_reschedule_subject_attendee": "Acção necessária para reagendamento: Por favor reserve uma nova hora para {{eventType}} com {{name}}",
"rejected_event_type_with_organizer": "Rejeitado: {{eventType}} com {{organizer}} em {{date}}",
"hi": "Olá",
"join_team": "Junte-se à equipa",
@ -1022,8 +1022,6 @@
"navigate": "Navegar",
"open": "Abrir",
"close": "Fechar",
"pro_feature_teams": "Esta é uma funcionalidade Pro. Actualize para o Pro para ver a disponibilidade da sua equipa.",
"pro_feature_workflows": "Esta é uma funcionalidade Pro. Actualize para o Pro para automatizar as suas notificações de eventos e lembretes com os fluxos de trabalho.",
"embed": "Incorporar",
"new_username": "Novo nome de utilizador",
"current_username": "Nome de utilizador atual",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Descarregar a aplicação para o computador",
"set_ping_link": "Definir ligação do Ping",
"missing_connected_calendar": "Nenhum calendário padrão associado",
"organizer_name_info": "O seu nome"
"organizer_name_info": "O seu nome",
"theme_light": "Claro",
"theme_dark": "Escuro"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} a anulat rezervarea și v-a cerut să alegeți o altă dată.",
"request_reschedule_title_organizer": "I-ați cerut lui {{attendee}} să reprogrameze",
"request_reschedule_subtitle_organizer": "Ați anulat rezervarea și {{attendee}} trebuie să aleagă o nouă dată pentru rezervarea cu dvs.",
"rescheduled_event_type_subject": "Cerere de reprogramare trimisă: {{eventType}} cu {{name}} în {{date}}",
"requested_to_reschedule_subject_attendee": "Acțiunea necesită reprogramare: rezervați o nouă dată pentru {{eventType}} cu {{name}}",
"reschedule_reason": "Motivul reprogramării",
"hi_user_name": "Salut {{userName}}",
"ics_event_title": "{{eventType}} cu {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Parolă ședință",
"meeting_url": "URL ședință",
"meeting_request_rejected": "Solicitarea dvs. în şedinţă a fost respinsă",
"rescheduled_event_type_subject": "Cerere de reprogramare trimisă: {{eventType}} cu {{name}} în {{date}}",
"requested_to_reschedule_subject_attendee": "Acțiunea necesită reprogramare: rezervați o nouă dată pentru {{eventType}} cu {{name}}",
"rejected_event_type_with_organizer": "Respins: {{eventType}} cu {{organizer}} pe {{date}}",
"hi": "Salut",
"join_team": "Alătură-te echipei",
@ -1022,8 +1022,6 @@
"navigate": "Navigare",
"open": "Deschidere",
"close": "Închidere",
"pro_feature_teams": "Aceasta este o caracteristică Pro. Faceți upgrade la Pro pentru a vedea disponibilitatea echipei dvs.",
"pro_feature_workflows": "Aceasta este o caracteristică Pro. Faceți upgrade la Pro pentru a automatiza notificările și mementourile evenimentului cu Fluxurile de lucru.",
"embed": "Încorporare",
"new_username": "Nume de utilizator nou",
"current_username": "Nume de utilizator actual",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Descărcați aplicația pentru desktop",
"set_ping_link": "Setare link Ping",
"missing_connected_calendar": "Nu este conectat niciun calendar implicit",
"organizer_name_info": "Numele tău"
"organizer_name_info": "Numele tău",
"theme_light": "Lumină",
"theme_dark": "Întunecat"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} отменил(-а) бронирование и попросил(-а) вас выбрать другое время.",
"request_reschedule_title_organizer": "Вы запросили у {{attendee}} изменение расписания",
"request_reschedule_subtitle_organizer": "Вы отменили бронирование, и {{attendee}} теперь должен(-на) выбрать новое время бронирования.",
"rescheduled_event_type_subject": "Запрос на перенос отправлен: {{eventType}} с {{name}}, {{date}}",
"requested_to_reschedule_subject_attendee": "Требуется перенести бронирование: забронируйте новое время для {{eventType}} с {{name}}",
"reschedule_reason": "Причина переноса",
"hi_user_name": "Привет {{userName}}",
"ics_event_title": "{{eventType}} с {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Пароль встречи",
"meeting_url": "URL встречи",
"meeting_request_rejected": "Ваш запрос на встречу отклонен",
"rescheduled_event_type_subject": "Запрос на перенос отправлен: {{eventType}} с {{name}}, {{date}}",
"requested_to_reschedule_subject_attendee": "Требуется перенести бронирование: забронируйте новое время для {{eventType}} с {{name}}",
"rejected_event_type_with_organizer": "Отклонено: {{eventType}} с {{organizer}} на {{date}}",
"hi": "Привет",
"join_team": "Вступить в команду",
@ -1022,8 +1022,6 @@
"navigate": "Перейти",
"open": "Открыть",
"close": "Закрыть",
"pro_feature_teams": "Эта функция доступна в тарифном плане Pro. Перейдите на тариф Pro, чтобы увидеть доступность команды.",
"pro_feature_workflows": "Эта функция доступна в тарифном плане Pro. Перейдите на тариф Pro, чтобы автоматизировать уведомления и напоминания о событиях с помощью рабочих процессов.",
"embed": "Встроить",
"new_username": "Новое имя пользователя",
"current_username": "Текущее имя пользователя",
@ -1035,5 +1033,7 @@
"using_additional_inputs_as_variables": "Как использовать ответы на дополнительные вопросы в качестве переменных?",
"download_desktop_app": "Скачать приложение для компьютера",
"set_ping_link": "Ссылка на Ping",
"organizer_name_info": "Ваше имя"
"organizer_name_info": "Ваше имя",
"theme_light": "Светлая",
"theme_dark": "Тёмная"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} je otkazao/la rezervaciju i tražio/la je da odaberete drugo vreme.",
"request_reschedule_title_organizer": "Zatražili ste da {{attendee}} promeni vreme rezervacije",
"request_reschedule_subtitle_organizer": "Otkazali ste rezervaciju i {{attendee}} treba da odabere novo vreme rezervacije sa vama.",
"rescheduled_event_type_subject": "Odloženo: {{eventType}} sa {{name}} datuma {{date}}",
"requested_to_reschedule_subject_attendee": "Promena vremena zahteva radnju: zakažite novo vreme za {{eventType}} sa {{name}}",
"reschedule_reason": "Razlog za promenu vremena",
"hi_user_name": "Zdravo {{name}}",
"ics_event_title": "{{eventType}} sa {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Lozinka Sastanka",
"meeting_url": "URL Sastanka",
"meeting_request_rejected": "Vaš zahtev za sastanak je odbijen",
"rescheduled_event_type_subject": "Odloženo: {{eventType}} sa {{name}} datuma {{date}}",
"requested_to_reschedule_subject_attendee": "Promena vremena zahteva radnju: zakažite novo vreme za {{eventType}} sa {{name}}",
"rejected_event_type_with_organizer": "Odbijeno: {{eventType}} sa {{organizer}} datuma {{date}}",
"hi": "Zdravo",
"join_team": "Pridruži se timu",
@ -1022,8 +1022,6 @@
"navigate": "Navigacija",
"open": "Otvori",
"close": "Zatvori",
"pro_feature_teams": "Ovo je Pro funkcija. Nadogradite na Pro verziju da biste videli dostupnost vašeg tima.",
"pro_feature_workflows": "Ovo je Pro funkcija. Nadogradite na Pro verziju da biste automatizovali notifikacije svojih događaja i podsetnike sa radnim tokovima.",
"embed": "Ugradi",
"new_username": "Novo korisničko ime",
"current_username": "Trenutno korisničko ime",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Preuzmite aplikaciju za stoni računar",
"set_ping_link": "Podesi Ping vezu",
"missing_connected_calendar": "Nije povezan podrazumevani kalendar",
"organizer_name_info": "Vaše ime"
"organizer_name_info": "Vaše ime",
"theme_light": "Svetla",
"theme_dark": "Tamna"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} har avbokat bokningen och bett dig att välja en annan tid.",
"request_reschedule_title_organizer": "Du har begärt att {{attendee}} ska boka om",
"request_reschedule_subtitle_organizer": "Du har avbokat bokningen och {{attendee}} ska välja en ny bokningstid hos dig.",
"rescheduled_event_type_subject": "Begäran om ny schemaläggning har skickats: {{eventType}} med {{name}} {{date}}",
"requested_to_reschedule_subject_attendee": "Åtgärd som krävs Tidsschema: Boka en ny tid för {{eventType}} hos {{name}}",
"reschedule_reason": "Anledning till ombokning",
"hi_user_name": "Hej {{name}}",
"ics_event_title": "{{eventType}} med {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Lösenord för möte",
"meeting_url": "Mötes-URL",
"meeting_request_rejected": "Din mötesförfrågan har avböjts",
"rescheduled_event_type_subject": "Begäran om ny schemaläggning har skickats: {{eventType}} med {{name}} {{date}}",
"requested_to_reschedule_subject_attendee": "Åtgärd som krävs Tidsschema: Boka en ny tid för {{eventType}} hos {{name}}",
"rejected_event_type_with_organizer": "Avvisad: {{eventType}} med {{organizer}} vid {{date}}",
"hi": "Hej",
"join_team": "Anslut till team",
@ -1022,8 +1022,6 @@
"navigate": "Navigera",
"open": "Öppna",
"close": "Stäng",
"pro_feature_teams": "Detta är en Pro-funktion. Uppgradera till Pro för att se lagets tillgänglighet.",
"pro_feature_workflows": "Detta är en Pro-funktion. Uppgradera till Pro för att automatisera dina händelseaviseringar och påminnelser med arbetsflöden.",
"embed": "Bädda in",
"new_username": "Nytt användarnamn",
"current_username": "Nuvarande användarnamn",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Ladda ned skrivbordsappen",
"set_ping_link": "Ange Ping-länk",
"missing_connected_calendar": "Ingen standardkalender ansluten",
"organizer_name_info": "Ditt namn"
"organizer_name_info": "Ditt namn",
"theme_light": "Ljus",
"theme_dark": "Mörk"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} rezervasyonu iptal etti ve sizden başka bir saat seçmenizi istiyor.",
"request_reschedule_title_organizer": "{{attendee}} adlı kişiye yeniden planlama talebinde bulundunuz",
"request_reschedule_subtitle_organizer": "Rezervasyonu iptal ettiniz ve {{attendee}} isimli kişinin sizinle yeni bir rezervasyon saati belirlemesi gerekiyor.",
"rescheduled_event_type_subject": "Yeniden planlama isteği gönderildi: {{date}} tarihinde {{name}} ile {{eventType}}",
"requested_to_reschedule_subject_attendee": "Yeniden Planlama İşlemi Gerekiyor: Lütfen {{name}} ile {{eventType}} için yeni bir saat belirleyin",
"reschedule_reason": "Yeniden planlama nedeni",
"hi_user_name": "Merhaba {{name}}",
"ics_event_title": "{{name}} ile {{eventType}}",
@ -92,8 +94,6 @@
"meeting_password": "Toplantı Şifresi",
"meeting_url": "Toplantı URL'si",
"meeting_request_rejected": "Toplantı isteğiniz reddedildi",
"rescheduled_event_type_subject": "Yeniden planlama isteği gönderildi: {{date}} tarihinde {{name}} ile {{eventType}}",
"requested_to_reschedule_subject_attendee": "Yeniden Planlama İşlemi Gerekiyor: Lütfen {{name}} ile {{eventType}} için yeni bir saat belirleyin",
"rejected_event_type_with_organizer": "Reddedildi: {{date}} tarihinde {{organizer}} ile {{eventType}}",
"hi": "Merhaba",
"join_team": "Ekibe katıl",
@ -1022,8 +1022,6 @@
"navigate": "Gezin",
"open": "Aç",
"close": "Kapat",
"pro_feature_teams": "Bu bir Pro sürüm özelliğidir. Ekibinizin müsaitlik durumunu görebilmek için Pro sürüme geçin.",
"pro_feature_workflows": "Bu bir Pro sürüm özelliğidir. İş Akışlarıyla ilgili etkinlik bildirimlerinizi ve hatırlatıcılarınızı otomatikleştirmek için Pro sürüme geçin.",
"embed": "Gömülü",
"new_username": "Yeni kullanıcı adı",
"current_username": "Mevcut kullanıcı adı",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Masaüstü uygulamasını indirin",
"set_ping_link": "Ping bağlantısını ayarla",
"missing_connected_calendar": "Bağlı takvim yok",
"organizer_name_info": "Adınız"
"organizer_name_info": "Adınız",
"theme_light": "Açık",
"theme_dark": "Koyu"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} скасував(-ла) бронювання та попросив(-ла) вас вибрати інший час.",
"request_reschedule_title_organizer": "Ви попросили учасника {{attendee}} перенести бронювання",
"request_reschedule_subtitle_organizer": "Ви скасували бронювання. {{attendee}} має забронювати інший час.",
"rescheduled_event_type_subject": "Запит на перенесення надіслано: {{eventType}} «{{name}}», {{date}}",
"requested_to_reschedule_subject_attendee": "Потрібно перенести бронювання: забронюйте новий час для заходу «{{eventType}}» із користувачем {{name}}",
"reschedule_reason": "Причина перенесення",
"hi_user_name": "Привіт, {{name}}!",
"ics_event_title": "{{eventType}} «{{name}}»",
@ -92,8 +94,6 @@
"meeting_password": "Пароль наради",
"meeting_url": "URL-адреса наради",
"meeting_request_rejected": "Ваш запит на нараду відхилено",
"rescheduled_event_type_subject": "Запит на перенесення надіслано: {{eventType}} «{{name}}», {{date}}",
"requested_to_reschedule_subject_attendee": "Потрібно перенести бронювання: забронюйте новий час для заходу «{{eventType}}» із користувачем {{name}}",
"rejected_event_type_with_organizer": "Відхилено: {{eventType}} «{{organizer}}», {{date}}",
"hi": "Привіт",
"join_team": "Увійти в команду",
@ -1022,8 +1022,6 @@
"navigate": "Перейти",
"open": "Відкрити",
"close": "Закрити",
"pro_feature_teams": "Ця функція доступна в плані Pro. Перейдіть на нього, щоб бачити відомості про доступність учасників своєї команди.",
"pro_feature_workflows": "Ця функція доступна в плані Pro. Перейдіть на нього, щоб автоматизувати сповіщення та нагадування про заходи за допомогою робочих процесів.",
"embed": "Вбудувати",
"new_username": "Нове ім’я користувача",
"current_username": "Поточне ім’я користувача",
@ -1035,5 +1033,7 @@
"using_additional_inputs_as_variables": "Як використовувати додаткові поля введення як змінні?",
"download_desktop_app": "Завантажити додаток для ПК",
"set_ping_link": "Установити Ping-посилання",
"organizer_name_info": "Ваше ім’я"
"organizer_name_info": "Ваше ім’я",
"theme_light": "Світла",
"theme_dark": "Темна"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} đã huỷ đặt chỗ đó và yêu cầu bạn chọn thời gian khác.",
"request_reschedule_title_organizer": "Bạn đã yêu cầu {{attendee}} xếp lại lịch",
"request_reschedule_subtitle_organizer": "Bạn đã huỷ mục đặt chỗ này và {{attendee}} nên chọn thời gian đặt chỗ mới với bạn.",
"rescheduled_event_type_subject": "Đã gửi yêu cầu xếp lại lịch: {{eventType}} với {{name}} lúc {{date}}",
"requested_to_reschedule_subject_attendee": "Thao tác cần làm để xếp lại lịch: Vui lòng đặt thời gian mới cho {{eventType}} với {{name}}",
"reschedule_reason": "Lý do xếp lại lịch",
"hi_user_name": "Xin chào {{name}}",
"ics_event_title": "{{eventType}} với {{name}}",
@ -92,8 +94,6 @@
"meeting_password": "Mật khẩu cuộc họp",
"meeting_url": "URL cuộc họp",
"meeting_request_rejected": "Yêu cầu lên cuộc họp của bạn đã bị từ chối",
"rescheduled_event_type_subject": "Đã gửi yêu cầu xếp lại lịch: {{eventType}} với {{name}} lúc {{date}}",
"requested_to_reschedule_subject_attendee": "Thao tác cần làm để xếp lại lịch: Vui lòng đặt thời gian mới cho {{eventType}} với {{name}}",
"rejected_event_type_with_organizer": "Bị từ chối: {{eventType}} với {{organizer}} vào {{date}}",
"hi": "Chào",
"join_team": "Tham gia nhóm",
@ -1022,8 +1022,6 @@
"navigate": "Điều hướng",
"open": "Mở",
"close": "Đóng",
"pro_feature_teams": "Đây là tính năng Chuyên nghiệp. Nâng cấp lên Chuyên nghiệp để xem trạng thái sẵn sàng của nhóm bạn.",
"pro_feature_workflows": "Đây là tính năng Chuyên nghiệp. Nâng cấp lên Chuyên nghiệp để tự động hoá thông báo và lời nhắc sự kiện với Tiến độ công việc.",
"embed": "Được nhúng",
"new_username": "Tên người dùng mới",
"current_username": "Tên người dùng hiện tại",
@ -1037,5 +1035,7 @@
"download_desktop_app": "Tải ứng dụng trên máy tính bàn",
"set_ping_link": "Đặt liên kết Ping",
"missing_connected_calendar": "Không có lịch được kết nối",
"organizer_name_info": "Tên của bạn"
"organizer_name_info": "Tên của bạn",
"theme_light": "Sáng",
"theme_dark": "Tối"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} 取消了预约并要求您选择其他时间。",
"request_reschedule_title_organizer": "您已请求 {{attendee}} 重新安排",
"request_reschedule_subtitle_organizer": "您已取消预约,{{attendee}} 应该与您一起选择新的预约时间。",
"rescheduled_event_type_subject": "重新安排请求已发送: 和 {{name}} 在 {{date}} 的 {{eventType}}",
"requested_to_reschedule_subject_attendee": "需要重新安排操作: 请为名称为 {{name}} 的 {{eventType}} 预约新的时间",
"reschedule_reason": "重新安排的原因",
"hi_user_name": "您好 {{name}}",
"ics_event_title": "与 {{name}} 的 {{eventType}}",
@ -92,8 +94,6 @@
"meeting_password": "会议密码",
"meeting_url": "会议链接",
"meeting_request_rejected": "您的会议请求已被拒绝",
"rescheduled_event_type_subject": "重新安排请求已发送: 和 {{name}} 在 {{date}} 的 {{eventType}}",
"requested_to_reschedule_subject_attendee": "需要重新安排操作: 请为名称为 {{name}} 的 {{eventType}} 预约新的时间",
"rejected_event_type_with_organizer": "已拒绝: 和 {{organizer}} 在 {{date}} 的 {{eventType}}",
"hi": "您好",
"join_team": "加入团队",
@ -1022,8 +1022,6 @@
"navigate": "导航",
"open": "打开",
"close": "关闭",
"pro_feature_teams": "这是专业版功能。升级到专业版可查看您团队的可用性。",
"pro_feature_workflows": "这是专业版功能。升级到专业版可以自动发送工作流程的事件通知和提醒。",
"embed": "嵌入",
"new_username": "新用户名",
"current_username": "当前用户名",
@ -1037,5 +1035,7 @@
"download_desktop_app": "下载桌面应用",
"set_ping_link": "设置 Ping 链接",
"missing_connected_calendar": "未连接默认日历",
"organizer_name_info": "您的姓名"
"organizer_name_info": "您的姓名",
"theme_light": "浅色",
"theme_dark": "深色"
}

View File

@ -73,6 +73,8 @@
"request_reschedule_subtitle": "{{organizer}} 已取消預約,並要求您選擇其他時間。",
"request_reschedule_title_organizer": "您已要求 {{attendee}} 重新預約",
"request_reschedule_subtitle_organizer": "您已取消預約,{{attendee}} 應和您重新挑選新的預約時間。",
"rescheduled_event_type_subject": "已送出重新預定的申請:在 {{date}} 與 {{name}} 的 {{eventType}}",
"requested_to_reschedule_subject_attendee": "需進行重新預約操作:請為與 {{name}} 的 {{eventType}} 預訂新時間",
"reschedule_reason": "重新預約的原因",
"hi_user_name": "哈囉 {{name}}",
"ics_event_title": "與 {{name}} 的 {{eventType}}",
@ -92,8 +94,6 @@
"meeting_password": "會議密碼",
"meeting_url": "會議網址",
"meeting_request_rejected": "會議請求遭到拒絕",
"rescheduled_event_type_subject": "已送出重新預定的申請:在 {{date}} 與 {{name}} 的 {{eventType}}",
"requested_to_reschedule_subject_attendee": "需進行重新預約操作:請為與 {{name}} 的 {{eventType}} 預訂新時間",
"rejected_event_type_with_organizer": "遭到拒絕:{{date}} 與 {{organizer}} 的 {{eventType}}",
"hi": "嗨",
"join_team": "加入團隊",
@ -1022,8 +1022,6 @@
"navigate": "導覽",
"open": "開啟",
"close": "關閉",
"pro_feature_teams": "此為 Pro 功能。升級至 Pro 即可查看團隊出席情形。",
"pro_feature_workflows": "此為 Pro 功能。升級至 Pro 即可透過工作流程自動處理活動通知與提醒。",
"embed": "內嵌",
"new_username": "新使用者名稱",
"current_username": "目前使用者名稱",
@ -1037,5 +1035,7 @@
"download_desktop_app": "下載桌面版應用程式",
"set_ping_link": "設定 Ping 連結",
"missing_connected_calendar": "無已連接的預設行事曆",
"organizer_name_info": "名字"
"organizer_name_info": "名字",
"theme_light": "亮色",
"theme_dark": "暗色"
}

Some files were not shown because too many files have changed in this diff Show More