Merge branch 'util/typed-query' into feature/booking-filters

This commit is contained in:
sean-brydon 2022-12-16 11:21:44 +00:00
commit c1e680f16b
171 changed files with 4856 additions and 1723 deletions

View File

@ -87,6 +87,7 @@ TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER=
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=
# This is used so we can bypass emails in auth flows for E2E testing
# Set it to "1" if you need to run E2E tests locally

View File

@ -429,6 +429,8 @@ following
12. Leave all other fields as they are
13. Complete setup and click View my new Messaging Service
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
15. Create a verify service
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
<!-- LICENSE -->

@ -1 +1 @@
Subproject commit 8b74f463f454cf84e0f39bf78ff7d0f245014caa
Subproject commit c129586336b287d7b93c516435d905337951d2d2

View File

@ -4,6 +4,7 @@ module.exports = {
stories: [
"../intro.stories.mdx",
"../../../packages/ui/components/**/*.stories.mdx",
"../../../packages/features/**/*.stories.mdx",
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: [
@ -50,6 +51,21 @@ module.exports = {
vm: false,
zlib: false,
};
config.module.rules.push({
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true, // Enable modules to help you using className
},
},
],
include: path.resolve(__dirname, "../src"),
});
return config;
},
};

View File

@ -15,7 +15,11 @@ export const parameters = {
},
};
addDecorator((storyFn) => <I18nextProvider i18n={i18n}>{storyFn()}</I18nextProvider>);
addDecorator((storyFn) => (
<I18nextProvider i18n={i18n}>
<div style={{ margin: "2rem" }}>{storyFn()}</div>
</I18nextProvider>
));
window.getEmbedNamespace = () => {
const url = new URL(document.URL);

View File

@ -2,4 +2,4 @@
@tailwind components;
@tailwind utilities;
@import "../../../packages/ui/styles/shared-globals.css"
@import "../../../packages/ui/styles/shared-globals.css"

View File

@ -1,3 +1,5 @@
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
.sbdocs {
font-family: 'Inter var' !important;
padding: 0!important;
@ -36,7 +38,7 @@
/** Docs table **/
.custom-args-wrapper{
max-height: 600px;
max-height: 400px;
overflow-y: scroll;
overflow-x: hidden;
margin-bottom: 1rem;

View File

@ -66,7 +66,9 @@ const Component = ({
}
);
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
// Such apps, can only be installed once.
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
return (
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">

View File

@ -61,7 +61,7 @@
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",
"@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.19",
"@vercel/og": "^0.0.21",
"accept-language-parser": "^1.5.0",
"async": "^3.2.4",
"bcryptjs": "^2.4.3",

View File

@ -1,32 +1,48 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { Button, Icon, showToast } from "@calcom/ui";
export default function Error500() {
const { t } = useLocale();
const router = useRouter();
return (
<div className="flex h-screen">
<div className="flex h-screen bg-gray-100">
<Head>
<title>Something unexpected occurred | {APP_NAME}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="m-auto text-center">
<h1 className="font-cal text-[250px] text-gray-900">
5
{
// eslint-disable-next-line @next/next/no-img-element
<img src="/error.svg" className="-mt-10 inline w-60" alt="0" />
}
0
</h1>
<h2 className="mb-2 -mt-16 text-3xl text-gray-600">It&apos;s not you, it&apos;s us.</h2>
<p className="mb-4 max-w-2xl text-gray-500">
<div className="rtl: m-auto rounded-md bg-white p-10 text-right ltr:text-left">
<h1 className="font-cal text-6xl text-black">500</h1>
<h2 className="mt-6 text-2xl font-medium text-black">It&apos;s not you, it&apos;s us.</h2>
<p className="mt-4 mb-6 max-w-2xl text-sm text-gray-600">
Something went wrong on our end. Get in touch with our support team, and well get it fixed right
away for you.
</p>
{router.query.error && (
<div className="mb-8 flex flex-col">
<p className="mb-4 max-w-2xl text-sm text-gray-600">
Please provide the following text when contacting support to better help you:
</p>
<pre className="w-full max-w-2xl whitespace-normal break-words rounded-md bg-gray-200 p-4 text-gray-900">
{router.query.error}
<br />
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon={Icon.FiCopy}
onClick={() => {
navigator.clipboard.writeText(router.query.error as string);
showToast("Link copied!", "success");
}}>
{t("copy")}
</Button>
</pre>
</div>
)}
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2">
Go back

View File

@ -13,10 +13,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
title: "30min between Pro Example and pro@example.com",
description: null,
additionalNotes: "asdasdas",
customInputs: {
"Custom input 01": "sadasdasdsadasd",
"Custom input 02": "asdasdasd",
},
startTime: "2022-06-03T09:00:00-06:00",
endTime: "2022-06-03T09:30:00-06:00",
organizer: {
@ -32,26 +28,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
timeZone: "America/Chihuahua",
language,
},
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
],
location: "Zoom video",
destinationCalendar: null,
hideCalendarNotes: false,
uid: "bwPWLpjYrx4rZf6MCZdKgE",
uid: "xxyPr4cg2xx4XoS2KeMEQy",
metadata: {},
cancellationReason: "It got late",
paymentInfo: { id: "pi_12312", link: "https://cal.com", reason: "no reason" },
recurringEvent: null,
};
@ -60,10 +42,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("DisabledAppEmail", {
appName: "Stripe",
appType: ["payment"],
t,
renderEmail("OrganizerRequestEmail", {
attendee: evt.attendees[0],
calEvent: evt,
})
);
res.end();

View File

@ -53,6 +53,8 @@ export default async function handler(req: NextApiRequest) {
interFontMedium,
]);
const ogConfig = {
width: 1200,
height: 630,
fonts: [
{ name: "inter", data: interFontData, weight: 400 },
{ name: "inter", data: interFontMediumData, weight: 500 },
@ -71,7 +73,7 @@ export default async function handler(req: NextApiRequest) {
meetingImage: searchParams.get("meetingImage"),
imageType,
});
return new ImageResponse(
const img = new ImageResponse(
(
<Meeting
title={title}
@ -80,7 +82,9 @@ export default async function handler(req: NextApiRequest) {
/>
),
ogConfig
);
) as { body: Buffer };
return new Response(img.body, { status: 200 });
}
case "app": {
const { name, description, slug } = appSchema.parse({
@ -89,7 +93,11 @@ export default async function handler(req: NextApiRequest) {
slug: searchParams.get("slug"),
imageType,
});
return new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig) as {
body: Buffer;
};
return new Response(img.body, { status: 200 });
}
case "generic": {
@ -99,7 +107,11 @@ export default async function handler(req: NextApiRequest) {
imageType,
});
return new ImageResponse(<Generic title={title} description={description} />, ogConfig);
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
body: Buffer;
};
return new Response(img.body, { status: 200 });
}
default:

View File

@ -1,15 +1,16 @@
import { GetStaticPaths, GetStaticProps } from "next";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { GetServerSidePropsContext } from "next";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule";
import { availabilityAsString } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import type { Schedule as ScheduleType } from "@calcom/types/schedule";
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
import {
Button,
Form,
@ -21,6 +22,7 @@ import {
SkeletonText,
Switch,
TimezoneSelect,
Tooltip,
VerticalDivider,
} from "@calcom/ui";
@ -29,7 +31,7 @@ import { HttpError } from "@lib/core/http/error";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import EditableHeading from "@components/ui/EditableHeading";
import { ssgInit } from "@server/lib/ssg";
import { ssrInit } from "@server/lib/ssr";
const querySchema = z.object({
schedule: stringOrNumber,
@ -38,31 +40,59 @@ const querySchema = z.object({
type AvailabilityFormValues = {
name: string;
schedule: ScheduleType;
dateOverrides: { ranges: TimeRange[] }[];
timeZone: string;
isDefault: boolean;
};
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
const { remove, append, update, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
name: "dateOverrides",
});
const { t } = useLocale();
return (
<div className="px-4 py-5 sm:p-6">
<h3 className="font-medium leading-6 text-gray-900">
{t("date_overrides")}{" "}
<Tooltip content={t("date_overrides_info")}>
<span className="inline-block">
<Icon.FiInfo />
</span>
</Tooltip>
</h3>
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
<div className="mt-1 space-y-2">
<DateOverrideList
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
remove={remove}
update={update}
items={fields}
workingHours={workingHours}
/>
<DateOverrideInputDialog
workingHours={workingHours}
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
onChange={(ranges) => append({ ranges })}
Trigger={
<Button color="secondary" StartIcon={Icon.FiPlus} data-testid="add-override">
Add an override
</Button>
}
/>
</div>
</div>
);
};
export default function Availability({ schedule }: { schedule: number }) {
const { t, i18n } = useLocale();
const utils = trpc.useContext();
const me = useMeQuery();
const { timeFormat } = me.data || { timeFormat: null };
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
const form = useForm<AvailabilityFormValues>();
const { control, reset } = form;
useEffect(() => {
if (!isLoading && data) {
reset({
name: data?.schedule?.name,
schedule: data.availability,
timeZone: data.timeZone,
isDefault: data.isDefault,
});
}
}, [data, isLoading, reset]);
const { data: defaultValues } = trpc.viewer.availability.defaultValues.useQuery({ scheduleId: schedule });
const form = useForm<AvailabilityFormValues>({ defaultValues });
const { control } = form;
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
if (prevDefaultId && currentDefaultId) {
@ -73,7 +103,7 @@ export default function Availability({ schedule }: { schedule: number }) {
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
}
}
utils.viewer.availability.schedule.get.setData({ scheduleId: data.schedule.id }, data);
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
utils.viewer.availability.list.invalidate();
showToast(
t("availability_updated_successfully", {
@ -98,17 +128,21 @@ export default function Availability({ schedule }: { schedule: number }) {
<Controller
control={form.control}
name="name"
render={({ field }) => <EditableHeading isReady={!isLoading} {...field} />}
render={({ field }) => (
<EditableHeading isReady={!isLoading} {...field} data-testid="availablity-title" />
)}
/>
}
subtitle={
data ? (
data.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
<br />
</span>
))
data.schedule.availability
.filter((availability) => !!availability.days.length)
.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
<br />
</span>
))
) : (
<SkeletonText className="h-4 w-48" />
)
@ -147,15 +181,16 @@ export default function Availability({ schedule }: { schedule: number }) {
<Form
form={form}
id="availability-form"
handleSubmit={async (values) => {
handleSubmit={async ({ dateOverrides, ...values }) => {
updateMutation.mutate({
scheduleId: schedule,
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
...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">
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
<div className=" py-5 pr-4 sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
{t("change_start_end")}
</h3>
@ -171,6 +206,7 @@ export default function Availability({ schedule }: { schedule: number }) {
/>
)}
</div>
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
@ -211,24 +247,19 @@ export default function Availability({ schedule }: { schedule: number }) {
);
}
export const getStaticProps: GetStaticProps = async (ctx) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const params = querySchema.safeParse(ctx.params);
const ssg = await ssgInit(ctx);
const ssr = await ssrInit(ctx);
if (!params.success) return { notFound: true };
const scheduleId = params.data.schedule;
await ssr.viewer.availability.schedule.get.fetch({ scheduleId });
await ssr.viewer.availability.defaultValues.fetch({ scheduleId });
return {
props: {
schedule: params.data.schedule,
trpcState: ssg.dehydrate(),
schedule: scheduleId,
trpcState: ssr.dehydrate(),
},
revalidate: 10, // seconds
};
};
export const getStaticPaths: GetStaticPaths = () => {
return {
paths: [],
fallback: "blocking",
};
};

View File

@ -1,10 +1,10 @@
import { useState } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Shell, SkeletonText } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery";
type User = RouterOutputs["viewer"]["me"];
export interface IBusySlot {
@ -16,7 +16,9 @@ export interface IBusySlot {
const AvailabilityView = ({ user }: { user: User }) => {
const { t } = useLocale();
const [selectedDate, setSelectedDate] = useState(dayjs());
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
const selectedDate = dayjs(date);
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
{
@ -30,6 +32,17 @@ const AvailabilityView = ({ user }: { user: User }) => {
}
);
const overrides =
data?.dateOverrides.reduce((acc, override) => {
if (
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
)
return acc;
acc.push({ ...override, source: "Date override" });
return acc;
}, [] as IBusySlot[]) || [];
return (
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
<div className="px-4 py-5 sm:p-6">
@ -37,9 +50,9 @@ const AvailabilityView = ({ user }: { user: User }) => {
<input
type="date"
className="inline h-8 border-none p-0"
defaultValue={selectedDate.format("YYYY-MM-DD")}
defaultValue={formattedSelectedDate}
onChange={(e) => {
if (e.target.value) setSelectedDate(dayjs(e.target.value));
if (e.target.value) setSelectedDate(e.target.value);
}}
/>
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
@ -49,39 +62,47 @@ const AvailabilityView = ({ user }: { user: User }) => {
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{isLoading ? (
<>
<SkeletonText className="block h-16 w-full" />
<SkeletonText className="block h-16 w-full" />
</>
) : data && data.busy.length > 0 ? (
data.busy
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={dayjs(slot.start).format("HH:mm")}
className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
{(() => {
if (isLoading)
return (
<>
<SkeletonText className="block h-16 w-full" />
<SkeletonText className="block h-16 w-full" />
</>
);
if (data && (data.busy.length > 0 || overrides.length > 0))
return [...data.busy, ...overrides]
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={dayjs(slot.start).format("HH:mm")}
className="overflow-hidden rounded-md bg-neutral-100"
data-testid="troubleshooter-busy-time">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span
className="font-medium text-neutral-800"
title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
</div>
</div>
</div>
))
) : (
<div className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
)}
));
return (
<div className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
);
})()}
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">

View File

@ -0,0 +1,450 @@
import { BookingStatus } from "@prisma/client";
import { createHmac } from "crypto";
import { instance } from "gaxios";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import z from "zod";
import { getEventLocationValue, getSuccessPageLocationMessage } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import { getRecurringWhen } from "@calcom/emails/src/components/WhenInfo";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import { processBookingConfirmation } from "@calcom/lib/server/queries/bookings/confirm";
import prisma from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button, Icon, TextArea } from "@calcom/ui";
import { HeadSeo } from "@components/seo/head-seo";
enum DirectAction {
"accept" = "accept",
"reject" = "reject",
}
const actionSchema = z.nativeEnum(DirectAction);
const refineParse = (result: z.SafeParseReturnType<any, any>, context: z.RefinementCtx) => {
if (result.success === false) {
result.error.issues.map((issue) => context.addIssue(issue));
}
};
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
const pageErrors = {
signature_mismatch: "Direct link signature doesn't match signed data",
booking_not_found: "Direct link booking not found",
user_not_found: "Direct link booking user not found",
};
const requestSchema = z.object({
link: z
.array(z.string())
.max(4)
.superRefine((data, ctx) => {
refineParse(actionSchema.safeParse(data[0]), ctx);
const signedData = `${data[1]}/${data[2]}`;
const sig = createHmac("sha1", CALENDSO_ENCRYPTION_KEY).update(signedData).digest("base64");
if (data[3] !== sig) {
ctx.addIssue({
message: pageErrors.signature_mismatch,
code: "custom",
});
console.log(signedData, data, data[3], "==", sig);
}
}),
reason: z.string().optional(),
});
function bookingContent(status: BookingStatus | undefined | null) {
switch (status) {
case BookingStatus.PENDING:
// Trying to reject booking without reason
return {
iconColor: "gray",
Icon: Icon.FiCalendar,
titleKey: "event_awaiting_approval",
subtitleKey: "someone_requested_an_event",
};
case BookingStatus.ACCEPTED:
// Booking was acepted successfully
return {
iconColor: "green",
Icon: Icon.FiCheck,
titleKey: "booking_confirmed",
subtitleKey: "emailed_you_and_any_other_attendees",
};
case BookingStatus.REJECTED:
// Booking was rejected successfully
return {
iconColor: "red",
Icon: Icon.FiX,
titleKey: "booking_rejection_success",
subtitleKey: "emailed_you_and_any_other_attendees",
};
default:
// Booking was already accepted or rejected
return {
iconColor: "yellow",
Icon: Icon.FiAlertTriangle,
titleKey: "booking_already_accepted_rejected",
};
}
}
export default function Directlink({ booking, reason, status }: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const acceptPath = router.asPath.replace("reject", "accept");
const rejectPath = router.asPath.replace("accept", "reject");
const [cancellationReason, setCancellationReason] = useState("");
function getRecipientStart(format: string) {
return dayjs(booking.startTime).tz(booking?.user?.timeZone).format(format);
}
function getRecipientEnd(format: string) {
return dayjs(booking.endTime).tz(booking?.user?.timeZone).format(format);
}
const organizer = {
...booking.attendees[0],
language: {
translate: t,
locale: booking.attendees[0].locale ?? "en",
},
};
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(booking.location)
? booking.location[0]
: // If there is no location set then we default to Cal Video
"integrations:daily";
const locationToDisplay = getSuccessPageLocationMessage(location, t);
const content = bookingContent(status);
const recurringInfo = getRecurringWhen({
recurringEvent: booking.eventType?.recurringEvent,
attendee: organizer,
});
return (
<>
<HeadSeo
title={t(content.titleKey)}
description=""
nextSeoProps={{
nofollow: true,
noindex: true,
}}
/>
<div className="dark:bg-darkgray-50 desktop-transparent min-h-screen bg-gray-100 px-4">
<main className="mx-auto max-w-3xl">
<div className="z-50 overflow-y-auto ">
<div className="flex items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
<div
className="main dark:bg-darkgray-100 inline-block transform overflow-hidden rounded-lg border bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 sm:my-[68px] sm:w-full sm:max-w-xl sm:py-8 sm:align-middle"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full sm:mx-auto bg-${content.iconColor}-100`}>
<content.Icon className={`h-5 w-5 text-${content.iconColor}-600`} />
</div>
<div className="mt-6 mb-8 last:mb-0 sm:text-center">
<h3
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
id="modal-headline">
{t(content.titleKey)}
</h3>
{content.subtitleKey && (
<div className="mt-3">
<p className="text-neutral-600 dark:text-gray-300">{t(content.subtitleKey)}</p>
</div>
)}
<div className="dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t border-[#e1e1e1] pt-8 text-left text-[#313131] dark:text-gray-300">
<div className="col-span-3 font-medium sm:col-span-1">{t("what")}</div>
<div className="col-span-3 mb-6 last:mb-0 sm:col-span-2">{booking.title}</div>
<div className="col-span-3 font-medium sm:col-span-1">{t("when")}</div>
<div className="col-span-3 mb-6 last:mb-0 sm:col-span-2">
{recurringInfo !== "" && (
<>
{recurringInfo}
<br />
</>
)}
{booking.eventType.recurringEvent?.count ? `${t("starting")} ` : ""}
{t(getRecipientStart("dddd").toLowerCase())},{" "}
{t(getRecipientStart("MMMM").toLowerCase())} {getRecipientStart("D, YYYY")}
<br />
{getRecipientStart("h:mma")} - {getRecipientEnd("h:mma")}{" "}
<span style={{ color: "#888888" }}>({booking.attendees[0].timeZone})</span>
</div>
{(booking?.user || booking?.attendees) && (
<>
<div className="col-span-3 font-medium sm:col-span-1">{t("who")}</div>
<div className="col-span-3 last:mb-0 sm:col-span-2">
<>
{booking?.user && (
<div className="mb-3">
<p>{booking.user.name}</p>
<p className="text-[#888888]">{booking.user.email}</p>
</div>
)}
{booking?.attendees.map((attendee) => (
<div key={attendee.name} className="mb-3 last:mb-0">
{attendee.name && <p>{attendee.name}</p>}
<p className="text-[#888888]">{attendee.email}</p>
</div>
))}
</>
</div>
</>
)}
{locationToDisplay && (
<>
<div className="col-span-3 mt-6 font-medium sm:col-span-1">{t("where")}</div>
<div className="col-span-3 mt-6 sm:col-span-2">
{locationToDisplay.startsWith("http") ? (
<a title="Meeting Link" href={locationToDisplay}>
{locationToDisplay}
</a>
) : (
locationToDisplay
)}
</div>
</>
)}
{booking?.description && (
<>
<div className="col-span-3 mt-9 font-medium sm:col-span-1">
{t("additional_notes")}
</div>
<div className="col-span-3 mb-2 mt-9 sm:col-span-2">
<p>{booking.description}</p>
</div>
</>
)}
{status === BookingStatus.REJECTED && reason && (
<>
<div className="col-span-3 mt-9 font-medium sm:col-span-1">
{t("rejection_reason")}
</div>
<div className="col-span-3 mb-2 mt-9 sm:col-span-2">
<p>{reason}</p>
</div>
</>
)}
</div>
{status === BookingStatus.PENDING && reason === undefined && (
<>
<hr className="mt-6" />
<div className="mt-5 text-left sm:mt-6">
<label className="font-medium text-[#313131] dark:text-white">
{`${t("rejection_reason")} (${t("optional").toLowerCase()})`}
</label>
<TextArea
value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)}
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white "
rows={3}
/>
<div className="flex flex-col-reverse rtl:space-x-reverse">
<div className="ml-auto flex w-full justify-end space-x-4">
<Button
color="secondary"
className="hidden text-center sm:block"
href={acceptPath}>
{t("booking_accept_intent")}
</Button>
<Button
className="hidden sm:block"
onClick={async () => {
router.push(
`${rejectPath}?reason=${encodeURIComponent(cancellationReason)}`
);
}}>
{t("rejection_confirmation")}
</Button>
<Button
color="secondary"
className="block text-center sm:hidden"
href={acceptPath}>
{t("accept")}
</Button>
<Button
className="block sm:hidden"
onClick={async () => {
router.push(
`${rejectPath}?reason=${encodeURIComponent(cancellationReason)}`
);
}}>
{t("reject")}
</Button>
</div>
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const parsedQuery = requestSchema.safeParse(context.query);
// Parsing error, showing error 500 with message
if (parsedQuery.success === false) {
return {
redirect: {
destination: `/500?error=${parsedQuery.error.errors[0].message.concat(
" accessing " + context.resolvedUrl
)}`,
permanent: false,
},
};
}
const {
link: [action, email, bookingUid],
reason,
} = parsedQuery.data;
const isAccept = action === DirectAction.accept;
const bookingRaw = await prisma?.booking.findFirst({
where: {
uid: bookingUid,
user: {
email,
},
},
select: {
location: true,
description: true,
id: true,
recurringEventId: true,
status: true,
title: true,
startTime: true,
endTime: true,
eventType: {
select: {
recurringEvent: true,
},
},
attendees: {
select: {
locale: true,
name: true,
email: true,
timeZone: true,
},
},
user: {
select: {
id: true,
email: true,
name: true,
timeZone: true,
locale: true,
destinationCalendar: true,
credentials: true,
username: true,
},
},
},
});
// Booking not found, showing error 500 with message
if (!bookingRaw) {
return {
redirect: {
destination: `/500?error=${pageErrors.booking_not_found.concat(" accessing " + context.resolvedUrl)}`,
permanent: false,
},
};
}
const booking = {
...bookingRaw,
startTime: bookingRaw.startTime.toString(),
endTime: bookingRaw.endTime.toString(),
eventType: {
...bookingRaw.eventType,
recurringEvent: parseRecurringEvent(bookingRaw?.eventType?.recurringEvent),
},
attendees: bookingRaw?.attendees.map((att) => ({
...att,
language: {
locale: att.locale ?? "en",
},
})),
};
// Booking user not found, showing error 500 with message
if (booking.user === null) {
return {
redirect: {
destination: `/500?error=${pageErrors.user_not_found.concat(" accessing " + context.resolvedUrl)}`,
permanent: false,
},
};
}
// Booking already accepted or rejected
if (booking.status !== BookingStatus.PENDING) {
return {
props: {
booking,
status: null,
},
};
}
// Trying to reject booking without reason
if (!isAccept && reason === undefined) {
return {
props: {
booking,
status: BookingStatus.PENDING,
},
};
}
// Booking good to be accepted or rejected, proceeding to mark it
let result: { status: BookingStatus | undefined } = { status: undefined };
try {
result = await processBookingConfirmation(
{
bookingId: booking.id,
user: booking.user,
recurringEventId: booking.recurringEventId,
confirmed: action === DirectAction.accept,
rejectionReason: reason,
},
prisma
);
} catch (e) {
if (e instanceof TRPCError) {
return {
redirect: {
destination: `/500?error=${e.message.concat(" accessing " + context.resolvedUrl)}`,
permanent: false,
},
};
}
}
return {
props: {
booking,
status: result.status,
reason: context.query.reason ?? null,
},
};
}

View File

@ -7,6 +7,7 @@ import React, { Fragment, useEffect, useState } from "react";
import CreateEventTypeButton from "@calcom/features/eventtypes/components/CreateEventTypeButton";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import isCalcom from "@calcom/lib/isCalcom";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Badge,
@ -28,6 +29,7 @@ import {
showToast,
Switch,
Tooltip,
TipBanner,
} from "@calcom/ui";
import { withQuery } from "@lib/QueryCell";
@ -587,6 +589,7 @@ const EventTypesPage = () => {
customLoader={<SkeletonLoader />}
success={({ data }) => (
<>
{isCalcom && <TipBanner />}
{data.eventTypeGroups.map((group, index) => (
<Fragment key={group.profile.slug}>
{/* hide list heading when there is only one (current user) */}
@ -605,7 +608,6 @@ const EventTypesPage = () => {
/>
</Fragment>
))}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog />
</>

View File

@ -10,7 +10,7 @@ function AdminAppsView() {
const { t } = useLocale();
return (
<>
<Meta title={t("apps")} description={t("apps_description")} />
<Meta title={t("apps")} description={t("admin_apps_description")} />
<AdminAppsList baseURL="/settings/admin/apps" className="w-0" />
</>
);

View File

@ -0,0 +1,54 @@
import { expect } from "@playwright/test";
import dayjs from "@calcom/dayjs";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.describe("Availablity tests", () => {
test.beforeEach(async ({ page, users }) => {
const user = await users.create();
await user.login();
await page.goto("/availability");
// We wait until loading is finished
await page.waitForSelector('[data-testid="schedules"]');
});
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
test("Date Overrides", async ({ page }) => {
await test.step("Can add a date override", async () => {
await page.locator('[data-testid="schedules"] > li a').click();
await page.locator('[data-testid="add-override"]').click();
await page.locator('[id="modal-title"]').waitFor();
await page.locator('[data-testid="incrementMonth"]').click();
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
await page.locator('[data-testid="date-override-mark-unavailable"]').click();
await page.locator('[data-testid="add-override-submit-btn"]').click();
await expect(page.locator('[data-testid="date-overrides-list"] > li')).toHaveCount(1);
await page.locator('[form="availability-form"][type="submit"]').click();
});
await test.step("Date override is displayed in troubleshooter", async () => {
const response = await page.waitForResponse("**/api/trpc/viewer.availability.schedule.update?batch=1");
const json = await response.json();
// @ts-expect-error trust me bro
const date = json[0].result.data.json.schedule.availability.find((a) => !!a.date);
const troubleshooterURL = `/availability/troubleshoot?date=${dayjs(date.date).format("YYYY-MM-DD")}`;
await page.goto(troubleshooterURL);
await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1);
});
});
test("Availablity pages", async ({ page }) => {
await test.step("Can add a new schedule", async () => {
await page.locator('[data-testid="new-schedule"]').click();
await page.locator('[id="name"]').fill("More working hours");
page.locator('[type="submit"]').click();
await expect(page.locator("[data-testid=availablity-title]")).toHaveValue("More working hours");
});
});
});

View File

@ -67,6 +67,7 @@ test("add webhook & test that creating an event triggers a webhook call", async
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
// if we change the shape of our webhooks, we can simply update this by clicking `u`
// console.log("BODY", body);

View File

@ -1 +1 @@
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between Nameless and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Nameless","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between Nameless and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Nameless","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{"videoCallUrl":"[redacted/dynamic]"},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}

View File

@ -0,0 +1,5 @@
<svg stroke="#888888" fill="none" stroke-width="2.5" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -52,6 +52,7 @@
"still_waiting_for_approval": "يوجد حدث لا يزال في انتظار الموافقة",
"event_is_still_waiting": "لا يزال طلب الحدث في الانتظار: {{attendeeName}} - {{date}} - {{eventType}}",
"no_more_results": "لا يوجد مزيد من النتائج",
"no_results": "لا نتائج",
"load_more_results": "تحميل المزيد من النتائج",
"integration_meeting_id": "مُعرّف اجتماع {{integrationName}}: {{meetingId}}",
"confirmed_event_type_subject": "تم التأكيد: {{eventType}} مع {{name}} في {{date}}",
@ -248,6 +249,7 @@
"add_to_calendar": "إضافة إلى التقويم",
"add_another_calendar": "إضافة تقويم آخر",
"other": "آخر",
"email_sign_in_subject": "رابط تسجيل الدخول بك لـ{{appName}}",
"emailed_you_and_attendees": "لقد أرسلنا إليك وإلى الحضور الآخرين دعوة للتقويم عبر البريد الإلكتروني تتضمن كل التفاصيل.",
"emailed_you_and_attendees_recurring": "لقد أرسلنا إليك وإلى الحضور الآخرين دعوة تقويم عبر البريد الإلكتروني لأول هذه الأحداث المتكررة.",
"emailed_you_and_any_other_attendees": "تم إرسال هذه المعلومات إليك وإلى الحضور الآخرين عبر البريد الإلكتروني.",
@ -383,7 +385,6 @@
"go_to_billing_portal": "انتقل إلى بوابة الفوترة",
"need_anything_else": "هل تحتاج إلى أي شيء آخر؟",
"further_billing_help": "إذا كنت تحتاج إلى أي مساعدة إضافية بخصوص الفوترة، فإن فريق الدعم لدينا في انتظارك لتقديم المساعدة.",
"contact_our_support_team": "الاتصال بفريق الدعم لدينا",
"uh_oh": "عذرًا!",
"no_event_types_have_been_setup": "لم يقم هذا المستخدم بإعداد أي أنواع للحدث حتى الآن.",
"edit_logo": "تعديل الشعار",
@ -420,6 +421,7 @@
"current_incorrect_password": "كلمة المرور الحالية غير صحيحة",
"password_hint_caplow": "مزيج من الحروف الكبيرة والحروف الصغيرة",
"password_hint_min": "8 أحرف كحد أدنى",
"password_hint_admin_min": "15 محرفًا كحد أدنى",
"password_hint_num": "يحتوي على رقم واحد على الأقل",
"invalid_password_hint": "يجب أن تتألف كلمة المرور على الأقل من 7 أحرف، ورقم واحد على الأقل، ومزيج من الحروف الكبيرة والصغيرة",
"incorrect_password": "كلمة المرور غير صحيحة.",
@ -463,11 +465,14 @@
"booking_confirmation": "قم بتأكيد {{eventTypeTitle}} مع {{profileName}}",
"booking_reschedule_confirmation": "أعد جدولة {{eventTypeTitle}} مع {{profileName}}",
"in_person_meeting": "الرابط أو الاجتماع الشخصي",
"attendeeInPerson": "شخصيًا (عنوان من سيحضر)",
"inPerson": "شخصيًا (عنوان المنظم)",
"link_meeting": "رابط الاجتماع",
"phone_call": "رقم هاتف الحضور",
"your_number": "رقم هاتفك",
"phone_number": "رقم الهاتف",
"attendee_phone_number": "رقم هاتف الحضور",
"organizer_phone_number": "رقم هاتف المنظم",
"host_phone_number": "رقم هاتفك",
"enter_phone_number": "أدخل رقم الهاتف",
"reschedule": "إعادة الجدولة",
@ -554,6 +559,10 @@
"collective": "جماعي",
"collective_description": "جدولة الاجتماعات عندما يكون جميع أعضاء الفريق المحددين متاحين.",
"duration": "المدة",
"available_durations": "المدد المتاحة",
"default_duration": "المدة الافتراضية",
"default_duration_no_options": "يرجى اختيار المدد المتاحة أولاً",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "الدقائق",
"round_robin": "الترتيب الدوري",
"round_robin_description": "نقل الاجتماعات بشكل دوري بين أعضاء الفريق المتعددين.",
@ -619,6 +628,7 @@
"teams": "الفرق",
"team": "فريق",
"team_billing": "الفوترة الخاصة بالفريق",
"team_billing_description": "إدارة الفواتير لفريقك",
"upgrade_to_flexible_pro_title": "لقد قمنا بتغيير الفوترة الخاصة بالفرق",
"upgrade_to_flexible_pro_message": "يوجد أعضاء في فريقك من دون مقاعد. قم بترقية خطة Pro خاصتك لتوفير المقاعد الناقصة.",
"changed_team_billing_info": "بدءًا من يناير 2022، أصبحنا نفرض الرسوم على مقاعد أعضاء الفريق بشكل منفرد. حاليًا، أعضاء فريقك الذين كانت لديهم خطة Pro مجانية أصبحوا في فترة تجريبية مدتها 14 يومًا. وبمجرد انتهاء الفترة التجريبية الخاصة بهم، سيجري إخفاء هؤلاء الأعضاء من فريقك ما لم تقم بالترقية الآن.",
@ -694,6 +704,7 @@
"hide_event_type": "إخفاء نوع الحدث",
"edit_location": "تعديل الموقع",
"into_the_future": "في المستقبل",
"when_booked_with_less_than_notice": "عند الحجز بأقل من إشعار <time></time>",
"within_date_range": "خلال فترة زمنية",
"indefinitely_into_future": "إلى أجل غير مسمى في المستقبل",
"add_new_custom_input_field": "إضافة خانة إدخال مخصص جديدة",
@ -713,6 +724,8 @@
"delete_account_confirmation_message": "هل تريد بالتأكيد حذف حسابك على {{appName}}؟ أي شخص شاركت رابط حسابك معه لن يستطيع بعد الآن الحجز باستخدامه، وستفقد أي تفضيلات حفظتها.",
"integrations": "التكاملات",
"apps": "التطبيقات",
"apps_description": "تمكين تطبيقات مثيل Cal الخاص بك",
"apps_listing": "قائمة التطبيقات",
"category_apps": "تطبيقات {{category}}",
"app_store": "متجر التطبيقات",
"app_store_description": "الربط بين الناس، والتكنولوجيا، ومكان العمل.",
@ -736,6 +749,7 @@
"toggle_calendars_conflict": "قم بتبديل التقويمات التي تريد التحقق من وجود تضاربات فيها لمنع حدوث حجز مزدوج.",
"select_destination_calendar": "إنشاء أحداث في",
"connect_additional_calendar": "ربط رزنامة إضافية",
"calendar_updated_successfully": "تم تحديث الرزنامة بنجاح",
"conferencing": "المؤتمرات عبر الفيديو",
"calendar": "التقويم",
"payments": "المدفوعات",
@ -768,6 +782,7 @@
"trending_apps": "التطبيقات الرائجة",
"explore_apps": "تطبيقات {{category}}",
"installed_apps": "التطبيقات المثبتة",
"free_to_use_apps": "متاح",
"no_category_apps": "لا توجد تطبيقات {{category}}",
"no_category_apps_description_calendar": "إضافة تطبيق تقويم للتحقق من أي تضارب لمنع أي حجوزات مزدوجة",
"no_category_apps_description_conferencing": "جرب إضافة تطبيق مؤتمرات لإنشاء مكالمات فيديو مع عملائك",
@ -806,6 +821,8 @@
"verify_wallet": "تأكيد المحفظة",
"connect_metamask": "توصيل Metamask",
"create_events_on": "إنشاء أحداث في",
"enterprise_license": "هذه هي ميزة للمؤسسات",
"enterprise_license_description": "لتمكين هذه الميزة، احصل على مفتاح نشر في وحدة التحكم {{consoleUrl}} وأضفه إلى وحدة التحكم الخاصة بك. nv باسم CALCOM_LICENSE_KEY. إذا كان لدى فريقك بالفعل ترخيص، يرجى الاتصال بـ {{supportMail}} للحصول على المساعدة.",
"missing_license": "الترخيص مفقود",
"signup_requires": "يلزم ترخيص تجاري",
"signup_requires_description": "لا تقدم شركة {{companyName}} حاليًا إصدارًا مجانيًا مفتوح المصدر لصفحة التسجيل. للحصول على حق الوصول الكامل إلى مكونات التسجيل، يجب أن تحصل على ترخيص تجاري. للاستخدام الشخصي، نوصي باستخدام منصة Prisma Data أو أي واجهة أخرى من واجهات Postgres لإنشاء الحسابات.",
@ -897,6 +914,7 @@
"user_impersonation_heading": "انتحال شخصية مستخدم",
"user_impersonation_description": "يسمح لفريق الدعم الخاص بنا بتسجيل الدخول مؤقتًا نيابة عنك لمساعدتنا على حل أي مشاكل تبلغ عنها بسرعة.",
"team_impersonation_description": "يسمح لأعضاء فريقك بتسجيل الدخول نيابة عنك مؤقتًا.",
"allow_booker_to_select_duration": "السماح لمن يقوم بالحجز بتحديد المدة",
"impersonate_user_tip": "يتم مراجعة جميع استخدامات هذه الميزة.",
"impersonating_user_warning": "انتحال اسم مستخدم \"{{user}}\".",
"impersonating_stop_instructions": "<0>انقر هنا لإيقاف </0>.",
@ -1014,6 +1032,9 @@
"error_removing_app": "خطأ في إزالة التطبيق",
"web_conference": "مؤتمر عبر الإنترنت",
"requires_confirmation": "يتطلب التأكيد",
"always_requires_confirmation": "دائمًا",
"requires_confirmation_threshold": "يتطلب تأكيد إذا تم حجزه مع إشعار $t({{unit}}_timeUnit){{time}}",
"may_require_confirmation": "قد يتطلب التأكيد",
"nr_event_type_one": "{{count}} نوع حدث",
"nr_event_type_other": "{{count}} من أنواع الحدث",
"add_action": "إضافة إجراء",
@ -1101,6 +1122,9 @@
"event_limit_tab_description": "ما تواتر مرات الحجز",
"event_advanced_tab_description": "إعدادات الرزنامة والمزيد...",
"event_advanced_tab_title": "متقدم",
"event_setup_multiple_duration_error": "إعداد الحدث: تستلزم فترات متعددة خيارًا واحدًا على الأقل.",
"event_setup_multiple_duration_default_error": "إعداد الحدث: الرجاء تحديد مدة افتراضية صالحة.",
"event_setup_booking_limits_error": "يجب أن تكون حدود الحجز بترتيب تصاعدي. [يوم،أسبوع،شهر،سنة]",
"select_which_cal": "اختر أي رزنامة تريد إضافة الحجز إليها",
"custom_event_name": "اسم حدث مخصص",
"custom_event_name_description": "إنشاء أسماء أحداث مخصصة لعرضها في حدث الرزنامة",
@ -1154,8 +1178,11 @@
"invoices": "الفواتير",
"embeds": "التضمينات",
"impersonation": "انتحال الشخصية",
"impersonation_description": "إعدادات إدارة انتحال شخصية مستخدم",
"users": "المستخدمون",
"profile_description": "إدارة الإعدادات لملفك الشخصي على {{appName}}",
"users_description": "يمكنك هنا العثور على قائمة بجميع المستخدمين",
"users_listing": "قائمة المستخدمين",
"general_description": "إدارة الإعدادات للغة والمنطقة الزمنية الخاصة بك",
"calendars_description": "قم بتهيئة كيفية تفاعل أنواع الأحداث مع التقويمات الخاصة بك",
"appearance_description": "إدارة إعدادات مظهر الحجز الخاص بك",
@ -1360,6 +1387,7 @@
"number_sms_notifications": "رقم الهاتف (إشعارات الرسائل النصية)",
"attendee_email_workflow": "اسم الحاضر",
"attendee_email_info": "البريد الإلكتروني للشخص الحجز",
"kbar_search_placeholder": "اكتب أمرًا أو بحثًا...",
"invalid_credential": "لا! يبدو أن الصلاحية انتهت أو تم إلغاؤها. يرجى إعادة التثبيت مرة أخرى.",
"choose_common_schedule_team_event": "اختر جدولاً زمنياً مشتركاً",
"choose_common_schedule_team_event_description": "قم بتمكين هذا الخيار إذا كنت ترغب في استخدام جدول زمني مشترك بين المضيفين. عند تعطيل هذا الخيار، سيتم حجز كل مضيف على أساس جدوله الافتراضي.",
@ -1370,5 +1398,44 @@
"test_preview": "اختبار المعاينة",
"route_to": "التوجيه إلى",
"test_preview_description": "اختبار نموذج التوجيه الخاص بك دون تقديم أي بيانات",
"test_routing": "اختبار التوجيه"
"test_routing": "اختبار التوجيه",
"payment_app_disabled": "قام المشرف بتعطيل تطبيق دفع",
"edit_event_type": "تعديل نوع الحدث",
"collective_scheduling": "جدولة جماعية",
"make_it_easy_to_book": "سهّل على فريقك الحجز عندما يكون الجميع متاحين.",
"find_the_best_person": "ابحث عن أفضل شخص متوفر، وقم بالتدوير بين أعضاء فريقك.",
"fixed_round_robin": "روبين دائري ثابت",
"add_one_fixed_attendee": "أضف حاضرًا ثابتًا وروبين مستدير من خلال عدد من الحاضرين.",
"calcom_is_better_with_team": "Cal.com أفضل مع الفرق",
"add_your_team_members": "أضف أعضاء فريقك إلى أنواع الأحداث الخاصة بك. استخدم الجدولة الجماعية لإدراج الجميع أو العثور على أنسب شخص مع جدولة روبن الدائرية.",
"booking_limit_reached": "تم الوصول إلى الحد الأقصى للحجز لهذا النوع من الأحداث",
"admin_has_disabled": "قام المشرف بتعطيل {{appName}}",
"disabled_app_affects_event_type": "قام المشرف بتعطيل {{appName}} مما يؤثر على نوع الحدث {{eventType}} لديك",
"disable_payment_app": "قام المشرف بتعطيل {{appName}} الذي يؤثر على نوع الحدث {{title}} لديك. ولا يزال بإمكان الحاضرين حجز هذا النوع من الأحداث، ولكن لن يُطالبوا بالدفع. يمكنك إخفاء نوع الحدث لمنع حدوث هذا إلى أن يقوم المشرف بتغيير طريقة الدفع لك.",
"payment_disabled_still_able_to_book": "لا يزال بإمكان الحاضرين حجز هذا النوع من الأحداث، ولكن لن يُطالبوا بالدفع. يمكنك إخفاء نوع الحدث لمنع حدوث هذا إلى أن يقوم المشرف بتغيير طريقة الدفع لك.",
"app_disabled_with_event_type": "عطّل المشرف {{appName}} مما يؤثر على نوع الحدث {{title}} لديك.",
"app_disabled_video": "عطّل المشرف {{appName}} الذي قد يؤثر على أنواع الأحداث لديك. إذا كان لديك أنواع من الأحداث مع {{appName}} كموقع، سيكون افتراضيًا من أجل فيديو Cal.",
"app_disabled_subject": "تم تعطيل {{appName}}",
"navigate_installed_apps": "الذهاب إلى التطبيقات المثبتة",
"disabled_calendar": "إذا كان لديك تقويم آخر مثبت، ستُضاف الحجوزات الجديدة إليه. إذا لم يكن الأمر كذلك، قم إذن بتوصيل تقويم جديد لكيلا تفوت أي حجوزات جديدة.",
"enable_apps": "تمكين التطبيقات",
"enable_apps_description": "تمكين التطبيقات التي يمكن للمستخدمين دمجها مع Cal.com",
"app_is_enabled": "تم تمكين {{appName}}",
"app_is_disabled": "تم تعطيل {{appName}}",
"keys_have_been_saved": "تم حفظ المفاتيح",
"disable_app": "تعطيل التطبيق",
"disable_app_description": "يمكن لتعطيل هذا التطبيق أن يسبب مشاكل مع كيفية تفاعل المستخدمين مع Cal",
"edit_keys": "تحرير المفاتيح",
"admin_apps_description": "تمكين تطبيقات مثيل Cal الخاص بك",
"no_available_apps": "لا توجد تطبيقات متاحة",
"no_available_apps_description": "الرجاء التأكد من وجود تطبيقات في نشرك تحت 'حزم/متجر تطبيقات'",
"no_apps": "لا توجد تطبيقات مفعلة في هذا المظهر من Cal",
"apps_settings": "إعدادات التطبيقات",
"fill_this_field": "يرجى ملء هذا الحقل",
"options": "خيارات",
"enter_option": "أدخل خيار {{index}}",
"add_an_option": "إضافة خيار",
"radio": "الراديو",
"event_type_duplicate_copy_text": "{{slug}}-نسخ",
"set_as_default": "تعيين كافتراضي"
}

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "Přejít na fakturační portál",
"need_anything_else": "Potřebujete ještě něco?",
"further_billing_help": "Pokud potřebujete další pomoc s fakturací, náš tým podpory je zde, aby vám pomohl.",
"contact_our_support_team": "Kontaktovat tým podpory",
"uh_oh": "Ale ne!",
"no_event_types_have_been_setup": "Tento uživatel zatím nezaložil žádné typy událostí.",
"edit_logo": "Upravit logo",

View File

@ -370,7 +370,6 @@
"go_to_billing_portal": "Zum Rechnungsportal gehen",
"need_anything_else": "Brauchen Sie etwas anderes?",
"further_billing_help": "Wenn Sie weitere Hilfe bei der Rechnungsstellung benötigen, hilft Ihnen unser Support-Team gerne weiter.",
"contact_our_support_team": "Kontaktieren Sie unser Support-Team",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "Dieser Benutzer hat noch keine Termintypen eingerichtet.",
"edit_logo": "Logo bearbeiten",
@ -1338,5 +1337,6 @@
"test_preview": "Vorschau testen",
"route_to": "Weiterleiten zu",
"test_preview_description": "Testen Sie Ihr Weiterleitungsformular, ohne Daten zu senden",
"test_routing": "Testweiterleitung"
"test_routing": "Testweiterleitung",
"admin_apps_description": "Hier findest du eine Auflistung deiner Apps"
}

View File

@ -0,0 +1,111 @@
{
"day_one": "{{count}} ημέρα",
"day_other": "{{count}} ημέρες",
"second_one": "{{count}} δευτερόλεπτο",
"second_other": "{{count}} δευτερόλεπτα",
"upgrade_now": "Αναβαθμίστε τώρα",
"accept_invitation": "Αποδοχή Πρόσκλησης",
"have_any_questions": "Έχετε ερωτήσεις? Είμαστε εδώ για να βοηθήσουμε.",
"reset_password_subject": "{{appName}}: Οδηγίες επαναφοράς κωδικού πρόσβασης",
"event_declined_subject": "Απορρίφθηκε: {{eventType}} με {{name}} στις {{date}}",
"event_cancelled_subject": "Ακύρωση: {{eventType}} με {{name}} στις {{date}}",
"need_to_reschedule_or_cancel": "Χρειάζεται να επαναπρογραμματίσετε ή να ακυρώσετε;",
"cancellation_reason": "Λόγος ακύρωσης (προαιρετικό)",
"rejection_reason": "Λόγος απόρριψης",
"rejection_reason_title": "Απόρριψη του αιτήματος κράτησης;",
"rejection_confirmation": "Απόρριψη κράτησης",
"error_message": "Το μήνυμα σφάλματος ήταν: '{{errorMessage}}'",
"refund_failed_subject": "Η επιστροφή χρημάτων απέτυχε: {{name}} - {{date}} - {{eventType}}",
"refund_failed": "Η επιστροφή χρημάτων για την εκδήλωση {{eventType}} με {{userName}} στις {{date}} απέτυχε.",
"a_refund_failed": "Αποτυχία επιστροφής χρημάτων",
"awaiting_payment_subject": "Σε αναμονή Πληρωμής: {{eventType}} με {{name}} στις {{date}}",
"meeting_awaiting_payment": "Η πληρωμή της συνάντησής σας εκκρεμεί",
"help": "Βοήθεια",
"price": "Τιμή",
"payment": "Πληρωμή",
"missing_card_fields": "Λείπουν τα πεδία της κάρτας",
"pay_now": "Πληρωμή τώρα",
"terms_summary": "Περίληψη όρων",
"open_env": "Ανοίξτε το .env και συμφωνήστε με την Άδεια χρήσης μας",
"env_changed": "Έχω αλλάξει το .env μου",
"accept_license": "Αποδοχή Άδειας",
"no_more_results": "Δεν υπάρχουν άλλα αποτελέσματα",
"no_results": "Δεν υπάρχουν αποτελέσματα",
"load_more_results": "Φόρτωση περισσότερων αποτελεσμάτων",
"integration_meeting_id": "ID συνάντησης {{integrationName}} : {{meetingId}}",
"confirmed_event_type_subject": "Επιβεβαίωση: {{eventType}} με {{name}} στις {{date}}",
"new_event_request": "Νέο αίτημα εκδήλωσης: {{attendeeName}} - {{date}} - {{eventType}}",
"confirm_or_reject_request": "Επιβεβαιώστε ή απορρίψτε το αίτημα",
"check_bookings_page_to_confirm_or_reject": "Ελέγξτε τη σελίδα κρατήσεων για να επιβεβαιώσετε ή να απορρίψετε την κράτηση.",
"event_awaiting_approval": "Ένα γεγονός περιμένει την έγκρισή σας",
"event_type": "Τύπος Συμβάντος",
"meeting_password": "Συνθηματικό Συνάντησης",
"meeting_url": "URL Συνάντησης",
"meeting_request_rejected": "Το αίτημα συνάντησής σας απορρίφθηκε",
"hi": "Γεια",
"manage_this_team": "Διαχείριση αυτής της ομάδας",
"team_info": "Πληροφορίες Ομάδας",
"hidden_team_member_title": "Είστε κρυμμένοι σε αυτήν την ομάδα",
"edit_webhook": "Επεξεργασία Webhook",
"delete_webhook": "Διαγραφή Webhook",
"webhook_enabled": "Ενεργοποιημένο Webhook",
"webhook_disabled": "Απενεργοποιημένο Webhook",
"webhook_response": "Απάντηση Webhook",
"webhook_test": "Έλεγχος Webhook",
"webhook_created_successfully": "Το Webhook δημιουργήθηκε επιτυχώς!",
"webhook_updated_successfully": "Το Webhook ενημερώθηκε επιτυχώς!",
"webhook_removed_successfully": "Το Webhook αφαιρέθηκε επιτυχώς!",
"dismiss": "Παράβλεψη",
"no_data_yet": "Δεν υπάρχουν δεδομένα ακόμη",
"upcoming": "Επερχόμενα",
"recurring": "Επαναλαμβανόμενα",
"past": "Παρελθοντικά",
"choose_a_file": "Επιλογή αρχείου...",
"upload_image": "Μεταφόρτωση εικόνας",
"username": "Όνομα χρήστη",
"is_still_available": "είναι ακόμα διαθέσιμο.",
"documentation": "Τεκμηρίωση",
"blog": "Ιστολόγιο",
"blog_description": "Διαβάστε τα τελευταία μας νέα και άρθρα",
"popular_pages": "Δημοφιλείς σελίδες",
"register_now": "Εγγραφή τώρα",
"register": "Εγγραφή",
"page_doesnt_exist": "Η σελίδα δεν υπάρχει.",
"check_spelling_mistakes_or_go_back": "Έλεγχος για ορθογραφικά λάθη ή επιστροφή στην προηγούμενη σελίδα.",
"404_page_not_found": "404: Η σελίδα δεν βρέθηκε.",
"getting_started": "Ξεκινήστε",
"already_have_an_account": "Έχετε ήδη λογαριασμό;",
"create_account": "Δημιουργία Λογαριασμού",
"confirm_password": "Επιβεβαίωση κωδικού πρόσβασης",
"create_your_account": "Δημιουργία λογαριασμού",
"sign_up": "Εγγραφή",
"youve_been_logged_out": "Έχετε αποσυνδεθεί",
"hope_to_see_you_soon": "Ελπίζουμε να σας ξαναδούμε σύντομα!",
"please_try_again_and_contact_us": "Παρακαλούμε δοκιμάστε ξανά και επικοινωνήστε μαζί μας αν το πρόβλημα παραμένει.",
"no_account_exists": "Δεν υπάρχει λογαριασμός που να ταιριάζει με τη διεύθυνση email.",
"2fa_enter_six_digit_code": "Εισάγετε τον εξαψήφιο κωδικό από την εφαρμογή ελέγχου ταυτότητας παρακάτω.",
"create_an_account": "Δημιουργία λογαριασμού",
"dont_have_an_account": "Δεν έχετε λογαριασμό;",
"sign_in_account": "Συνδεθείτε στο λογαριασμό σας",
"sign_in": "Είσοδος",
"connect": "Σύνδεση",
"try_for_free": "Δοκιμάστε δωρεάν",
"add_to_calendar": "Προσθήκη στο ημερολόγιο",
"add_another_calendar": "Προσθήκη άλλου ημερολογίου",
"meeting_is_scheduled": "Η συνάντηση έχει προγραμματιστεί",
"submitted": "Η κράτησή σας έχει υποβληθεί",
"reset_password": "Επαναφορά Κωδικού Πρόσβασης",
"change_your_password": "Αλλαγή κωδικού πρόσβασης",
"show_password": "Εμφάνιση κωδικού πρόσβασης",
"hide_password": "Απόκρυψη κωδικού πρόσβασης",
"try_again": "Δοκιμάστε ξανά",
"sunday_time_error": "Μη έγκυρη ώρα την Κυριακή",
"monday_time_error": "Μη έγκυρη ώρα τη Δευτέρα",
"tuesday_time_error": "Μη έγκυρη ώρα την Τρίτη",
"wednesday_time_error": "Μη έγκυρη ώρα την Τετάρτη",
"thursday_time_error": "Μη έγκυρη ώρα την Πέμπτη",
"friday_time_error": "Μη έγκυρη ώρα την Παρασκευή",
"saturday_time_error": "Μη έγκυρη ώρα το Σάββατο",
"no_meeting_found": "Δε βρέθηκε συνάντηση",
"bookings": "Κρατήσεις"
}

View File

@ -6,7 +6,7 @@
"second_other": "{{count}} seconds",
"upgrade_now": "Upgrade now",
"accept_invitation": "Accept Invitation",
"calcom_explained": "{{appName}} is the open source Calendly alternative putting you in control of your own data, workflow and appearance.",
"calcom_explained": "{{appName}} provides scheduling infrastructure for absolutely everyone.",
"have_any_questions": "Have questions? We're here to help.",
"reset_password_subject": "{{appName}}: Reset password instructions",
"event_declined_subject": "Declined: {{eventType}} with {{name}} at {{date}}",
@ -303,6 +303,7 @@
"cancelling_all_recurring": "These are all remaining instances in the recurring event.",
"error_with_status_code_occured": "An error with status code {{status}} occurred.",
"booking_already_cancelled": "This booking was already cancelled",
"booking_already_accepted_rejected": "This booking was already accepted or rejected",
"go_back_home": "Go back home",
"or_go_back_home": "Or go back home",
"no_availability": "Unavailable",
@ -385,7 +386,8 @@
"go_to_billing_portal": "Go to the billing portal",
"need_anything_else": "Need anything else?",
"further_billing_help": "If you need any further help with billing, our support team are here to help.",
"contact_our_support_team": "Contact our support team",
"contact": "Contact",
"our_support_team": "our support team",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "This user hasn't set up any event types yet.",
"edit_logo": "Edit logo",
@ -918,7 +920,7 @@
"allow_booker_to_select_duration": "Allow booker to select duration",
"impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "<0>Click Here to stop</0>.",
"impersonating_stop_instructions": "Click here to stop",
"event_location_changed": "Updated - Your event changed the location",
"location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}",
"current_location": "Current Location",
@ -1326,6 +1328,8 @@
"booking_confirmation_success": "Booking confirmation succeeded",
"booking_rejection_success": "Booking rejection succeeded",
"booking_confirmation_fail": "Booking confirmation failed",
"booking_tentative": "This booking is tentative",
"booking_accept_intent": "Oops, I want to accept",
"we_wont_show_again": "We won't show this again",
"couldnt_update_timezone": "We couldn't update the timezone",
"updated_timezone_to": "Updated timezone to {{formattedCurrentTz}}",
@ -1427,7 +1431,7 @@
"disable_app": "Disable App",
"disable_app_description": "Disabling this app could cause problems with how your users interact with Cal",
"edit_keys": "Edit Keys",
"apps_description": "Enable apps for your instance of Cal",
"admin_apps_description": "Enable apps for your instance of Cal",
"no_available_apps": "There are no available apps",
"no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'",
"no_apps": "There are no apps enabled in this instance of Cal",
@ -1438,8 +1442,27 @@
"add_an_option": "Add an option",
"radio": "Radio",
"individual":"Individual",
"all_bookings_filter_label":"All Bookings",
"date_overrides": "Date overrides",
"date_overrides_subtitle": "Add dates when your availability changes from your daily hours.",
"date_overrides_info": "Date overrides are archived automatically after the date has passed",
"date_overrides_dialog_which_hours": "Which hours are you free?",
"date_overrides_dialog_which_hours_unavailable": "Which hours are you busy?",
"date_overrides_dialog_title": "Select the dates to override",
"date_overrides_unavailable": "Unavailable all day",
"date_overrides_mark_all_day_unavailable_one": "Mark unavailable (All day)",
"date_overrides_mark_all_day_unavailable_other": "Mark unavailable on selected dates",
"date_overrides_add_btn": "Add Override",
"date_overrides_update_btn": "Update Override",
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default",
"hide_eventtype_details": "Hide EventType Details",
"all_bookings_filter_label":"All Bookings",
"verification_code_sent": "Verification code sent",
"verified_successfully": "Verified successfully",
"wrong_code": "Wong verification code",
"not_verified": "Not yet verified",
"no_availability_in_month": "No availability in {{month}}",
"view_next_month": "View next month",
"send_code" : "Send code",
"number_verified": "Number Verified"
}

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "Ir al portal de facturación",
"need_anything_else": "¿Necesita algo más?",
"further_billing_help": "Si necesita más ayuda con la facturación, nuestro equipo de soporte está aquí para ayudarlo.",
"contact_our_support_team": "Contacta con nuestro equipo de soporte",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "Este usuario aún no ha configurado ningún tipo de evento.",
"edit_logo": "Cambiar la marca",

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "Accéder au portail de facturation",
"need_anything_else": "Besoin d'autre chose ?",
"further_billing_help": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.",
"contact_our_support_team": "Contactez notre équipe d'assistance",
"uh_oh": "Oups !",
"no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de types d'événements.",
"edit_logo": "Modifier le logo",
@ -562,6 +561,8 @@
"duration": "Durée",
"available_durations": "Durées disponibles",
"default_duration": "Durée par défaut",
"default_duration_no_options": "Veuillez d'abord choisir les durées disponibles",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "Minutes",
"round_robin": "Round Robin",
"round_robin_description": "Faites tourner les réunions entre plusieurs membres de l'équipe.",
@ -627,6 +628,7 @@
"teams": "Équipes",
"team": "Équipe",
"team_billing": "Facturation d'équipe",
"team_billing_description": "Gérer la facturation pour votre équipe",
"upgrade_to_flexible_pro_title": "Nous avons modifié la facturation pour les équipes",
"upgrade_to_flexible_pro_message": "Des membres dans votre équipe n'ont pas de place. Mettez à niveau votre offre pro pour couvrir les places manquantes.",
"changed_team_billing_info": "Depuis janvier 2022, nous facturons chaque place aux membres de l'équipe. Les membres de votre équipe qui ont eu la version Pro gratuitement disposent maintenant d'un essai de 14 jours. Une fois leur période d'essai expirée, ces membres seront cachés pour votre équipe, sauf si vous mettez à niveau maintenant.",
@ -702,6 +704,7 @@
"hide_event_type": "Masquer le type d'événement",
"edit_location": "Modifier le lieu",
"into_the_future": "dans le futur",
"when_booked_with_less_than_notice": "Si réservé avec moins de <time></time> de préavis",
"within_date_range": "Dans une plage de dates",
"indefinitely_into_future": "Sans doute dans le futur",
"add_new_custom_input_field": "Ajouter un nouveau champ de saisie personnalisé",
@ -721,6 +724,7 @@
"delete_account_confirmation_message": "Êtes-vous sûr de vouloir supprimer votre compte {{appName}} ? Toute personne avec qui vous avez partagé le lien de votre compte ne pourra plus réserver en utilisant ce lien et toutes les préférences que vous avez enregistrées seront perdues.",
"integrations": "Intégrations",
"apps": "Applications",
"apps_listing": "Liste des applications",
"category_apps": "Applications {{category}}",
"app_store": "App Store",
"app_store_description": "Connecter les personnes, la technologie et l'espace de travail.",
@ -744,6 +748,7 @@
"toggle_calendars_conflict": "Activer/désactiver les calendriers pour lesquels vous souhaiter vérifier les conflits afin d'éviter les doubles réservations.",
"select_destination_calendar": "Créer des événements le",
"connect_additional_calendar": "Connecter un calendrier supplémentaire",
"calendar_updated_successfully": "Calendrier mis à jour avec succès",
"conferencing": "Conférence",
"calendar": "Calendrier",
"payments": "Paiements",
@ -776,6 +781,7 @@
"trending_apps": "Applications populaires",
"explore_apps": "{{category}} applications",
"installed_apps": "Applications installées",
"free_to_use_apps": "Gratuit",
"no_category_apps": "Aucune application {{category}}",
"no_category_apps_description_calendar": "Ajouter une application de calendrier pour vérifier les conflits et éviter les doubles réservations",
"no_category_apps_description_conferencing": "Essayez d'ajouter une application de conférence pour interconnecter les appels vidéo avec vos clients",
@ -814,6 +820,8 @@
"verify_wallet": "Vérifier le portefeuille",
"connect_metamask": "Connecter Metamask",
"create_events_on": "Créer des événements le :",
"enterprise_license": "Il s'agit d'une fonctionnalité d'entreprise",
"enterprise_license_description": "Pour activer cette fonctionnalité, obtenez une clé de déploiement sur la console {{consoleUrl}} et ajoutez-la à votre .env en tant que CALCOM_LICENSE_KEY. Si votre équipe a déjà une licence, veuillez contacter {{supportMail}} pour obtenir de l'aide.",
"missing_license": "Licence manquante",
"signup_requires": "Licence commerciale requise",
"signup_requires_description": "{{companyName}} ne propose pas actuellement de version open source gratuite de la page d'inscription. Pour obtenir un accès complet aux composants d'inscription, vous devez acquérir une licence commerciale. Pour une utilisation personnelle, nous recommandons la Plateforme de Données Prisma ou toute autre interface Postgres pour créer des comptes.",
@ -905,6 +913,7 @@
"user_impersonation_heading": "Connexion en tant qu'autre utilisateur",
"user_impersonation_description": "Permet à notre équipe d'assistance de se connecter temporairement lorsque vous nous aidez à résoudre rapidement tous les problèmes que vous nous signalez.",
"team_impersonation_description": "Permet aux administrateurs de votre équipe de se connecter temporairement en tant que vous-même.",
"allow_booker_to_select_duration": "Autoriser l'organisateur à sélectionner la durée",
"impersonate_user_tip": "Toutes les utilisations de cette fonctionnalité sont vérifiées.",
"impersonating_user_warning": "Identification du nom d'utilisateur \"{{user}}\".",
"impersonating_stop_instructions": "<0>Cliquez ici pour arrêter</0>.",
@ -1022,6 +1031,9 @@
"error_removing_app": "Erreur lors de la suppression de l'application",
"web_conference": "Conférence en ligne",
"requires_confirmation": "Nécessite une confirmation",
"always_requires_confirmation": "Toujours",
"requires_confirmation_threshold": "Nécessite une confirmation si réservé avec un préavis de < {{time}} $t({{unit}}_timeUnit)",
"may_require_confirmation": "Peut nécessiter une confirmation",
"nr_event_type_one": "{{count}} type d'événement",
"nr_event_type_other": "{{count}} types d'événements",
"add_action": "Ajouter une action",
@ -1109,6 +1121,9 @@
"event_limit_tab_description": "Fréquence de réservation",
"event_advanced_tab_description": "Paramètres du calendrier & plus...",
"event_advanced_tab_title": "Avancé",
"event_setup_multiple_duration_error": "Configuration de l'événement : plusieurs durées requièrent au moins une option.",
"event_setup_multiple_duration_default_error": "Configuration de l'événement: veuillez sélectionner une durée par défaut valide.",
"event_setup_booking_limits_error": "Les limites de réservation doivent être en ordre croissant. [jour,semaine,mois,année]",
"select_which_cal": "Sélectionnez le calendrier auquel ajouter des réservations",
"custom_event_name": "Nom de l'événement personnalisé",
"custom_event_name_description": "Créer des noms d'événements personnalisés à afficher sur l'événement du calendrier",
@ -1162,8 +1177,11 @@
"invoices": "Factures",
"embeds": "Intègre",
"impersonation": "Identification",
"impersonation_description": "Paramètres de gestion de l'identité de l'utilisateur",
"users": "Utilisateurs",
"profile_description": "Gérer les paramètres de votre profil {{appName}}",
"users_description": "Vous trouverez ici une liste de tous les utilisateurs",
"users_listing": "Liste des utilisateurs",
"general_description": "Gérer les paramètres pour votre langue et votre fuseau horaire",
"calendars_description": "Configurez la manière dont vos types d'événements interagissent avec vos calendriers",
"appearance_description": "Gérer les paramètres pour votre apparence de réservation",
@ -1368,6 +1386,7 @@
"number_sms_notifications": "Numéro de téléphone (notifications SMS)",
"attendee_email_workflow": "E-mail du participant",
"attendee_email_info": "E-mail de la personne ayant réservé",
"kbar_search_placeholder": "Saisissez une commande ou une recherche...",
"invalid_credential": "Oh non ! L'autorisation semble avoir expiré ou avoir été révoquée. Veuillez la réinstaller.",
"choose_common_schedule_team_event": "Choisissez un horaire commun",
"choose_common_schedule_team_event_description": "Activez cette option si vous souhaitez utiliser un horaire commun entre les hôtes. Si désactivée, chaque hôte sera réservé en fonction de son planning par défaut.",
@ -1378,5 +1397,8 @@
"test_preview": "Tester l'aperçu",
"route_to": "Router vers",
"test_preview_description": "Tester votre formulaire de routage sans envoyer de données",
"test_routing": "Tester le routage"
"test_routing": "Tester le routage",
"payment_app_disabled": "Un administrateur a désactivé une application de paiement",
"edit_event_type": "Modifier le type d'événement",
"admin_apps_description": "Activer les applications pour votre instance de Cal"
}

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "מעבר אל פורטל החיובים",
"need_anything_else": "צריך משהו נוסף?",
"further_billing_help": "אם דרוש לך סיוע נוסף בענייני חיוב, צוות התמיכה שלנו כאן כדי לעזור.",
"contact_our_support_team": "יצירת קשר עם צוות התמיכה",
"uh_oh": "אוי, לא!",
"no_event_types_have_been_setup": "משתמש זה עדיין לא הגדיר סוג אירוע.",
"edit_logo": "עריכת לוגו",

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "Vai al portale di fatturazione",
"need_anything_else": "Hai bisogno di altro?",
"further_billing_help": "Se hai bisogno di ulteriore aiuto per la fatturazione, il nostro team di supporto è qui per aiutarti.",
"contact_our_support_team": "Contatta il nostro team di supporto",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "Questo utente non ha ancora impostato alcun tipo di evento.",
"edit_logo": "Modifica logo",
@ -1404,5 +1403,40 @@
"edit_event_type": "Modifica tipo di evento",
"collective_scheduling": "Pianificazione collettiva",
"make_it_easy_to_book": "Facilita la prenotazione del tuo team quando tutti sono disponibili.",
"find_the_best_person": "Trova la persona più adatta disponibile e passa tra i membri del team."
"find_the_best_person": "Trova la persona più adatta disponibile e passa tra i membri del team.",
"fixed_round_robin": "Round robin fisso",
"add_one_fixed_attendee": "Aggiungi un partecipante fisso e fai intervenire a turno altri partecipanti.",
"calcom_is_better_with_team": "Cal.com funziona meglio in team",
"add_your_team_members": "Aggiungi i membri del tuo team ai tuoi tipi di eventi. Usa la pianificazione collettiva per includere tutti o trova la persona più adatta con la pianificazione round robin.",
"booking_limit_reached": "È stato raggiunto il limite di prenotazione per questo tipo di evento",
"admin_has_disabled": "Un amministratore ha disabilitato {{appName}}",
"disabled_app_affects_event_type": "Un amministratore ha disabilitato {{appName}}, il che influenza il tuo tipo di evento {{eventType}}",
"disable_payment_app": "L'amministratore ha disabilitato {{appName}}, il che influenza il tuo tipo di evento {{title}}. I partecipanti sono ancora in grado di prenotare questo tipo di evento, ma non gli verrà richiesto di pagare. Per evitarlo, è possibile nascondere questo tipo di evento fino a che l'amministratore non riattivi il tuo metodo di pagamento.",
"payment_disabled_still_able_to_book": "I partecipanti sono ancora in grado di prenotare questo tipo di evento, ma non gli verrà richiesto di pagare. Per evitarlo, è possibile nascondere questo tipo di evento fino a che l'amministratore non riattivi il tuo metodo di pagamento.",
"app_disabled_with_event_type": "L'amministratore ha disabilitato {{appName}}, il che influenza il tuo tipo di evento {{title}}.",
"app_disabled_video": "L'amministratore ha disabilitato {{appName}}, il che potrebbe influenzare i tuoi tipi di eventi. Se hai dei tipi di eventi che utilizzano {{appName}} come luogo, allora verrà usato Cal Video come luogo predefinito.",
"app_disabled_subject": "{{appName}} è stato disabilitato",
"navigate_installed_apps": "Vai alle app installate",
"disabled_calendar": "Se si dispone di un altro calendario installato, le nuove prenotazioni saranno aggiunte a quel calendario. Se così non fosse, connettere un nuovo calendario in modo da non perdere nessuna nuova prenotazione.",
"enable_apps": "Abilita app",
"enable_apps_description": "Abilita le app che gli utenti possono integrare con Cal.com",
"app_is_enabled": "{{appName}} è abilitato",
"app_is_disabled": "{{appName}} è disabilitato",
"keys_have_been_saved": "Le chiavi sono state salvate",
"disable_app": "Disabilita app",
"disable_app_description": "La disattivazione di questa app potrebbe causare problemi con il modo in cui i tuoi utenti interagiscono con Cal",
"edit_keys": "Modifica chiavi",
"admin_apps_description": "Abilita le app per la tua istanza di Cal",
"no_available_apps": "Nessuna app disponibile",
"no_available_apps_description": "Assicurarsi che ci siano delle applicazioni nella propria distribuzione in 'packages/app-store'",
"no_apps": "Non ci sono applicazioni abilitate in questa istanza di Cal",
"apps_settings": "Impostazioni delle app",
"fill_this_field": "Compilare il campo",
"options": "Opzioni",
"enter_option": "Immetti opzione {{index}}",
"add_an_option": "Aggiungi un'opzione",
"radio": "Radio",
"event_type_duplicate_copy_text": "{{slug}}-copia",
"set_as_default": "Imposta come predefinito",
"hide_eventtype_details": "Nascondi dettagli tipo di evento"
}

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "請求ポータルに移動",
"need_anything_else": "他に必要なものはありませんか?",
"further_billing_help": "請求に関するサポートが必要な場合は、サポートチームがお手伝いします。",
"contact_our_support_team": "サポートチームに問い合わせる",
"uh_oh": "おっと!",
"no_event_types_have_been_setup": "このユーザーはまだイベントタイプを設定していません。",
"edit_logo": "ロゴを編集",

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "결제 포털로 이동",
"need_anything_else": "다른 것이 필요하십니까?",
"further_billing_help": "청구와 관련하여 추가 도움이 필요한 경우 지원 팀이 도와드리겠습니다.",
"contact_our_support_team": "지원팀에 문의하기",
"uh_oh": "오!",
"no_event_types_have_been_setup": "사용자가 이벤트 타입을 설정하지 않았습니다.",
"edit_logo": "로고 수정하기",
@ -1427,6 +1426,7 @@
"disable_app": "앱 비활성화",
"disable_app_description": "이 앱을 비활성화하면 사용자가 Cal과 상호 작용하는 방식에 문제가 발생할 수 있습니다",
"edit_keys": "키 편집",
"admin_apps_description": "Cal 인스턴스용 앱 활성화",
"no_available_apps": "사용 가능한 앱이 없습니다",
"no_available_apps_description": "'packages/app-store' 아래 배포에 앱이 있는지 확인하세요",
"no_apps": "Cal의 이 인스턴스에서 활성화된 앱이 없습니다",

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "Ga naar het facturatiepaneel",
"need_anything_else": "Nog iets nodig?",
"further_billing_help": "Als u verdere hulp nodig heeft bij facturering, is ons ondersteuningsteam er om u te helpen.",
"contact_our_support_team": "Neem contact op met onze klantenservice",
"uh_oh": "Oeps!",
"no_event_types_have_been_setup": "Deze gebruiker heeft nog geen evenementen.",
"edit_logo": "Bewerk logo",

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "Gå til betalingsportalen",
"need_anything_else": "Trenger du noe annet?",
"further_billing_help": "Hvis du trenger ytterligere hjelp med betaling, er supportteamet vårt her for å hjelpe.",
"contact_our_support_team": "Kontakt supportteamet vårt",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "Denne brukeren har ikke konfigurert noen hendelsestyper ennå.",
"edit_logo": "Rediger logo",

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "Przejdź do portalu rozliczeniowego",
"need_anything_else": "Potrzebujesz czegoś innego?",
"further_billing_help": "Jeśli potrzebujesz dalszej pomocy przy rozliczaniu, nasz zespół wsparcia jest tutaj, aby pomóc.",
"contact_our_support_team": "Skontaktuj się z naszym zespołem wsparcia",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "Ten użytkownik nie ustawił jeszcze żadnych typów wydarzeń.",
"edit_logo": "Edytuj logo",

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "Ir para o portal de pagamento",
"need_anything_else": "Precisa de algo mais?",
"further_billing_help": "Se precisar de mais ajuda com o faturamento, o nosso time de suporte está aqui para ajudar.",
"contact_our_support_team": "Fale com a nossa equipe de suporte",
"uh_oh": "Oh oh!",
"no_event_types_have_been_setup": "Este usuário ainda não definiu nenhum tipo de evento.",
"edit_logo": "Editar logotipo",

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "Ir para o portal de pagamento",
"need_anything_else": "Precisa de algo mais?",
"further_billing_help": "Se precisar de mais ajuda com a faturação, a nossa equipa de suporte está aqui para ajudar.",
"contact_our_support_team": "Contacte a nossa equipa de suporte",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "Este utilizador ainda não configurou nenhum tipo de evento.",
"edit_logo": "Editar logótipo",
@ -1427,6 +1426,7 @@
"disable_app": "Desativar a aplicação",
"disable_app_description": "A desativação desta aplicação pode causar problemas na forma como os seus utilizadores interagem com o Cal",
"edit_keys": "Editar chaves",
"admin_apps_description": "Ativar aplicações para a sua instância do Cal",
"no_available_apps": "Não existem aplicações disponíveis",
"no_available_apps_description": "Por favor, certifique-se que existem aplicações na sua instalação em 'packages/app-store'",
"no_apps": "Não existem aplicações ativas nesta instância do Cal",

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "Accesați portalul de facturare",
"need_anything_else": "Aveți nevoie de altceva?",
"further_billing_help": "Dacă ai nevoie de ajutor suplimentar pentru facturare, echipa noastră de asistență este aici pentru a te ajuta.",
"contact_our_support_team": "Contactează echipa noastră de suport",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "Acest utilizator nu a configurat încă niciun tip de eveniment.",
"edit_logo": "Editare logo",

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "Перейти на платёжный портал",
"need_anything_else": "Нужно что-нибудь еще?",
"further_billing_help": "Если вам нужна дальнейшая помощь с оплатой счета, наша служба поддержки готова помочь.",
"contact_our_support_team": "Свяжитесь с нашей службой поддержки",
"uh_oh": "О нет!",
"no_event_types_have_been_setup": "Этот пользователь еще не создал ни одного события.",
"edit_logo": "Изменить логотип",

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "Idite na portal naplate",
"need_anything_else": "Treba li vam još nešto?",
"further_billing_help": "Ako vam treba pomoć sa naplatom, naš tim podrške je tu da pomogne.",
"contact_our_support_team": "Kontaktirjate naš tim podrške",
"uh_oh": "O ne!",
"no_event_types_have_been_setup": "Ovaj korisnik još nije podesio tipove događaja.",
"edit_logo": "Uredi logo",
@ -1427,6 +1426,7 @@
"disable_app": "Onemogući aplikaciju",
"disable_app_description": "Onemogućavanje ove aplikacije može da stvori probleme u komunikaciji vaših korisnika sa Cal-om",
"edit_keys": "Izmeni ključeve",
"admin_apps_description": "Omogućite aplikacije za vašu instancu Cal-a",
"no_available_apps": "Nema dostupnih aplikacija",
"no_available_apps_description": "Uverite se da ima aplikacija u vašoj fascikli za instalaciju pod „packages/app-store“",
"no_apps": "Nema omogućenih aplikacija u ovoj instanci Cal-a",

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "Gå till faktureringsportalen",
"need_anything_else": "Behöver du något annat?",
"further_billing_help": "Om du behöver ytterligare hjälp med fakturering finns vårt supportteam här för att hjälpa till.",
"contact_our_support_team": "Kontakta vårt kundsupportteam",
"uh_oh": "Åh nej!",
"no_event_types_have_been_setup": "Den här användaren har inte skapat några händelsetyper ännu.",
"edit_logo": "Ändra logotyp",
@ -1427,6 +1426,7 @@
"disable_app": "Inaktivera app",
"disable_app_description": "Inaktivering av den här appen kan orsaka problem med hur dina användare interagerar med Cal",
"edit_keys": "Redigera nycklar",
"admin_apps_description": "Aktivera appar för din Cal-version",
"no_available_apps": "Det finns inga tillgängliga appar",
"no_available_apps_description": "Se till att det finns appar i din distribution under \"paket/app-store\"",
"no_apps": "Det finns inga appar aktiverade i denna instans av Cal",

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "Fatura portalına git",
"need_anything_else": "Başka bir şeye ihtiyacınız var mı?",
"further_billing_help": "Faturalandırma konusunda daha fazla yardıma ihtiyacınız olursa destek ekibimiz size yardımcı olmaya hazır.",
"contact_our_support_team": "Destek ekibimizle iletişime geçin",
"uh_oh": "Hay aksi!",
"no_event_types_have_been_setup": "Bu kullanıcı henüz etkinlik türü ayarlamadı.",
"edit_logo": "Logoyu düzenle",
@ -705,6 +704,7 @@
"hide_event_type": "Etkinlik türünü gizle",
"edit_location": "Konumu düzenle",
"into_the_future": "gelecekte",
"when_booked_with_less_than_notice": "<time></time> bildiriminden daha az süre ile rezervasyon yapıldığında",
"within_date_range": "Bir tarih aralığında",
"indefinitely_into_future": "Süresiz olarak gelecekte",
"add_new_custom_input_field": "Yeni özel veri girdi alanı ekle",
@ -1033,6 +1033,7 @@
"web_conference": "Web konferansı",
"requires_confirmation": "Onay gerekli",
"always_requires_confirmation": "Her zaman",
"requires_confirmation_threshold": "{{time}} $t({{unit}}_timeUnit) bildiriminden daha az süre ile rezervasyon yapıldığında onay gerekir",
"may_require_confirmation": "Onay gerekebilir",
"nr_event_type_one": "{{count}} etkinlik türü",
"nr_event_type_other": "{{count}} etkinlik türü",
@ -1401,11 +1402,19 @@
"payment_app_disabled": "Bir yönetici, ödeme uygulamasını devre dışı bıraktı",
"edit_event_type": "Etkinlik türünü düzenle",
"collective_scheduling": "Toplu Planlama",
"make_it_easy_to_book": "Herkes müsait olduğunda ekibinizin rezervasyon yapmasını kolaylaştırın.",
"find_the_best_person": "Mevcut en iyi kişiyi bulun ve ekip üyeleriniz arasında rotasyon yapın.",
"fixed_round_robin": "Sabit döngü",
"add_one_fixed_attendee": "Tek bir sabit katılımcı ve birden fazla katılımcının olduğu bir döngü ekleyin.",
"calcom_is_better_with_team": "Cal.com ekiplerle daha iyidir",
"add_your_team_members": "Ekip üyelerinizi etkinlik türlerinize ekleyin. Herkesi eklemek için toplu planlamayı kullanın veya döngüsel planlama ile en uygun kişiyi bulun.",
"booking_limit_reached": "Bu etkinlik türü için Rezervasyon Sınırına ulaşıldı",
"admin_has_disabled": "Bir yönetici {{appName}} uygulamasını devre dışı bıraktı",
"disabled_app_affects_event_type": "Bir yönetici {{eventType}} etkinlik türünüzü etkileyen {{appName}} uygulamasını devre dışı bıraktı",
"disable_payment_app": "Yönetici, {{title}} etkinlik türünüzü etkileyebilecek {{appName}} uygulamasını devre dışı bıraktı. Katılımcılar yine de bu tür bir etkinlik için rezervasyon yaptırabilirler ancak herhangi bir ödeme yapmalarına gerek yoktur. Bu durumu önlemek için yöneticiniz ödeme yönteminizi yeniden etkinleştirene kadar etkinlik türünü gizleyebilirsiniz.",
"payment_disabled_still_able_to_book": "Katılımcılar yine de bu tür bir etkinlik için rezervasyon yaptırabilirler ancak herhangi bir ödeme yapmalarına gerek yoktur. Bu durumu önlemek için yöneticiniz ödeme yönteminizi yeniden etkinleştirene kadar etkinlik türünü gizleyebilirsiniz.",
"app_disabled_with_event_type": "Yönetici, etkinlik {{title}} türünüzü etkileyen {{appName}} uygulamasını devre dışı bıraktı.",
"app_disabled_video": "Yönetici, etkinlik türlerinizi etkileyebilecek {{appName}} uygulamasını devre dışı bıraktı. Konum olarak {{appName}} olan etkinlik türleriniz varsa varsayılan uygulama Cal Video olacaktır.",
"app_disabled_subject": "{{appName}} devre dışı bırakıldı",
"navigate_installed_apps": "Yüklü uygulamalara git",
"disabled_calendar": "Yüklü başka bir takviminiz varsa yeni randevular bu takvime eklenecektir. Aksi takdirde yeni bir takvim bağlamazsanız yeni rezervasyonları kaçırabilirsiniz.",
@ -1415,13 +1424,18 @@
"app_is_disabled": "{{appName}} devre dışı bırakıldı",
"keys_have_been_saved": "Anahtarlar kaydedildi",
"disable_app": "Uygulamaları Devre Dışı Bırak",
"disable_app_description": "Bu uygulamanın devre dışı bırakılması kullanıcılarınızın Cal ile etkileşimde bulunmaları konusunda sorunlara neden olabilir",
"edit_keys": "Anahtarları Düzenle",
"admin_apps_description": "Cal örneğiniz için uygulamayı etkinleştirin",
"no_available_apps": "Kullanılabilir uygulama yok",
"no_available_apps_description": "'Paketler/uygulama mağazası' altındaki dağıtım klasörünüzde uygulamaların olduğundan emin olun",
"no_apps": "Bu Cal örneğindeki hiçbir uygulama etkin değil",
"apps_settings": "Uygulama ayarları",
"fill_this_field": "Lütfen bu alanı doldurun",
"options": "Seçenekler",
"enter_option": "{{index}} Seçeneğini Girin",
"add_an_option": "Bir seçenek ekle",
"radio": "Radio",
"event_type_duplicate_copy_text": "{{slug}}-kopyala",
"set_as_default": "Varsayılan olarak ayarla"
}

View File

@ -52,6 +52,7 @@
"still_waiting_for_approval": "Захід досі чекає на схвалення",
"event_is_still_waiting": "Запит на захід досі в очікуванні: {{attendeeName}} {{date}} {{eventType}}",
"no_more_results": "Інших результатів немає",
"no_results": "Немає результатів",
"load_more_results": "Завантажити інші результати",
"integration_meeting_id": "Ідентифікатор наради «{{integrationName}}»: {{meetingId}}",
"confirmed_event_type_subject": "Підтверджено: {{eventType}} «{{name}}», {{date}}",
@ -248,6 +249,7 @@
"add_to_calendar": "Додати в календар",
"add_another_calendar": "Додати інший календар",
"other": "Інше",
"email_sign_in_subject": "Ваше посилання для входу в {{appName}}",
"emailed_you_and_attendees": "Ми надіслали вам та іншим учасникам запрошення до календаря з усіма деталями.",
"emailed_you_and_attendees_recurring": "Ми надіслали вам та іншим учасникам запрошення з календаря на перший із цих періодичних заходів.",
"emailed_you_and_any_other_attendees": "Цю інформацію надіслано вам і всім іншим учасникам.",
@ -383,7 +385,6 @@
"go_to_billing_portal": "Перейти на портал виставлення рахунків",
"need_anything_else": "Потрібно щось інше?",
"further_billing_help": "Якщо вам потрібна додаткова допомога з виставленням рахунків, наша служба підтримки готова її надати.",
"contact_our_support_team": "Зв’язатися зі службою підтримки",
"uh_oh": "Отакої!",
"no_event_types_have_been_setup": "Цей користувач ще не налаштував жодних типів заходів.",
"edit_logo": "Редагувати логотип",
@ -420,6 +421,7 @@
"current_incorrect_password": "Поточний пароль неправильний",
"password_hint_caplow": "Комбінація великих і малих літер",
"password_hint_min": "Щонайменше 8 символів завдовжки",
"password_hint_admin_min": "Щонайменше 15 символів",
"password_hint_num": "Містить принаймні 1 цифру",
"invalid_password_hint": "Пароль має містити не менше ніж 7 символів: принаймні одну цифру та комбінацію великих і малих літер",
"incorrect_password": "Пароль неправильний.",
@ -463,11 +465,14 @@
"booking_confirmation": "Підтвердьте свій захід ({{eventTypeTitle}}) із користувачем {{profileName}}",
"booking_reschedule_confirmation": "Перенесіть свій захід ({{eventTypeTitle}}) із користувачем {{profileName}}",
"in_person_meeting": "Посилання або особиста зустріч",
"attendeeInPerson": "Особисто (адреса відвідувача)",
"inPerson": "Особисто (адреса організатора)",
"link_meeting": "Нарада з посиланням",
"phone_call": "Номер телефону учасника",
"your_number": "Ваш номер телефону",
"phone_number": "Номер телефону",
"attendee_phone_number": "Номер телефону учасника",
"organizer_phone_number": "Телефон організатора",
"host_phone_number": "Ваш номер телефону",
"enter_phone_number": "Введіть номер телефону",
"reschedule": "Перенести",
@ -554,6 +559,10 @@
"collective": "Групова",
"collective_description": "Плануйте наради, коли доступні всі вибрані учасники команди.",
"duration": "Тривалість",
"available_durations": "Доступні варіанти тривалості",
"default_duration": "Тривалість за замовчуванням",
"default_duration_no_options": "Спочатку виберіть доступні варіанти тривалості",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "Хвилини",
"round_robin": "Ротація",
"round_robin_description": "Кілька учасників команди призначаються для нарад циклічно й по черзі.",
@ -619,6 +628,7 @@
"teams": "Команди",
"team": "Команда",
"team_billing": "Виставлення рахунків для команд",
"team_billing_description": "Керуйте виставленням рахунків у своїй команді",
"upgrade_to_flexible_pro_title": "Ми змінили умови оплати для команд",
"upgrade_to_flexible_pro_message": "У вашій команді є учасники без придбаних місць. Перейдіть на план Pro, щоб отримати всі потрібні місця.",
"changed_team_billing_info": "Станом на січень 2022 року оплата з учасників команди стягується за кількістю місць. Учасників вашої команди, які безкоштовно користувалися функціями Pro, тепер переведено на 14-денні пробні версії. Щойно пробний період завершиться, учасників вашої команди, для яких не придбано план Pro, буде приховано.",
@ -694,6 +704,7 @@
"hide_event_type": "Приховати тип заходу",
"edit_location": "Змінити розташування",
"into_the_future": "у майбутньому",
"when_booked_with_less_than_notice": "Якщо бронювання створено менше ніж за <time></time> до заходу",
"within_date_range": "У діапазоні дат",
"indefinitely_into_future": "Колись у майбутньому",
"add_new_custom_input_field": "Додати нове власне поле введення",
@ -713,6 +724,7 @@
"delete_account_confirmation_message": "Справді видалити обліковий запис {{appName}}? Усі, кому ви надавали посилання на свій обліковий запис, більше не зможуть бронювати ваш час за його допомогою. Усі збережені налаштування буде втрачено.",
"integrations": "Інтеграції",
"apps": "Додатки",
"apps_listing": "Список додатків",
"category_apps": "Додатки з категорії «{{category}}»",
"app_store": "App Store",
"app_store_description": "Спілкування та технології на робочому місці.",
@ -736,6 +748,7 @@
"toggle_calendars_conflict": "Увімкніть ті календарі, які потрібно перевірити на наявність конфліктів, щоб уникнути подвійних бронювань.",
"select_destination_calendar": "Створюйте заходи в календарі",
"connect_additional_calendar": "Підключити додатковий календар",
"calendar_updated_successfully": "Календар оновлено",
"conferencing": "Відеоконференції",
"calendar": "Календар",
"payments": "Платежі",
@ -768,6 +781,7 @@
"trending_apps": "Популярні додатки",
"explore_apps": "Додатки з категорії «{{category}}»",
"installed_apps": "Установлені додатки",
"free_to_use_apps": "Безкоштовні",
"no_category_apps": "{{category}} — немає додатків",
"no_category_apps_description_calendar": "Додайте додаток для календаря, щоб перевіряти, чи немає конфліктів у розкладі, і уникати подвійних бронювань",
"no_category_apps_description_conferencing": "Спробуйте додати додаток для конференцій, щоб інтегрувати можливість відеорозмов зі своїми клієнтами",
@ -806,6 +820,8 @@
"verify_wallet": "Пройдіть перевірку гаманця",
"connect_metamask": "Підключіть Metamask",
"create_events_on": "Створюйте заходи в календарі",
"enterprise_license": "Це корпоративна функція",
"enterprise_license_description": "Щоб увімкнути цю функцію, отримайте ключ розгортання в консолі {{consoleUrl}} і додайте його у свій файл .env як CALCOM_LICENSE_KEY. Якщо у вашої команди вже є ліцензія, зверніться по допомогу за адресою {{supportMail}}.",
"missing_license": "Відсутня ліцензія",
"signup_requires": "Потрібна комерційна ліцензія",
"signup_requires_description": "{{companyName}} зараз не надає безкоштовну версію сторінки реєстрації з відкритим кодом. Щоб отримати повний доступ до складових функціоналу реєстрації, потрібно придбати комерційну ліцензію. Для особистого використання та створення облікових записів радимо Prisma Data Platform або будь-який інший інтерфейс Postgres.",
@ -897,6 +913,7 @@
"user_impersonation_heading": "Виконання ролі користувача",
"user_impersonation_description": "Ви можете дозволити нашій команді підтримки тимчасово входити в систему під вашим іменем, щоб швидко вирішувати проблеми, про які ви повідомляєте.",
"team_impersonation_description": "Дозвольте адміністраторам своєї команди тимчасово входити під вашим іменем.",
"allow_booker_to_select_duration": "Дозволити автору бронювання вибирати тривалість",
"impersonate_user_tip": "Усі випадки використання цієї функції підпадають під аудит.",
"impersonating_user_warning": "Виконується роль користувача {{user}}.",
"impersonating_stop_instructions": "<0>Натисніть тут, щоб зупинити</0>.",
@ -1014,6 +1031,9 @@
"error_removing_app": "Не вдалося вилучити додаток",
"web_conference": "Вебконференція",
"requires_confirmation": "Потрібне підтвердження",
"always_requires_confirmation": "Завжди",
"requires_confirmation_threshold": "Вимагати підтвердження, якщо бронювання створено менше ніж за {{time}} $t({{unit}}_timeUnit) до заходу",
"may_require_confirmation": "Може вимагати підтвердження",
"nr_event_type_one": "{{count}} тип заходу",
"nr_event_type_other": "Типів заходів: {{count}}",
"add_action": "Додати дію",
@ -1101,6 +1121,9 @@
"event_limit_tab_description": "Як часто ваш час можуть бронювати",
"event_advanced_tab_description": "Налаштування календаря та інше…",
"event_advanced_tab_title": "Додатково",
"event_setup_multiple_duration_error": "Налаштування заходу: якщо мають бути доступні різні варіанти, потрібно вказати принаймні один.",
"event_setup_multiple_duration_default_error": "Налаштування заходу: виберіть припустиму тривалість за замовчуванням.",
"event_setup_booking_limits_error": "Ліміти на бронювання мають бути впорядковані за зростанням. [день,тиждень,місяць,рік]",
"select_which_cal": "Виберіть календар, у який додаватимуться бронювання",
"custom_event_name": "Користувацька назва події",
"custom_event_name_description": "Вибирайте для заходів власні назви, що показуватимуться в календарі",
@ -1154,8 +1177,11 @@
"invoices": "Рахунки-фактури",
"embeds": "Вставки",
"impersonation": "Вхід під іншим іменем",
"impersonation_description": "Налаштування входу під іншим іменем",
"users": "Користувачі",
"profile_description": "Керуйте налаштуваннями свого профілю {{appName}}",
"users_description": "Тут наведено список усіх користувачів",
"users_listing": "Список користувачів",
"general_description": "Налаштуйте параметри мови й часового поясу",
"calendars_description": "Налаштуйте, як типи заходів мають взаємодіяти з вашими календарями",
"appearance_description": "Налаштуйте варіанти оформлення свого бронювання",
@ -1360,6 +1386,7 @@
"number_sms_notifications": "Номер телефону (SMS-сповіщення)",
"attendee_email_workflow": "Адреса ел. пошти учасника",
"attendee_email_info": "Адреса ел. пошти особи, яка бронює",
"kbar_search_placeholder": "Введіть команду або пошуковий запис…",
"invalid_credential": "Отакої! Схоже, дозвіл більше не дійсний або його відкликано. Перевстановіть додаток знову.",
"choose_common_schedule_team_event": "Виберіть спільний розклад",
"choose_common_schedule_team_event_description": "Увімкніть цей параметр, щоб використовувати спільний для двох ведучих розклад. Якщо цей параметр вимкнено, бронювання для кожного з ведучих відбуватиметься за їхніми власними графіками.",
@ -1370,5 +1397,43 @@
"test_preview": "Перевірити попередній перегляд",
"route_to": "Кінцева точка",
"test_preview_description": "Перевірте свою форму переспрямування без надсилання даних",
"test_routing": "Перевірка переспрямування"
"test_routing": "Перевірка переспрямування",
"payment_app_disabled": "Додаток для оплати вимкнув адміністратор",
"edit_event_type": "Редагувати тип заходу",
"collective_scheduling": "Колективне планування",
"make_it_easy_to_book": "Просте бронювання у випадках, коли всі члени вашої команди доступні.",
"find_the_best_person": "Пошук найкращого доступного члена команди та циклічна ротація між ними.",
"fixed_round_robin": "Фіксована циклічна ротація",
"add_one_fixed_attendee": "Додавання одного фіксованого учасника та циклічна ротація між кількома учасниками.",
"calcom_is_better_with_team": "З Cal.com краще працювати в командах",
"add_your_team_members": "Додавайте членів своєї команди в типи заходів. Колективне планування дає змогу включати всіх або знаходити найвідповіднішого члена завдяки циклічній ротації.",
"booking_limit_reached": "Для цього типу заходу досягнуто ліміт бронювання",
"admin_has_disabled": "Адміністратор вимкнув {{appName}}",
"disabled_app_affects_event_type": "Адміністратор вимкнув {{appName}}, що впливає на ваш тип заходу «{{eventType}}»",
"disable_payment_app": "Адміністратор вимкнув {{appName}}, що впливає на ваш тип заходу «{{title}}». Учасники все одно можуть бронювати події такого типу, але від них не вимагатиметься оплата. Ви можете приховати цей тип заходу, щоб цього не ставалося, та дочекатися на активацію способу оплати з боку адміністратора.",
"payment_disabled_still_able_to_book": "Учасники все одно можуть бронювати події такого типу, але від них не вимагатиметься оплата. Ви можете приховати цей тип заходу, щоб цього не ставалося, та дочекатися на активацію способу оплати з боку адміністратора.",
"app_disabled_with_event_type": "Адміністратор вимкнув {{appName}}, що впливає на ваш тип заходу «{{title}}».",
"app_disabled_video": "Адміністратор вимкнув {{appName}}, що впливає на ваші типи заходів. Якщо у вас є типи заходів, де {{appName}} визначає розташування, за замовчуванням використовуватиметься Cal Video.",
"app_disabled_subject": "{{appName}} вимкнено",
"navigate_installed_apps": "Перейти до встановлених додатків",
"disabled_calendar": "Якщо ви встановите інший календар, у нього буде додано нові бронювання. Якщо цього не станеться, під’єднайте новий календар, щоб не пропускати нові бронювання.",
"enable_apps": "Увімкнення додатків",
"enable_apps_description": "Активуйте додатки, які користувачі зможуть інтегрувати з Cal.com",
"app_is_enabled": "{{appName}} увімкнено",
"app_is_disabled": "{{appName}} вимкнено",
"keys_have_been_saved": "Ключі збережено",
"disable_app": "Вимкнути додаток",
"disable_app_description": "Якщо вимкнути цей додаток, у користувачів можуть виникнути проблеми із роботою з Cal",
"edit_keys": "Редагувати ключі",
"no_available_apps": "Немає доступних додатків",
"no_available_apps_description": "Перевірте, чи в «packages/app-store» є додатки для розгортання",
"no_apps": "У цьому екземплярі Cal немає ввімкнених додатків",
"apps_settings": "Налаштування додатків",
"fill_this_field": "Заповніть це поле",
"options": "Варіанти",
"enter_option": "Введіть варіант {{index}}",
"add_an_option": "Додайте варіант",
"radio": "Радіо",
"event_type_duplicate_copy_text": "{{slug}}-копія",
"set_as_default": "Встановити за замовчуванням"
}

View File

@ -385,7 +385,6 @@
"go_to_billing_portal": "Đi tới cổng thanh toán",
"need_anything_else": "Cần gì nữa không?",
"further_billing_help": "Nếu bạn cần thêm bất kỳ trợ giúp nào về thanh toán, nhóm hỗ trợ của chúng tôi luôn sẵn sàng trợ giúp.",
"contact_our_support_team": "Liên hệ với nhóm hỗ trợ của chúng tôi",
"uh_oh": "\bÔi không!",
"no_event_types_have_been_setup": "Người dùng này chưa thiết lập bất kỳ loại sự kiện nào.",
"edit_logo": "Chỉnh sửa logo",
@ -422,6 +421,7 @@
"current_incorrect_password": "Mật khẩu hiện tại không đúng",
"password_hint_caplow": "Kết hợp các chữ cái in hoa & in thường",
"password_hint_min": "Dài tối thiểu 8 ký tự",
"password_hint_admin_min": "Dài tối thiểu 15 ký tự",
"password_hint_num": "Chứa ít nhất 1 con số",
"invalid_password_hint": "Mật khẩu phải có tối thiểu 7 ký tự chứa ít nhất một con số và phải kết hợp chữ cái in hoa lẫn in thường",
"incorrect_password": "Mật khẩu không đúng.",
@ -465,11 +465,14 @@
"booking_confirmation": "Xác nhận {{eventTypeTitle}} của bạn với {{profileName}}",
"booking_reschedule_confirmation": "Lên lịch lại {{eventTypeTitle}} của bạn với {{profileName}}",
"in_person_meeting": "Gặp mặt trực tiếp",
"attendeeInPerson": "Đích thân (địa chỉ người tham gia)",
"inPerson": "Đích thân (địa chỉ người tổ chức)",
"link_meeting": "Liên kết cuộc họp",
"phone_call": "Số điện thoại của người tham gia",
"your_number": "Số điện thoại của bạn",
"phone_number": "Số điện thoại",
"attendee_phone_number": "Số điện thoại của người tham gia",
"organizer_phone_number": "Số điện thoại người tổ chức",
"host_phone_number": "Số điện thoại của bạn",
"enter_phone_number": "Nhập số điện thoại",
"reschedule": "Thay đổi lịch hẹn",
@ -556,6 +559,10 @@
"collective": "Tập thể",
"collective_description": "Lên lịch họp khi tất cả các thành viên trong nhóm đã chọn đều có mặt.",
"duration": "Khoảng thời gian",
"available_durations": "Khoảng thời gian khả dụng",
"default_duration": "Khoảng thời gian mặc định",
"default_duration_no_options": "Vui lòng chọn trước tiên những khoảng thời gian khả dụng",
"multiple_duration_mins": "{{count}}$t(minute_timeUnit)",
"minutes": "Phút",
"round_robin": "Round Robin",
"round_robin_description": "Luân phiên những cuộc họp giữa các thành viên trong nhóm.",
@ -621,6 +628,7 @@
"teams": "Các nhóm",
"team": "Nhóm",
"team_billing": "Thanh toán nhóm",
"team_billing_description": "Quản lí thanh toán cho đội ngũ của bạn",
"upgrade_to_flexible_pro_title": "Chúng tôi đã thay đổi thanh toán cho các nhóm",
"upgrade_to_flexible_pro_message": "Có những thành viên trong nhóm của bạn không có gói. Nâng cấp gói PRO của bạn để trang trải những gói bị thiếu.",
"changed_team_billing_info": "Kể từ tháng 1 năm 2022, chúng tôi tính phí trên cơ sở từng người cho các thành viên trong nhóm. Các thành viên trong nhóm của bạn mà từng có PRO miễn phí nay sẽ chuyển sang dùng thử 14 ngày. Khi thời gian dùng thử hết hạn, các thành viên này sẽ bị ẩn khỏi nhóm của bạn trừ khi bạn nâng cấp.",
@ -696,6 +704,7 @@
"hide_event_type": "Ẩn loại sự kiện",
"edit_location": "Chỉnh sửa vị trí",
"into_the_future": "trong tương lai",
"when_booked_with_less_than_notice": "Khi đặt hẹn với khoảng thời gian thông báo ít hơn <time></time>",
"within_date_range": "Trong phạm vi ngày",
"indefinitely_into_future": "Vô thời hạn trong tương lai",
"add_new_custom_input_field": "Thêm trường tùy chỉnh mới",
@ -715,6 +724,7 @@
"delete_account_confirmation_message": "Bạn có chắc chắn muốn xóa tài khoản {{appName}} của mình không? Bất kỳ ai mà bạn đã chia sẻ liên kết tài khoản của mình sẽ không thể đặt trước bằng liên kết đó nữa và mọi tùy chọn bạn đã lưu sẽ bị mất.",
"integrations": "Tích hợp",
"apps": "Ứng dụng",
"apps_listing": "Danh sách ứng dụng",
"category_apps": "Ứng dụng {{category}}",
"app_store": "Cửa hàng ứng dụng",
"app_store_description": "Kết nối con người, công nghệ và nơi làm việc.",
@ -738,6 +748,7 @@
"toggle_calendars_conflict": "Bật lịch mà bạn muốn kiểm tra trùng ngày để tránh đặt lịch hẹn trùng.",
"select_destination_calendar": "Tạo sự kiện trên",
"connect_additional_calendar": "Kết nối lịch bổ sung",
"calendar_updated_successfully": "Đã cập nhật lịch thành công",
"conferencing": "Hội nghị",
"calendar": "Lịch",
"payments": "Thanh toán",
@ -770,6 +781,7 @@
"trending_apps": "Ứng dụng thịnh hành",
"explore_apps": "{{category}} ứng dụng",
"installed_apps": "Ứng dụng đã cài đặt",
"free_to_use_apps": "Miễn phí",
"no_category_apps": "Không có ứng dụng {{category}}",
"no_category_apps_description_calendar": "Thêm một ứng dụng lịch để kiểm tra xung đột nhằm tránh đặt lịch hẹn trùng",
"no_category_apps_description_conferencing": "Thử thêm vào một ứng dụng hội nghị để hợp nhất cuộc gọi video với khách hàng của bạn",
@ -808,6 +820,8 @@
"verify_wallet": "Xác minh Ví",
"connect_metamask": "Kết nối Metamask",
"create_events_on": "Tạo sự kiện trên",
"enterprise_license": "Đây là tính năng doanh nghiệp",
"enterprise_license_description": "Để bật tính năng này, nhận khoá triển khai tại console {{consoleUrl}} và thêm nó vào .env của bạn ở dạng CALCOM_LICENSE_KEY. Nếu nhóm của bạn đã có giấy phép, vui lòng liên hệ {{supportMail}} để được trợ giúp.",
"missing_license": "Giấy phép bị thiếu",
"signup_requires": "Yêu cầu giấy phép thương mại",
"signup_requires_description": "{{companyName}} hiện không cung cấp phiên bản nguồn mở miễn phí của trang đăng ký. Để nhận toàn quyền truy cập vào các thành phần đăng ký, bạn cần có giấy phép thương mại. Đối với mục đích sử dụng cá nhân, chúng tôi khuyên bạn nên sử dụng Nền tảng dữ liệu Prisma hoặc bất kỳ giao diện Postgres nào khác để tạo tài khoản.",
@ -899,6 +913,7 @@
"user_impersonation_heading": "Mạo danh người dùng",
"user_impersonation_description": "Cho phép đội ngũ hỗ trợ tạm thời đăng nhập với tư cách là bạn nhằm giúp chúng tôi nhanh chóng giải quyết mọi vấn đề mà bạn báo cáo cho chúng tôi.",
"team_impersonation_description": "Cho phép các quản trị viên của nhóm bạn tạm thời đăng nhập bằng danh tính của bạn.",
"allow_booker_to_select_duration": "Cho phép người đặt hẹn chọn khoảng thời gian",
"impersonate_user_tip": "Tất cả những lần dùng tính năng này đều bị kiểm toán.",
"impersonating_user_warning": "Đang mạo danh tên người dùng \"{{user}}\".",
"impersonating_stop_instructions": "<0>Nhấp vào đây để ngừng lại</0>.",
@ -1016,6 +1031,9 @@
"error_removing_app": "Lỗi khi gỡ bỏ ứng dụng",
"web_conference": "Hội nghị web",
"requires_confirmation": "Yêu cầu xác nhận",
"always_requires_confirmation": "Luôn luôn",
"requires_confirmation_threshold": "Cần xác nhận nếu đặt hẹn với thông báo trước chưa đầy {{time}}$t({{unit}}_timeUnit)",
"may_require_confirmation": "Có thể yêu cầu xác nhận",
"nr_event_type_one": "{{count}} loại sự kiện",
"nr_event_type_other": "{{count}} loại sự kiện",
"add_action": "Thêm hoạt động",
@ -1103,6 +1121,9 @@
"event_limit_tab_description": "Bạn có thể được đặt lịch bao lần",
"event_advanced_tab_description": "Cài đặt cho lịch & nhiều tính năng khác...",
"event_advanced_tab_title": "Nâng cao",
"event_setup_multiple_duration_error": "Thiết lập sự kiện: Nhiều khoảng thời gian cần ít nhất 1 tuỳ chọn.",
"event_setup_multiple_duration_default_error": "Thiết lập sự kiện: Vui lòng chọn một khoảng thời gian mặc định hợp lệ.",
"event_setup_booking_limits_error": "Giới hạn lịch hẹn phải theo thứ tự tăng dần. [day,week,month,year]",
"select_which_cal": "Chọn lịch nào cần thêm lịch hẹn vào",
"custom_event_name": "Tên sự kiện tuỳ chỉnh",
"custom_event_name_description": "Tạo những tên sự kiện tuỳ chỉnh để hiển thị ở phần sự kiện lịch",
@ -1156,8 +1177,11 @@
"invoices": "Hoá đơn",
"embeds": "Nhúng",
"impersonation": "Mạo danh",
"impersonation_description": "Cài đặt để quản lý mạo danh người dùng",
"users": "Người dùng",
"profile_description": "Quản lí cài đặt cho hồ sơ {{appName}} của bạn",
"users_description": "Tại đây bạn có thể tìm thấy danh sách tất cả người dùng",
"users_listing": "Danh sách người dùng",
"general_description": "Quản lí cài đặt cho ngôn ngữ và múi giờ của bạn",
"calendars_description": "Cấu hình cách các loại sự kiện của bạn tương tác với lịch của bạn",
"appearance_description": "Quản lí cài đặt cho giao diện lịch hẹn của bạn",
@ -1362,6 +1386,7 @@
"number_sms_notifications": "Số điện thoại (thông báo SMS)",
"attendee_email_workflow": "Email người tham dự",
"attendee_email_info": "Email người tham gia lịch hẹn",
"kbar_search_placeholder": "Nhập một lệnh hoặc tìm kiếm...",
"invalid_credential": "Ôi không! Có vẻ như quyền đã hết hạn hoặc đã bị thu hồi. Vui lòng cài đặt lại.",
"choose_common_schedule_team_event": "Chọn một lịch chung",
"choose_common_schedule_team_event_description": "Bật mục này nếu bạn muốn dùng lịch thông thường giữa các chủ sự kiện. Khi tắt đi, mỗi chủ sự kiện sẽ được đặt lịch theo lịch mặc định của họ.",
@ -1372,5 +1397,9 @@
"test_preview": "Kiểm tra Xem trước",
"route_to": "Định hướng đến",
"test_preview_description": "Kiểm tra biểu mẫu định hướng mà không cần gửi bất kỳ dữ liệu nào",
"test_routing": "Kiểm tra định hướng"
"test_routing": "Kiểm tra định hướng",
"payment_app_disabled": "Một quản trị viên đã vô hiệu hoá một ứng dụng thanh toán",
"edit_event_type": "Sửa loại sự kiện",
"collective_scheduling": "Lên lịch tập thể",
"admin_apps_description": "Bật các ứng dụng cho thực thể Cal của bạn"
}

View File

@ -249,6 +249,7 @@
"add_to_calendar": "添加到日历",
"add_another_calendar": "添加另一个日历",
"other": "其他",
"email_sign_in_subject": "您的 {{appName}} 登录链接",
"emailed_you_and_attendees": "我们通过电子邮件给您和其他参与者发送了一份包含所有详细信息的日历邀请。",
"emailed_you_and_attendees_recurring": "我们通过电子邮件给您和其他参与者发送了第一次定期活动的日历邀请。",
"emailed_you_and_any_other_attendees": "已向您和其他参与者发送了关于此信息的电子邮件",
@ -384,7 +385,6 @@
"go_to_billing_portal": "转到账单门户网站",
"need_anything_else": "还有其他需要?",
"further_billing_help": "如果您在支付帐单时需要任何进一步的帮助,我们的支持团队会给您提供帮助。",
"contact_our_support_team": "联系我们的支持团队",
"uh_oh": "糟糕!",
"no_event_types_have_been_setup": "此用户尚未设置任何活动类型。",
"edit_logo": "编辑标志",
@ -421,6 +421,7 @@
"current_incorrect_password": "当前密码不正确",
"password_hint_caplow": "大写和小写字母混合",
"password_hint_min": "至少 8 个字符长",
"password_hint_admin_min": "至少 15 个字符长",
"password_hint_num": "包含至少 1 个数字",
"invalid_password_hint": "密码必须是至少 7 个字符长,包含至少一个数字,并且是大写和小写字母混合",
"incorrect_password": "密码不正确。",
@ -464,11 +465,14 @@
"booking_confirmation": "确认您和 {{profileName}} 的 {{eventTypeTitle}}",
"booking_reschedule_confirmation": "重新安排您和 {{profileName}} 的 {{eventTypeTitle}}",
"in_person_meeting": "线上或线下会议",
"attendeeInPerson": "本人(参与者地址)",
"inPerson": "本人(组织者地址)",
"link_meeting": "线上会议",
"phone_call": "参与者电话号码",
"your_number": "您的电话号码",
"phone_number": "电话号码",
"attendee_phone_number": "参与者电话号码",
"organizer_phone_number": "组织者电话号码",
"host_phone_number": "您的电话号码",
"enter_phone_number": "输入电话号码",
"reschedule": "重新安排",
@ -555,6 +559,10 @@
"collective": "集体模式",
"collective_description": "当所有选定的团队成员都可预约时安排会议。",
"duration": "时长",
"available_durations": "可用时长",
"default_duration": "默认时长",
"default_duration_no_options": "请先选择可用时长",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "分钟",
"round_robin": "轮流模式",
"round_robin_description": "和多个团队成员之间轮流举行会议。",
@ -620,6 +628,7 @@
"teams": "团队",
"team": "团队",
"team_billing": "团队计费",
"team_billing_description": "管理您团队的计费",
"upgrade_to_flexible_pro_title": "我们更改了团队的计费方式",
"upgrade_to_flexible_pro_message": "您的团队中有没有位置的成员。升级到专业版来获得更多位置。",
"changed_team_billing_info": "自2022年1月起我们将按每个位置向团队成员收取费用。 您的团队中免费获得专业版的成员现在正在进行为期14天的试用。 试用期结束后,这些成员将在您的团队中隐藏,除非您现在升级到专业版。",
@ -714,6 +723,8 @@
"delete_account_confirmation_message": "您确定要删除您的 {{appName}} 账户? 任何曾通过您的账户链接与您预约过的人都将无法再使用它和您预约,您保存的任何偏好设置都将丢失。",
"integrations": "集成",
"apps": "应用",
"apps_description": "为您的 Cal 实例启用应用",
"apps_listing": "应用列表",
"category_apps": "{{category}}应用",
"app_store": "应用商店",
"app_store_description": "连接人员、技术和工作场所。",
@ -737,6 +748,7 @@
"toggle_calendars_conflict": "切换要检查冲突的日历,以防止重复预约。",
"select_destination_calendar": "创建活动于",
"connect_additional_calendar": "连接其他日历",
"calendar_updated_successfully": "日历已成功更新",
"conferencing": "会议",
"calendar": "日历",
"payments": "付款",
@ -769,6 +781,7 @@
"trending_apps": "热门应用",
"explore_apps": "{{category}}应用",
"installed_apps": "已安装的应用",
"free_to_use_apps": "免费",
"no_category_apps": "无{{category}}应用",
"no_category_apps_description_calendar": "添加日历应用以检查冲突,防止重复预约",
"no_category_apps_description_conferencing": "尝试添加会议应用以整合与客户的视频通话",
@ -807,6 +820,8 @@
"verify_wallet": "验证钱包",
"connect_metamask": "连接元掩码",
"create_events_on": "活动创建于:",
"enterprise_license": "这是企业版功能",
"enterprise_license_description": "要启用此功能,请在 {{consoleUrl}} 控制台获取一个部署密钥,并将其添加到您的 .env 中作为 CALCOM_LICENSE_KEY。如果您的团队已经有许可证请联系 {{supportMail}} 获取帮助。",
"missing_license": "缺少许可证",
"signup_requires": "需要商业许可证",
"signup_requires_description": "{{companyName}} 免费开源版目前不提供的注册页面。 要获得对注册页面组件的完全访问权,您需要获得商业许可证。 对于个人使用用途我们推荐使用Prisma 数据平台或任何其他Postgres接口创建账户。",
@ -898,6 +913,7 @@
"user_impersonation_heading": "用户模拟",
"user_impersonation_description": "允许我们的支持团队临时以您的身份登录,帮助我们快速解决您报告给我们的问题。",
"team_impersonation_description": "允许您的团队成员以您的身份临时登录。",
"allow_booker_to_select_duration": "允许预约者选择时长",
"impersonate_user_tip": "此功能的所有使用都已审核。",
"impersonating_user_warning": "正在模拟用户名“{{user}}”。",
"impersonating_stop_instructions": "<0>点击此处停止</0>。",
@ -1015,6 +1031,8 @@
"error_removing_app": "删除应用时出错",
"web_conference": "网络会议",
"requires_confirmation": "需要确认",
"always_requires_confirmation": "始终",
"may_require_confirmation": "可能需要确认",
"nr_event_type_one": "{{count}} 种活动类型",
"nr_event_type_other": "{{count}} 种活动类型",
"add_action": "添加操作",
@ -1102,6 +1120,9 @@
"event_limit_tab_description": "您多久可被预约一次",
"event_advanced_tab_description": "日历设置及更多...",
"event_advanced_tab_title": "高级",
"event_setup_multiple_duration_error": "活动设置: 多个时长需要至少 1 个选项。",
"event_setup_multiple_duration_default_error": "活动设置: 请选择有效的默认时长。",
"event_setup_booking_limits_error": "预约限制必须按升序排列。[日,周,月,年]",
"select_which_cal": "选择要添加预约的日历",
"custom_event_name": "自定义活动名称",
"custom_event_name_description": "创建要在日历活动上显示的自定义活动名称",
@ -1155,8 +1176,11 @@
"invoices": "发票",
"embeds": "嵌入项",
"impersonation": "模拟",
"impersonation_description": "用于管理用户模拟的设置",
"users": "用户",
"profile_description": "管理您的 {{appName}} 个人资料的设置",
"users_description": "在这里可以找到所有用户的列表",
"users_listing": "用户列表",
"general_description": "管理语言和时区的设置",
"calendars_description": "配置您的活动类型与日历的交互方式",
"appearance_description": "管理预约页面外观的设置",
@ -1361,6 +1385,7 @@
"number_sms_notifications": "电话号码(短信通知)",
"attendee_email_workflow": "参与者电子邮件",
"attendee_email_info": "预约人的电子邮件",
"kbar_search_placeholder": "输入命令或搜索...",
"invalid_credential": "哦不!看起来权限已过期或被撤销。请重新安装。",
"choose_common_schedule_team_event": "选择通用时间表",
"choose_common_schedule_team_event_description": "如果您希望在主持人之间使用通用时间表,请启用此选项。如果禁用,将根据主持人的默认时间表预约每个主持人。",
@ -1371,5 +1396,36 @@
"test_preview": "测试预览",
"route_to": "根据途径找到",
"test_preview_description": "测试途径表格而不提交任何数据",
"test_routing": "测试途径"
"test_routing": "测试途径",
"payment_app_disabled": "管理员已禁用支付应用",
"edit_event_type": "编辑活动类型",
"collective_scheduling": "集体日程安排",
"fixed_round_robin": "固定轮流模式",
"calcom_is_better_with_team": "Cal.com 更适合团队",
"admin_has_disabled": "管理员已禁用 {{appName}}",
"disabled_app_affects_event_type": "管理员已禁用会影响活动类型 {{eventType}} 的 {{appName}}",
"disable_payment_app": "管理员已禁用会影响活动类型 {{title}} 的 {{appName}}。参与者仍然能预约此类型的活动,但不会被提示付款。您可以隐藏该活动类型来防止出现这种情况,直到管理员重新启用您的支付方式。",
"payment_disabled_still_able_to_book": "参与者仍然能预约此类型的活动,但不会被提示付款。您可以隐藏该活动类型来防止出现这种情况,直到管理员重新启用您的支付方式。",
"app_disabled_with_event_type": "管理员已禁用会影响活动类型 {{title}} 的 {{appName}}。",
"app_disabled_video": "管理员已禁用可能会影响活动类型的 {{appName}}。如果您有将 {{appName}} 作为位置的活动类型,则默认为 Cal Video。",
"app_disabled_subject": "{{appName}} 已被禁用",
"navigate_installed_apps": "转到已安装的应用",
"enable_apps": "启用应用",
"enable_apps_description": "启用用户可以与 Cal.com 集成的应用",
"app_is_enabled": "{{appName}} 已启用",
"app_is_disabled": "{{appName}} 已禁用",
"keys_have_been_saved": "密钥已保存",
"disable_app": "禁用应用",
"disable_app_description": "禁用此应用可能会导致您的用户与 Cal 的交互方式出现问题",
"edit_keys": "编辑密钥",
"admin_apps_description": "为您的 Cal 实例启用应用",
"no_available_apps": "没有可用的应用",
"no_available_apps_description": "请确保您在“packages/app-store”下的部署中有应用",
"no_apps": "此 Cal 实例中未启用任何应用",
"apps_settings": "应用设置",
"fill_this_field": "请填写此字段",
"options": "选项",
"enter_option": "输入选项 {{index}}",
"add_an_option": "添加选项",
"set_as_default": "设置为默认"
}

View File

@ -383,7 +383,6 @@
"go_to_billing_portal": "前往付費入口",
"need_anything_else": "還要什麼嗎?",
"further_billing_help": "如果有關於付費的進一步需求,我們的支援團隊隨時上前幫忙。",
"contact_our_support_team": "聯繫支援團隊",
"uh_oh": "哎呦喂呀!",
"no_event_types_have_been_setup": "使用者尚未設定任何活動類型。",
"edit_logo": "編輯標誌",

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

View File

@ -432,6 +432,11 @@ hr {
border: none !important;
}
.react-date-picker__inputGroup__input {
padding-top: 0;
padding-bottom: 0;
}
/* animations */
.slideInBottom {
animation-duration: 0.3s;

View File

@ -10,6 +10,7 @@ const HAWAII_AND_NEWYORK_TEAM = [
timeZone: "America/Detroit", // GMT -4 per 22th of Aug, 2022
workingHours: [{ days: [1, 2, 3, 4, 5], startTime: 780, endTime: 1260 }],
busy: [],
dateOverrides: [],
},
{
timeZone: "Pacific/Honolulu", // GMT -10 per 22th of Aug, 2022
@ -20,6 +21,7 @@ const HAWAII_AND_NEWYORK_TEAM = [
{ days: [5], startTime: 780, endTime: 1439 },
],
busy: [],
dateOverrides: [],
},
];

@ -1 +1 @@
Subproject commit 30cbf3990bb5baae7fd4fd46c93f6e5bc402f84c
Subproject commit 794dda81932f6c0572941fb70cc38b599510d31f

View File

@ -39,6 +39,7 @@ import { metadata as stripepayment_meta } from "./stripepayment/_metadata";
import { metadata as tandemvideo_meta } from "./tandemvideo/_metadata";
import { metadata as telegram_meta } from "./telegram/_metadata";
import { metadata as typeform_meta } from "./typeform/_metadata";
import { metadata as vimcal_meta } from "./vimcal/_metadata";
import { metadata as vital_meta } from "./vital/_metadata";
import { metadata as weather_in_your_calendar_meta } from "./weather_in_your_calendar/_metadata";
import { metadata as whatsapp_meta } from "./whatsapp/_metadata";
@ -86,6 +87,7 @@ export const appStoreMetadata = {
tandemvideo: tandemvideo_meta,
telegram: telegram_meta,
typeform: typeform_meta,
vimcal: vimcal_meta,
vital: vital_meta,
weather_in_your_calendar: weather_in_your_calendar_meta,
whatsapp: whatsapp_meta,

View File

@ -38,6 +38,7 @@ export const apiHandlers = {
tandemvideo: import("./tandemvideo/api"),
telegram: import("./telegram/api"),
typeform: import("./typeform/api"),
vimcal: import("./vimcal/api"),
vital: import("./vital/api"),
weather_in_your_calendar: import("./weather_in_your_calendar/api"),
whatsapp: import("./whatsapp/api"),

View File

@ -40,8 +40,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await prisma.credential.create({
data,
});
} catch (reason) {
logger.error("Could not add this caldav account", reason);
} catch (e) {
logger.error("Could not add this caldav account", e);
if (e instanceof Error) {
let message = e.message;
if (e.message.indexOf("Invalid credentials") > -1 && url.indexOf("dav.php") > -1) {
const parsedUrl = new URL(url);
const adminUrl =
parsedUrl.protocol +
"//" +
parsedUrl.hostname +
(parsedUrl.port ? ":" + parsedUrl.port : "") +
"/admin/?/settings/standard/";
message = `Couldn\'t connect to caldav account, please verify WebDAV authentication type is set to "Basic"`;
return res.status(500).json({ message, actionUrl: adminUrl });
}
}
return res.status(500).json({ message: "Could not add this caldav account" });
}

View File

@ -18,6 +18,7 @@ export default function CalDavCalendarSetup() {
});
const [errorMessage, setErrorMessage] = useState("");
const [errorActionUrl, setErrorActionUrl] = useState("");
return (
<div className="flex h-screen bg-gray-200">
@ -49,6 +50,9 @@ export default function CalDavCalendarSetup() {
const json = await res.json();
if (!res.ok) {
setErrorMessage(json?.message || t("something_went_wrong"));
if (json.actionUrl) {
setErrorActionUrl(json.actionUrl);
}
} else {
router.push(json.url);
}
@ -78,7 +82,24 @@ export default function CalDavCalendarSetup() {
/>
</fieldset>
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
{errorMessage && (
<Alert
severity="error"
title={errorMessage}
actions={
errorActionUrl !== "" ? (
<Button
href={errorActionUrl}
color="secondary"
target="_blank"
className="ml-5 w-32 !p-5">
Go to Admin
</Button>
) : undefined
}
className="my-4"
/>
)}
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
<Button type="button" color="secondary" onClick={() => router.back()}>
{t("cancel")}

View File

@ -135,7 +135,7 @@ export const FormActionsDropdown = ({ form, children }: { form: RoutingForm; chi
return (
<dropdownCtx.Provider value={{ dropdown: true }}>
<Dropdown>
<DropdownMenuTrigger asChild>
<DropdownMenuTrigger data-testid="form-dropdown" asChild>
<Button
type="button"
size="icon"

View File

@ -120,6 +120,7 @@ const Actions = ({
{typeformApp?.isInstalled ? (
<FormActionsDropdown form={form}>
<FormAction
data-testid="copy-redirect-url"
routingForm={form}
action="copyRedirectUrl"
color="minimal"
@ -172,6 +173,7 @@ const Actions = ({
</FormAction>
{typeformApp ? (
<FormAction
data-testid="copy-redirect-url"
routingForm={form}
action="copyRedirectUrl"
color="minimal"

View File

@ -131,6 +131,7 @@ export default function RoutingForms({
</FormAction>
{typeformApp?.isInstalled ? (
<FormAction
data-testid="copy-redirect-url"
routingForm={form}
action="copyRedirectUrl"
color="minimal"

View File

@ -8,7 +8,7 @@ async function gotoRoutingLink(page: Page, formId: string) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
async function addForm(page: Page) {
export async function addForm(page: Page) {
await page.click('[data-testid="new-routing-form"]');
await page.fill("input[name]", "Test Form Name");
await page.click('[data-testid="add-form"]');
@ -41,7 +41,7 @@ async function verifySelectOptions(
};
}
async function fillForm(
export async function fillForm(
page: Page,
form: { description: string; field?: { typeIndex: number; label: string } }
) {

View File

@ -157,13 +157,32 @@ export default class Office365CalendarService implements Calendar {
}
async listCalendars(): Promise<IntegrationCalendar[]> {
const response = await this.fetcher(`/me/calendars`);
let responseBody = await handleErrorsJson<{ value: OfficeCalendar[] }>(response);
// If responseBody is valid then parse the JSON text
if (typeof responseBody === "string") {
responseBody = JSON.parse(responseBody) as { value: OfficeCalendar[] };
const officeCalendars: OfficeCalendar[] = [];
// List calendars from MS are paginated
let finishedParsingCalendars = false;
// Store @odata.nextLink if in response
let requestLink = "/me/calendars";
while (!finishedParsingCalendars) {
const response = await this.fetcher(requestLink);
let responseBody = await handleErrorsJson<{ value: OfficeCalendar[]; "@odata.nextLink"?: string }>(
response
);
// If responseBody is valid then parse the JSON text
if (typeof responseBody === "string") {
responseBody = JSON.parse(responseBody) as { value: OfficeCalendar[] };
}
officeCalendars.push(...responseBody.value);
if (responseBody["@odata.nextLink"]) {
requestLink = responseBody["@odata.nextLink"].replace(this.apiGraphUrl, "");
} else {
finishedParsingCalendars = true;
}
}
return responseBody?.value.map((cal: OfficeCalendar) => {
return officeCalendars.map((cal: OfficeCalendar) => {
const calendar: IntegrationCalendar = {
externalId: cal.id ?? "No Id",
integration: this.integrationName,

View File

@ -0,0 +1,71 @@
import { Page, expect } from "@playwright/test";
import {
addForm as addRoutingForm,
fillForm as fillRoutingForm,
} from "@calcom/app-store/ee/routing-forms/playwright/tests/basic.e2e";
import { CAL_URL } from "@calcom/lib/constants";
import { Fixtures, test } from "@calcom/web/playwright/lib/fixtures";
const installApps = async (page: Page, users: Fixtures["users"]) => {
const user = await users.create({ username: "routing-forms" });
await user.login();
await page.goto(`/apps/routing-forms`);
await page.click('[data-testid="install-app-button"]');
await page.waitForNavigation({
url: (url) => url.pathname === `/apps/routing-forms/forms`,
});
await page.goto(`/apps/typeform`);
await page.click('[data-testid="install-app-button"]');
await page.waitForNavigation({
url: (url) => url.pathname === `/apps/typeform/how-to-use`,
});
};
test.describe("Typeform App", () => {
test.afterEach(async ({ users }) => {
// This also delete forms on cascade
await users.deleteAll();
});
test.describe("Typeform Redirect Link", () => {
test("should copy link in editing area", async ({ page, context, users }) => {
await installApps(page, users);
context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.goto(`/apps/routing-forms/forms`);
const formId = await addRoutingForm(page);
await fillRoutingForm(page, {
description: "",
field: { label: "test", typeIndex: 1 },
});
await page.click('[data-testid="form-dropdown"]');
await page.click('[data-testid="copy-redirect-url"]');
const text = await page.evaluate(async () => {
return navigator.clipboard.readText();
});
expect(text).toBe(`${CAL_URL}/router?form=${formId}&test={Recalled_Response_For_This_Field}`);
});
test("should copy link in RoutingForms list", async ({ page, context, users }) => {
await installApps(page, users);
context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.goto("/apps/routing-forms/forms");
const formId = await addRoutingForm(page);
await fillRoutingForm(page, {
description: "",
field: { label: "test", typeIndex: 1 },
});
await page.goto("/apps/routing-forms/forms");
await page.click('[data-testid="form-dropdown"]');
await page.click('[data-testid="copy-redirect-url"]');
const text = await page.evaluate(async () => {
return navigator.clipboard.readText();
});
expect(text).toBe(`${CAL_URL}/router?form=${formId}&test={Recalled_Response_For_This_Field}`);
});
});
});

View File

@ -0,0 +1,11 @@
---
description: The world's fastest calendar, beautifully designed for a remote world
items:
- /api/app-store/vimcal/1.gif
- /api/app-store/vimcal/2.gif
- /api/app-store/vimcal/3.gif
- /api/app-store/vimcal/4.gif
---
{description}

View File

@ -0,0 +1,10 @@
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
...config,
} as AppMeta;
export default metadata;

View File

@ -0,0 +1,21 @@
import { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import { createDefaultInstallation } from "../../_utils/installation";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
// Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
redirect: {
newTab: true,
url: "https://cal.com/blog/cal-plus-vimcal",
},
createCredential: ({ appType, user, slug }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
};
export default handler;

View File

@ -0,0 +1 @@
export { default as add } from "./add";

View File

@ -0,0 +1,16 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Vimcal",
"slug": "vimcal",
"type": "vimcal_other",
"imageSrc": "/api/app-store/vimcal/icon.svg",
"logo": "/api/app-store/vimcal/icon.svg",
"url": "https://cal.com/apps/vimcal",
"variant": "other",
"categories": ["calendar"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"description": "The world's fastest calendar, beautifully designed for a remote world\r",
"extendsFeature": "User",
"__createdUsingCli": true
}

View File

@ -0,0 +1,2 @@
export * as api from "./api";
export { metadata } from "./_metadata";

View File

@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/vimcal",
"version": "0.0.0",
"main": "./index.ts",
"description": "The world's fastest calendar, beautifully designed for a remote world\r",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
}
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -0,0 +1,28 @@
<svg width="62" height="58" viewBox="0 0 62 58" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.90676 11.7682C0.331514 15.5535 -0.904363 20.8542 2.31152 24.925C4.55796 27.7688 8.12817 28.9722 11.4731 28.3502C25.0725 15.2446 38.1409 10.9884 48.1289 10.1525C49.6733 10.0233 51.1425 9.97591 52.5271 9.98991C55.2643 7.11068 58.346 4.54111 61.7196 2.35078L61.7443 2.28935C61.7443 2.28935 47.1718 -1.08376 32.9863 0.929534C20.5999 2.68751 8.38054 7.03215 3.90676 11.7682Z" fill="url(#paint0_linear_1303_1047)"/>
<path d="M13.9571 39.6669C11.2816 36.28 11.3264 31.6336 13.7764 28.3277C26.6775 16.3673 38.9317 12.4894 48.26 11.7088L50.7032 12.0128C46.1398 17.3558 42.6942 23.6199 40.6579 30.4189C37.9724 31.1454 35.321 32.4756 32.801 34.2211C29.4853 36.5177 26.3539 39.561 23.607 42.988C20.1257 43.8278 16.3134 42.6495 13.9571 39.6669Z" fill="url(#paint1_linear_1303_1047)"/>
<path d="M24.7519 44.0672C24.5613 44.4084 24.3899 44.7653 24.2397 45.1369C22.2998 49.9396 24.6453 55.3958 29.4789 57.3235C31.0785 57.9616 32.7496 58.1333 34.3407 57.9032C35.9331 57.6814 37.4896 57.0516 38.8456 55.9941C39.1282 55.7738 39.3946 55.5405 39.6449 55.296C39.4201 54.2267 39.2282 53.1435 39.0705 52.047C38.0873 45.208 38.525 38.4931 40.1611 32.1969C37.9955 32.9019 35.8236 34.0315 33.7003 35.5022C30.4914 37.7248 27.4375 40.6986 24.7519 44.0672Z" fill="url(#paint2_linear_1303_1047)"/>
<defs>
<linearGradient id="paint0_linear_1303_1047" x1="57.8884" y1="7.12205" x2="-11.0432" y2="37.0341" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFB6BE"/>
<stop offset="0.113339" stop-color="#FA6753"/>
<stop offset="0.432815" stop-color="#F50058"/>
<stop offset="0.61187" stop-color="#F50081"/>
<stop offset="0.77952" stop-color="#C71FD6"/>
</linearGradient>
<linearGradient id="paint1_linear_1303_1047" x1="57.8876" y1="7.12205" x2="-11.0441" y2="37.0341" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFB6BE"/>
<stop offset="0.113339" stop-color="#FA6753"/>
<stop offset="0.432815" stop-color="#F50058"/>
<stop offset="0.61187" stop-color="#F50081"/>
<stop offset="0.77952" stop-color="#C71FD6"/>
</linearGradient>
<linearGradient id="paint2_linear_1303_1047" x1="57.8879" y1="7.12206" x2="-11.0437" y2="37.0341" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFB6BE"/>
<stop offset="0.113339" stop-color="#FA6753"/>
<stop offset="0.432815" stop-color="#F50058"/>
<stop offset="0.61187" stop-color="#F50081"/>
<stop offset="0.77952" stop-color="#C71FD6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -47,6 +47,7 @@ const getEventType = async (id: number) => {
startTime: true,
endTime: true,
days: true,
date: true,
},
},
},
@ -225,18 +226,32 @@ export async function getUserAvailability(
const startGetWorkingHours = performance.now();
const timeZone = schedule.timeZone || eventType?.timeZone || currentUser.timeZone;
const workingHours = getWorkingHours(
{ timeZone },
const availability =
schedule.availability ||
(eventType?.availability.length ? eventType.availability : currentUser.availability)
);
(eventType?.availability.length ? eventType.availability : currentUser.availability);
const workingHours = getWorkingHours({ timeZone }, availability);
const endGetWorkingHours = performance.now();
logger.debug(`getWorkingHours took ${endGetWorkingHours - startGetWorkingHours}ms for userId ${userId}`);
const dateOverrides = availability
.filter((availability) => !!availability.date)
.map((override) => {
const startTime = dayjs.utc(override.startTime);
const endTime = dayjs.utc(override.endTime);
return {
start: dayjs.utc(override.date).hour(startTime.hour()).minute(startTime.minute()).toDate(),
end: dayjs.utc(override.date).hour(endTime.hour()).minute(endTime.minute()).toDate(),
};
});
return {
busy: bufferedBusyTimes,
timeZone,
workingHours,
dateOverrides,
currentSeats,
};
}

View File

@ -30,6 +30,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat";
import isBetween from "dayjs/plugin/isBetween";
import isToday from "dayjs/plugin/isToday";
import localizedFormat from "dayjs/plugin/localizedFormat";
import minmax from "dayjs/plugin/minMax";
import relativeTime from "dayjs/plugin/relativeTime";
import timeZone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
@ -44,6 +45,7 @@ dayjs.extend(relativeTime);
dayjs.extend(timeZone);
dayjs.extend(toArray);
dayjs.extend(utc);
dayjs.extend(minmax);
export type Dayjs = dayjs.Dayjs;

View File

@ -1,30 +1,33 @@
import { LinkIcon } from "./LinkIcon";
export const CallToAction = (props: { label: string; href: string }) => (
export const CallToAction = (props: { label: string; href: string; secondary?: boolean }) => (
<p
style={{
display: "inline-block",
background: "#292929",
background: props.secondary ? "#FFFFFF" : "#292929",
border: props.secondary ? "1px solid #d1d5db" : "",
color: "#ffffff",
fontFamily: "Roboto, Helvetica, sans-serif",
fontSize: "16px",
fontSize: "14px",
fontWeight: 500,
lineHeight: "120%",
lineHeight: "20px",
margin: 0,
textDecoration: "none",
textTransform: "none",
padding: "10px 25px",
padding: "10px 16px",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
msoPaddingAlt: "0px",
borderRadius: "3px",
borderRadius: "6px",
boxSizing: "border-box",
}}>
<a
style={{ color: "#FFFFFF", textDecoration: "none" }}
style={{ color: props.secondary ? "#292929" : "#FFFFFF", textDecoration: "none" }}
href={props.href}
target="_blank"
rel="noreferrer">
{props.label} <LinkIcon />
{props.label}
<LinkIcon secondary />
</a>
</p>
);

View File

@ -4,7 +4,6 @@ export const CallToActionTable = (props: { children: React.ReactNode }) => (
<tr>
<td
align="center"
bgcolor="#292929"
role="presentation"
style={{
border: "none",
@ -13,7 +12,6 @@ export const CallToActionTable = (props: { children: React.ReactNode }) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
msoPaddingAlt: "10px 25px",
background: "#292929",
}}
valign="middle">
{props.children}

View File

@ -1,9 +1,11 @@
import { BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants";
export const LinkIcon = () => (
export const LinkIcon = ({ secondary }: { secondary?: boolean }) => (
<img
src={IS_PRODUCTION ? BASE_URL + "/emails/linkIcon.png" : "https://app.cal.com/emails/linkIcon.png"}
width="12px"
srcSet={IS_PRODUCTION ? BASE_URL + "/emails/linkIcon.svg" : "https://app.cal.com/emails/linkIcon.svg"}
width="16px"
style={{ marginBottom: "-3px", marginLeft: "8px", ...(secondary && { filter: "brightness(80%)" }) }}
alt=""
/>
);

View File

@ -0,0 +1,3 @@
export const Separator = () => (
<p style={{ width: "16px", height: "16px", display: "inline-block" }}>&nbsp;</p>
);

View File

@ -0,0 +1,191 @@
/* eslint-disable @next/next/no-head-element */
import BaseTable from "./BaseTable";
import EmailBodyLogo from "./EmailBodyLogo";
import EmailHead from "./EmailHead";
import EmailScheduledBodyHeaderContent from "./EmailScheduledBodyHeaderContent";
import EmailSchedulingBodyDivider from "./EmailSchedulingBodyDivider";
import EmailSchedulingBodyHeader, { BodyHeadType } from "./EmailSchedulingBodyHeader";
import RawHtml from "./RawHtml";
import Row from "./Row";
const Html = (props: { children: React.ReactNode }) => (
<>
<RawHtml html="<!doctype html>" />
<html>{props.children}</html>
</>
);
export const V2BaseEmailHtml = (props: {
children: React.ReactNode;
callToAction?: React.ReactNode;
subject: string;
title?: string;
subtitle?: React.ReactNode;
headerType?: BodyHeadType;
}) => {
return (
<Html>
<EmailHead title={props.subject} />
<body style={{ wordSpacing: "normal", backgroundColor: "#F5F5F5" }}>
<div style={{ backgroundColor: "#F5F5F5" }}>
<RawHtml
html={`<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
/>
<div style={{ margin: "0px auto", maxWidth: 600 }}>
<Row align="center" border="0" style={{ width: "100%" }}>
<td
style={{
direction: "ltr",
fontSize: "0px",
padding: "0px",
paddingTop: "40px",
textAlign: "center",
}}>
<RawHtml
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->`}
/>
</td>
</Row>
</div>
{props.headerType && <EmailSchedulingBodyHeader headerType={props.headerType} />}
{props.title && <EmailScheduledBodyHeaderContent title={props.title} subtitle={props.subtitle} />}
{(props.headerType || props.title || props.subtitle) && <EmailSchedulingBodyDivider />}
<RawHtml
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" className="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
/>
<div
style={{
margin: "0px auto",
maxWidth: 600,
}}>
<Row align="center" border="0" style={{ width: "100%" }}>
<td
style={{
direction: "ltr",
fontSize: 0,
padding: 0,
textAlign: "center",
}}>
<RawHtml
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td className="" style="vertical-align:top;width:598px;" ><![endif]-->`}
/>
<div
className="mj-column-per-100 mj-outlook-group-fix"
style={{
fontSize: 0,
textAlign: "left",
direction: "ltr",
display: "inline-block",
verticalAlign: "top",
width: "100%",
border: "1px solid #E1E1E1",
borderRadius: "6px",
}}>
<Row
border="0"
style={{
verticalAlign: "top",
borderRadius: "6px",
background: "#FFFFFF",
backgroundColor: "#FFFFFF",
}}
width="100%">
<td
align="left"
style={{
fontSize: 0,
padding: "40px",
wordBreak: "break-word",
}}>
<div
style={{
fontFamily: "Roboto, Helvetica, sans-serif",
fontSize: 16,
fontWeight: 500,
lineHeight: 1,
textAlign: "left",
color: "#3E3E3E",
}}>
{props.children}
</div>
</td>
</Row>
</div>
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
</td>
</Row>
</div>
{props.callToAction && <EmailSchedulingBodyDivider />}
<RawHtml
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" className="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
/>
<div
style={{
margin: "0px auto",
maxWidth: 600,
}}>
<Row align="center" border="0" style={{ width: "100%" }}>
<td
style={{
direction: "ltr",
fontSize: 0,
padding: 0,
textAlign: "center",
}}>
<RawHtml
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td className="" style="vertical-align:top;width:598px;" ><![endif]-->`}
/>
{props.callToAction && (
<div
className="mj-column-per-100 mj-outlook-group-fix"
style={{
fontSize: 0,
textAlign: "left",
direction: "ltr",
display: "inline-block",
verticalAlign: "top",
width: "100%",
}}>
<BaseTable border="0" style={{ verticalAlign: "top" }} width="100%">
<tbody>
<tr>
<td
align="center"
vertical-align="middle"
style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
{props.callToAction}
</td>
</tr>
<tr>
<td
align="left"
style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
<div
style={{
fontFamily: "Roboto, Helvetica, sans-serif",
fontSize: 13,
lineHeight: 1,
textAlign: "left",
color: "#000000",
}}
/>
</td>
</tr>
</tbody>
</BaseTable>
</div>
)}
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
</td>
</Row>
</div>
<EmailBodyLogo />
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
</div>
</body>
</Html>
);
};

View File

@ -3,21 +3,27 @@ import { RRule } from "rrule";
import dayjs from "@calcom/dayjs";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Info } from "./Info";
function getRecurringWhen({ calEvent }: { calEvent: CalendarEvent }) {
if (calEvent.recurringEvent) {
const t = calEvent.attendees[0].language.translate;
const rruleOptions = new RRule(calEvent.recurringEvent).options;
const recurringEvent: RecurringEvent = {
export function getRecurringWhen({
recurringEvent,
attendee,
}: {
recurringEvent?: RecurringEvent | null;
attendee: Pick<Person, "language">;
}) {
if (recurringEvent) {
const t = attendee.language.translate;
const rruleOptions = new RRule(recurringEvent).options;
const recurringEventConfig: RecurringEvent = {
freq: rruleOptions.freq,
count: rruleOptions.count || 1,
interval: rruleOptions.interval,
};
return ` - ${getEveryFreqFor({ t, recurringEvent })}`;
return `${getEveryFreqFor({ t, recurringEvent: recurringEventConfig })}`;
}
return "";
}
@ -33,10 +39,15 @@ export function WhenInfo(props: { calEvent: CalendarEvent; timeZone: string; t:
return dayjs(props.calEvent.endTime).tz(timeZone).format(format);
}
const recurringInfo = getRecurringWhen({
recurringEvent: props.calEvent.recurringEvent,
attendee: props.calEvent.attendees[0],
});
return (
<div>
<Info
label={`${t("when")} ${getRecurringWhen(props)}`}
label={`${t("when")} ${recurringInfo !== "" ? ` - ${recurringInfo}` : ""}`}
lineThrough={
!!props.calEvent.cancellationReason && !props.calEvent.cancellationReason.includes("$RCH$")
}

View File

@ -1,4 +1,5 @@
export { BaseEmailHtml } from "./BaseEmailHtml";
export { V2BaseEmailHtml } from "./V2BaseEmailHtml";
export { CallToAction } from "./CallToAction";
export { CallToActionTable } from "./CallToActionTable";
export { CustomInputs } from "./CustomInputs";
@ -10,3 +11,4 @@ export { default as RawHtml } from "./RawHtml";
export { WhenInfo } from "./WhenInfo";
export { WhoInfo } from "./WhoInfo";
export { AppsStatus } from "./AppsStatus";
export { Separator } from "./Separator";

View File

@ -1,27 +1,42 @@
import { CallToAction, CallToActionTable } from "../components";
import { createHmac } from "crypto";
import { CallToAction, CallToActionTable, Separator } from "../components";
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
export const OrganizerRequestEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
<OrganizerScheduledEmail
title={
props.title || props.calEvent.recurringEvent?.count
? "event_awaiting_approval_recurring"
: "event_awaiting_approval"
}
subtitle={<>{props.calEvent.organizer.language.translate("someone_requested_an_event")}</>}
headerType="calendarCircle"
subject="event_awaiting_approval_subject"
callToAction={
<CallToActionTable>
<CallToAction
label={props.calEvent.organizer.language.translate("confirm_or_reject_request")}
href={
process.env.NEXT_PUBLIC_WEBAPP_URL +
(props.calEvent.recurringEvent?.count ? "/bookings/recurring" : "/bookings/upcoming")
}
/>
</CallToActionTable>
}
{...props}
/>
);
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
export const OrganizerRequestEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => {
const signedData = `${props.attendee.email}/${props.calEvent.uid}`;
const signature = createHmac("sha1", CALENDSO_ENCRYPTION_KEY).update(signedData).digest("base64");
return (
<OrganizerScheduledEmail
title={
props.title || props.calEvent.recurringEvent?.count
? "event_awaiting_approval_recurring"
: "event_awaiting_approval"
}
subtitle={<>{props.calEvent.organizer.language.translate("someone_requested_an_event")}</>}
headerType="calendarCircle"
subject="event_awaiting_approval_subject"
callToAction={
<CallToActionTable>
<CallToAction
label={props.calEvent.organizer.language.translate("accept")}
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/booking/direct/accept/${encodeURIComponent(
props.attendee.email
)}/${encodeURIComponent(props.calEvent.uid as string)}/${encodeURIComponent(signature)}`}
/>
<Separator />
<CallToAction
label={props.calEvent.organizer.language.translate("reject")}
secondary
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/booking/direct/reject/${encodeURIComponent(
props.attendee.email
)}/${encodeURIComponent(props.calEvent.uid as string)}/${encodeURIComponent(signature)}`}
/>
</CallToActionTable>
}
{...props}
/>
);
};

View File

@ -1,8 +1,8 @@
import type { TFunction } from "next-i18next";
import { APP_NAME } from "@calcom/lib/constants";
import { APP_NAME, BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants";
import { BaseEmailHtml, CallToAction } from "../components";
import { V2BaseEmailHtml, CallToAction } from "../components";
type TeamInvite = {
language: TFunction;
@ -12,15 +12,36 @@ type TeamInvite = {
joinLink: string;
};
export const TeamInviteEmail = (props: TeamInvite & Partial<React.ComponentProps<typeof BaseEmailHtml>>) => {
export const TeamInviteEmail = (
props: TeamInvite & Partial<React.ComponentProps<typeof V2BaseEmailHtml>>
) => {
return (
<BaseEmailHtml
<V2BaseEmailHtml
subject={props.language("user_invited_you", {
user: props.from,
team: props.teamName,
appName: APP_NAME,
})}>
<p>
<img
height="64"
src={
IS_PRODUCTION
? BASE_URL + "/emails/teamCircle@2x.png"
: "http://localhost:3000/emails/teamCircle@2x.png"
}
style={{
border: "0",
display: "block",
outline: "none",
textDecoration: "none",
height: "64px",
fontSize: "13px",
marginBottom: "24px",
}}
width="64"
alt=""
/>
<p style={{ fontSize: "24px", marginBottom: "16px" }}>
<>
{props.language("user_invited_you", {
user: props.from,
@ -30,21 +51,22 @@ export const TeamInviteEmail = (props: TeamInvite & Partial<React.ComponentProps
!
</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "32px" }}>
<>{props.language("calcom_explained", { appName: APP_NAME })}</>
</p>
<CallToAction label={props.language("accept_invitation")} href={props.joinLink} />
<div style={{ lineHeight: "6px" }}>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<div style={{ borderTop: "1px solid #E1E1E1", marginTop: "32px", paddingTop: "32px" }}>
<p style={{ fontWeight: 400, margin: 0 }}>
<>
{props.language("have_any_questions")}{" "}
<a href="mailto:support@cal.com" style={{ color: "#3E3E3E" }} target="_blank" rel="noreferrer">
<>{props.language("contact_our_support_team")}</>
</a>
<>{props.language("contact")}</>
</a>{" "}
{props.language("our_support_team")}
</>
</p>
</div>
</BaseEmailHtml>
</V2BaseEmailHtml>
);
};

View File

@ -1,4 +0,0 @@
# Availability related code will live here
- [ ] Maybe migrate `getBusyTimes` here
- [ ] Maybe migrate `getUserAvailability` here (or into `users` feature)

View File

@ -195,6 +195,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
},
availability: {
select: {
date: true,
startTime: true,
endTime: true,
days: true,
@ -834,6 +835,8 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
evt.appsStatus = Object.values(calcAppsStatus);
}
let videoCallUrl;
if (originalRescheduledBooking?.uid) {
// Use EventManager to conditionally use all needed integrations.
const updateManager = await eventManager.reschedule(
@ -868,9 +871,9 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
metadata.conferenceData = updatedEvent.conferenceData;
metadata.entryPoints = updatedEvent.entryPoints;
handleAppsStatus(results, booking);
videoCallUrl = metadata.hangoutLink || videoCallUrl;
}
}
if (noEmail !== true) {
await sendRescheduledEmails({
...evt,
@ -892,6 +895,9 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
results = createManager.results;
referencesToCreate = createManager.referencesToCreate;
videoCallUrl = evt.videoCallData && evt.videoCallData.url ? evt.videoCallData.url : null;
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
errorCode: "BookingCreatingMeetingFailed",
@ -908,6 +914,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
metadata.conferenceData = results[0].createdEvent?.conferenceData;
metadata.entryPoints = results[0].createdEvent?.entryPoints;
handleAppsStatus(results, booking);
videoCallUrl = metadata.hangoutLink || videoCallUrl;
}
if (noEmail !== true) {
await sendScheduledEmails({
@ -944,7 +951,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
}
log.debug(`Booking ${organizerUser.username} completed`);
const metadata = videoCallUrl ? { videoCallUrl } : undefined;
if (isConfirmedByDefault) {
const eventTrigger: WebhookTriggerEvents = rescheduleUid
? WebhookTriggerEvents.BOOKING_RESCHEDULED
@ -1000,7 +1007,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
...eventTypeInfo,
bookingId,
rescheduleUid,
metadata: reqBody.metadata,
metadata: { ...metadata, ...reqBody.metadata },
eventTypeId,
status: "ACCEPTED",
smsReminderNumber: booking?.smsReminderNumber || undefined,
@ -1042,6 +1049,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
uid: booking.uid,
},
data: {
metadata,
references: {
createMany: {
data: referencesToCreate,

View File

@ -0,0 +1,79 @@
import dayjs from "@calcom/dayjs";
import { TimeRange } from "@calcom/types/schedule";
import { CalendarEvent } from "./types/events";
const startDate = dayjs().set("hour", 11).set("minute", 0);
export const events: CalendarEvent[] = [
{
id: 1,
title: "Event 1",
start: startDate.add(10, "minutes").toDate(),
end: startDate.add(45, "minutes").toDate(),
allDay: false,
source: "Booking",
status: "ACCEPTED",
},
{
id: 2,
title: "Event 2",
start: startDate.add(1, "day").toDate(),
end: startDate.add(1, "day").add(30, "minutes").toDate(),
allDay: false,
source: "Booking",
status: "ACCEPTED",
},
{
id: 2,
title: "Event 3",
start: startDate.add(2, "day").toDate(),
end: startDate.add(2, "day").add(60, "minutes").toDate(),
allDay: false,
source: "Booking",
status: "ACCEPTED",
},
{
id: 3,
title: "Event 4",
start: startDate.add(3, "day").toDate(),
end: startDate.add(3, "day").add(2, "hour").add(30, "minutes").toDate(),
allDay: false,
source: "Booking",
status: "ACCEPTED",
},
{
id: 5,
title: "Event 4 Overlap",
start: startDate.add(3, "day").add(30, "minutes").toDate(),
end: startDate.add(3, "day").add(2, "hour").add(45, "minutes").toDate(),
allDay: false,
source: "Booking",
status: "ACCEPTED",
},
{
id: 4,
title: "Event 1 Overlap",
start: startDate.toDate(),
end: startDate.add(30, "minutes").toDate(),
allDay: false,
source: "Booking",
status: "ACCEPTED",
},
{
id: 6,
title: "Event 1 Overlap Two",
start: startDate.toDate(),
end: startDate.add(30, "minutes").toDate(),
allDay: false,
source: "Booking",
status: "ACCEPTED",
},
];
export const blockingDates: TimeRange[] = [
{
start: startDate.add(1, "day").hour(10).toDate(),
end: startDate.add(1, "day").hour(13).toDate(),
},
];

View File

@ -0,0 +1,133 @@
import React, { useEffect, useMemo, useRef } from "react";
import { useCalendarStore } from "../state/store";
import "../styles/styles.css";
import { CalendarComponentProps } from "../types/state";
import { getDaysBetweenDates, getHoursToDisplay } from "../utils";
import { DateValues } from "./DateValues";
import { BlockedList } from "./blocking/BlockedList";
import { EmptyCell } from "./event/Empty";
import { EventList } from "./event/EventList";
import { SchedulerColumns } from "./grid";
import { SchedulerHeading } from "./heading/SchedulerHeading";
import { HorizontalLines } from "./horizontalLines";
import { VeritcalLines } from "./verticalLines";
export function Calendar(props: CalendarComponentProps) {
const container = useRef<HTMLDivElement | null>(null);
const containerNav = useRef<HTMLDivElement | null>(null);
const containerOffset = useRef<HTMLDivElement | null>(null);
const initalState = useCalendarStore((state) => state.initState);
const startDate = useCalendarStore((state) => state.startDate);
const endDate = useCalendarStore((state) => state.endDate);
const startHour = useCalendarStore((state) => state.startHour || 0);
const endHour = useCalendarStore((state) => state.endHour || 23);
const usersCellsStopsPerHour = useCalendarStore((state) => state.gridCellsPerHour || 4);
const days = useMemo(() => getDaysBetweenDates(startDate, endDate), [startDate, endDate]);
const hours = useMemo(() => getHoursToDisplay(startHour || 0, endHour || 23), [startHour, endHour]);
const numberOfGridStopsPerDay = hours.length * usersCellsStopsPerHour;
// Initalise State on inital mount
useEffect(() => {
initalState(props);
}, [props, initalState]);
return (
<MobileNotSupported>
<div
className="scheduler-wrapper flex h-full w-full flex-col overflow-y-scroll"
style={
{ "--one-minute-height": `calc(1.75rem/(60/${usersCellsStopsPerHour}))` } as React.CSSProperties // This can't live in the css file because it's a dynamic value and css variable gets super
}>
<SchedulerHeading />
<div ref={container} className="relative isolate flex flex-auto flex-col bg-white">
<div
style={{ width: "165%" }}
className="flex max-w-full flex-none flex-col sm:max-w-none md:max-w-full">
<DateValues containerNavRef={containerNav} days={days} />
{/* TODO: Implement this at a later date. */}
{/* <CurrentTime
containerNavRef={containerNav}
containerOffsetRef={containerOffset}
containerRef={container}
/> */}
<div className="flex flex-auto">
<div className="sticky left-0 z-10 w-14 flex-none bg-white ring-1 ring-gray-100" />
<div className="grid flex-auto grid-cols-1 grid-rows-1 ">
<HorizontalLines
hours={hours}
numberOfGridStopsPerCell={usersCellsStopsPerHour}
containerOffsetRef={containerOffset}
/>
<VeritcalLines days={days} />
{/* Empty Cells */}
<SchedulerColumns
zIndex={50}
offsetHeight={containerOffset.current?.offsetHeight}
gridStopsPerDay={numberOfGridStopsPerDay}>
<>
{[...Array(days.length)].map((_, i) => (
<li
key={i}
style={{
gridRow: `2 / span ${numberOfGridStopsPerDay}`,
position: "relative",
}}>
{/* While startDate < endDate: */}
{[...Array(numberOfGridStopsPerDay)].map((_, j) => {
const key = `${i}-${j}`;
return (
<EmptyCell
key={key}
day={days[i].toDate()}
gridCellIdx={j}
totalGridCells={numberOfGridStopsPerDay}
selectionLength={endHour - startHour}
startHour={startHour}
/>
);
})}
</li>
))}
</>
</SchedulerColumns>
<SchedulerColumns
offsetHeight={containerOffset.current?.offsetHeight}
gridStopsPerDay={numberOfGridStopsPerDay}>
{/*Loop over events per day */}
{days.map((day, i) => {
return (
<li key={day.toISOString()} className="relative" style={{ gridColumnStart: i + 1 }}>
<EventList day={day} />
<BlockedList day={day} containerRef={container} />
</li>
);
})}
</SchedulerColumns>
</div>
</div>
</div>
</div>
</div>
</MobileNotSupported>
);
}
/** @todo Will be removed once we have mobile support */
const MobileNotSupported = ({ children }: { children: React.ReactNode }) => {
return (
<>
<div className="flex h-full flex-col items-center justify-center sm:hidden">
<h1 className="text-2xl font-bold">Mobile not supported yet </h1>
<p className="text-gray-500">Please use a desktop browser to view this page</p>
</div>
<div className="hidden sm:block">{children}</div>
</>
);
};

View File

@ -0,0 +1,60 @@
import React from "react";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
type Props = {
days: dayjs.Dayjs[];
containerNavRef: React.RefObject<HTMLDivElement>;
};
export function DateValues({ days, containerNavRef }: Props) {
return (
<div
ref={containerNavRef}
className="sticky top-0 z-30 flex-none border-b border-b-gray-300 bg-white sm:pr-8">
<div className="flex text-sm leading-6 text-gray-500 sm:hidden" data-dayslength={days.length}>
{days.map((day) => {
const isToday = dayjs().isSame(day, "day");
return (
<button
key={day.toString()}
type="button"
className="flex flex-1 flex-col items-center pt-2 pb-3">
{day.format("dd")}{" "}
<span
className={classNames(
"mt-1 flex h-8 w-8 items-center justify-center font-semibold text-gray-900",
isToday && "rounded-full bg-gray-900 text-white"
)}>
{day.format("D")}
</span>
</button>
);
})}
</div>
<div className="-mr-px hidden auto-cols-fr text-sm leading-6 text-gray-500 sm:flex ">
<div className="col-end-1 w-14" />
{days.map((day) => {
const isToday = dayjs().isSame(day, "day");
return (
<div
key={day.toString()}
className={classNames("flex flex-1 items-center justify-center py-3", isToday && "font-bold")}>
<span>
{day.format("ddd")}{" "}
<span
className={classNames(
"items-center justify-center p-1",
isToday && "rounded-full bg-gray-900 text-white"
)}>
{day.format("DD")}
</span>
</span>
</div>
);
})}
</div>
</div>
);
}

View File

@ -0,0 +1,129 @@
import { useMemo } from "react";
import shallow from "zustand/shallow";
import dayjs from "@calcom/dayjs";
import { useCalendarStore } from "../../state/store";
import { BlockedTimeCell } from "./BlockedTimeCell";
type Props = {
day: dayjs.Dayjs;
containerRef: React.RefObject<HTMLDivElement>;
};
function roundX(x: number, roundBy: number) {
return Math.round(x / roundBy) * roundBy;
}
type BlockedDayProps = {
startHour: number;
endHour: number;
day: dayjs.Dayjs;
};
function BlockedBeforeToday({ day, startHour, endHour }: BlockedDayProps) {
return (
<>
{day.isBefore(dayjs(), "day") && (
<div
key={day.format("YYYY-MM-DD")}
className="absolute z-40 w-full"
style={{
top: `var(--one-minute-height)`,
zIndex: 60,
height: `calc(${(endHour + 1 - startHour) * 60} * var(--one-minute-height))`, // Add 1 to endHour to include the last hour that we add to display the last vertical line
}}>
<BlockedTimeCell />
</div>
)}
</>
);
}
function BlockedToday({
day,
startHour,
gridCellsPerHour,
endHour,
}: BlockedDayProps & { gridCellsPerHour: number }) {
const dayStart = useMemo(() => day.startOf("day").hour(startHour), [day, startHour]);
const dayEnd = useMemo(() => day.startOf("day").hour(endHour), [day, endHour]);
const dayEndInMinutes = useMemo(() => dayEnd.diff(dayStart, "minutes"), [dayEnd, dayStart]);
let nowComparedToDayStart = useMemo(() => dayjs().diff(dayStart, "minutes"), [dayStart]);
if (nowComparedToDayStart > dayEndInMinutes) nowComparedToDayStart = dayEndInMinutes;
return (
<>
{day.isToday() && (
<div
key={day.format("YYYY-MM-DD")}
className="absolute z-40 w-full"
style={{
top: `var(--one-minute-height)`, // Still need this as this var takes into consideration the offset of the "AllDayEvents" bar
zIndex: 60,
height: `calc(${roundX(
nowComparedToDayStart,
60 / gridCellsPerHour
)} * var(--one-minute-height) - 2px)`, // We minus the border width to make it 🧹
}}>
<BlockedTimeCell />
</div>
)}
</>
);
}
export function BlockedList({ day }: Props) {
const { startHour, blockingDates, endHour, gridCellsPerHour } = useCalendarStore(
(state) => ({
startHour: state.startHour || 0,
endHour: state.endHour || 23,
blockingDates: state.blockingDates,
gridCellsPerHour: state.gridCellsPerHour || 4,
}),
shallow
);
return (
<>
<BlockedBeforeToday day={day} startHour={startHour} endHour={endHour} />
<BlockedToday gridCellsPerHour={gridCellsPerHour} day={day} startHour={startHour} endHour={endHour} />
{blockingDates &&
blockingDates.map((event, i) => {
const dayStart = day.startOf("day").hour(startHour);
const blockingStart = dayjs(event.start);
const eventEnd = dayjs(event.end);
const eventStart = dayStart.isAfter(blockingStart) ? dayStart : blockingStart;
if (!eventStart.isSame(day, "day")) {
return null;
}
if (eventStart.isBefore(dayjs())) {
if (eventEnd.isBefore(dayjs())) {
return null;
}
}
const eventDuration = eventEnd.diff(eventStart, "minutes");
const eventStartHour = eventStart.hour();
const eventStartDiff = (eventStartHour - (startHour || 0)) * 60;
return (
<div
key={`${eventStart.toISOString()}-${i}`}
className="absolute w-full"
style={{
zIndex: 60,
top: `calc(${eventStartDiff}*var(--one-minute-height))`,
height: `calc(${eventDuration}*var(--one-minute-height))`,
}}>
<BlockedTimeCell />
</div>
);
})}
</>
);
}

View File

@ -0,0 +1,15 @@
import { classNames } from "@calcom/lib";
export function BlockedTimeCell() {
return (
<div
className={classNames("group absolute inset-0 flex h-full flex-col hover:cursor-not-allowed")}
style={{
backgroundColor: "#D1D5DB",
opacity: 0.2,
background:
"repeating-linear-gradient( -45deg, #E5E7EB, #E5E7EB 4.5px, #D1D5DB 4.5px, #D1D5DB 22.5px )",
}}
/>
);
}

View File

@ -0,0 +1,69 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import {
Examples,
Example,
Note,
Title,
CustomArgsTable,
VariantsTable,
VariantRow,
} from "@calcom/storybook/components";
import { events, blockingDates } from "../_storybookData";
import "../styles/styles.css";
import { CalendarEvent } from "../types/events";
import { Calendar } from "./Calendar";
<Meta title="UI/Calendar" component={Calendar} />
<Title title="Calendar" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
## Props
The Args Table below shows you a breakdown of what props can be passed into the Calendar component. All props should have a desciption to make it self explanitory to see what is going on.
<CustomArgsTable of={Calendar} />
## Example
There will be a few examples of how to use the Calendar component to show different usecases.
export const Template = (args) => <Calendar {...args} />;
<Canvas>
<Story
name="Customising Start Hour and EndHour"
argTypes={{
startHour: {
control: { type: "number", min: 0, max: 23, step: 1 },
},
endHour: {
control: { type: "number", min: 0, max: 23, step: 1 },
},
}}
args={{
sortEvents: true,
startHour: 8,
endHour: 20,
events: events,
hoverEventDuration: 0,
blockingDates: blockingDates,
}}>
{Template.bind({})}
</Story>
</Canvas>
<Canvas>
<Story name="Onclick Handlers">
<Calendar
startHour={8}
endHour={17}
events={events}
onEventClick={(e) => alert(e.title)}
onEmptyCellClick={(date) => alert(date.toString())}
sortEvents
hoverEventDuration={30}
/>
</Story>
</Canvas>

View File

@ -0,0 +1,43 @@
import React, { useEffect, useState } from "react";
import dayjs from "@calcom/dayjs";
import { useCalendarStore } from "../../state/store";
type Props = {
containerNavRef: React.RefObject<HTMLDivElement>;
containerRef: React.RefObject<HTMLDivElement>;
containerOffsetRef: React.RefObject<HTMLDivElement>;
};
export function CurrentTime({ containerOffsetRef }: Props) {
const [currentTimePos, setCurrentTimePos] = useState<number>(0);
const { startHour, endHour } = useCalendarStore((state) => ({
startHour: state.startHour || 0,
endHour: state.endHour || 23,
}));
useEffect(() => {
// Set the container scroll position based on the current time.
let currentMinute = new Date().getHours() * 60;
currentMinute = currentMinute + new Date().getMinutes();
if (containerOffsetRef.current) {
const totalHours = endHour - startHour;
const currentTimePos = currentMinute;
setCurrentTimePos(currentTimePos);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startHour, endHour]);
return (
<div
className="absolute z-40 ml-3 flex h-1 items-center justify-center border-gray-800"
aria-hidden="true"
style={{ top: `calc(${currentTimePos}*var(--one-minute-height))`, zIndex: 100 }}>
{dayjs().format("HH:mm")}
<div className="ml-1 h-3 w-1 bg-gray-800" />
<div className="h-1 w-full bg-gray-800" />
</div>
);
}

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