Compare commits

...
Author SHA1 Message Date
emrysal 6838133865 chore: release v5.6.16
Post release / release (push) Canceled after 0s
2025-09-09 10:21:58 +00:00
Calcom BotandLingo.dev 4b2bb2271a feat: update translations via @LingoDotDev (#23703)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-09 06:47:06 -03:00
20b7ac8c0d feat: implement tiered Intercom chat system replacing Plain (#23285)
* feat: implement tiered Intercom chat system replacing Plain

- Add TieredIntercomChat component with customer tier detection
- Add IntercomContactForm for free users using Intercom API
- Add /api/intercom-conversation endpoint for free user support
- Update UserDropdown to use tiered chat logic
- Replace Plain chat with Intercom in app providers
- Remove all Plain chat components and related files
- Use useHasPaidPlan hook for customer tier detection

Paying customers see Intercom widget, free users see contact form
that creates conversations via Intercom API.

Co-Authored-By: peer@cal.com <peer@cal.com>

* fix: add missing environment variables to turbo.json globalEnv

- Add NEXT_PUBLIC_WEBAPP_URL, NEXT_PUBLIC_WEBSITE_URL, NEXT_PUBLIC_STRIPE_PUBLIC_KEY
- Add NEXT_PUBLIC_IS_E2E, NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLY
- Add NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD, NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID
- Resolves turbo/no-undeclared-env-vars ESLint errors blocking commits

Co-Authored-By: peer@cal.com <peer@cal.com>

* fix: resolve linting errors in platform examples base package

- Fix prettier/prettier formatting (quotes and comma)
- Add underscore prefix to unused variables
- Resolves CI failure in @calcom/base#lint check

Co-Authored-By: peer@cal.com <peer@cal.com>

* remove plain usage

* placement fixes

* fix: pop closing immediately issue

* stuff

* proper error and additional user info

* add key to .env.example and remove unused plain route

* fix conversation route

* refactor intercom dynamic provider

* code rabbit fixes

* feat: introduce tiered support feature flag

* fix: type check

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-09 18:20:22 +09:00
Calcom BotandLingo.dev ca7c432728 feat: update translations via @LingoDotDev (#23699)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-09 08:49:09 +00:00
SomandHariom Balhara 3b966191e3 fix: prevent footer doubling with CSS position absolute for grid layouts (#23368)
* fix: prevent footer doubling with CSS position absolute for grid layouts

* fix: use :host selector for Shadow DOM compatibility in footer doubling fix

* Add better comments and updae playground to be ab le to test it

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-09-09 08:13:15 +00:00
a7889984a0 fix: design improvements for log in screen for cal video (#22359)
* fix: design improvements for log in screen for cal video

Signed-off-by: bandhan-majumder <bandhanmajumder16@gmail.com>

* fix: type

Signed-off-by: bandhan-majumder <bandhanmajumder16@gmail.com>

* Update videos-single-view.tsx

* Update videos-single-view.tsx

* Update videos-single-view.tsx

* add: light mode

* Update videos-single-view.tsx

* add suggestions 

added padding change and removed hard coded values

* Update videos-single-view.tsx

* Update videos-single-view.tsx

* Update apps/web/modules/videos/views/videos-single-view.tsx

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* Update common.json

* Update videos-single-view.tsx

* remove isLoggedin from LogInOverlayProps

* type fix

* screen flash fixed

* rabbit suggestions

* rabbit suggestions

* Update videos-single-view.tsx

* Update videos-single-view.tsx

* Update videos-single-view.tsx

* flakey test

* Update videos-single-view.tsx

* Update videos-single-view.tsx

* Update videos-single-view.tsx

* add: deps

* coderabbit

---------

Signed-off-by: bandhan-majumder <bandhanmajumder16@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-09-09 07:44:29 +00:00
Anik Dhabal Babu 3c72d7aaa0 fix: Don’t allow hosts to cancel a booking without providing a reason through v1 (#23647)
* fix: hosts is cancelling booking through apiV1

* tweak
2025-09-09 05:30:51 +00:00
Romit ece0030978 fix (#23685) 2025-09-09 04:20:20 +00:00
Anik Dhabal Babu a26a769119 fix: send awaiting payment email only to the current seat (#23630) 2025-09-09 03:50:24 +00:00
Anik Dhabal Babu 3313de1eec fix: change internal note preset fkey to cascade (#23613) 2025-09-09 03:13:31 +00:00
Anik Dhabal Babu 7a6e16631b fix: guests are not adding when reschedule booking (#23620)
* fix: guests are not adding when reschedule booking

* Update addGuests.handler.ts

* add types
2025-09-09 03:12:41 +00:00
Anik Dhabal Babu ce4d71ba2e fix: workflow attach ics file is in base64 format (#23680) 2025-09-09 03:11:34 +00:00
300cd31efb chore: redis cache for calendars in api v2 (#23622)
* chore: implement redis cache for calendars api v2

* fixup! chore: implement redis cache for calendars api v2

* fixup! fixup! chore: implement redis cache for calendars api v2

* fix leverage genetic type of redis.get

* refactor: address supalarry's PR feedback

- Create CalendarsCacheService to separate cache responsibilities
- Rename cache methods to be more descriptive (getConnectedAndDestinationCalendarsCache, setConnectedAndDestinationCalendarsCache)
- Remove unused redisService injection from CalendarsController
- Move all cache-related functionality to dedicated service

Addresses comments 9, 10, and 11 from supalarry on PR #23622

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* refactor: address supalarry's latest PR feedback

- Remove redundant deleteConnectedAndDestinationCalendarsCache from CalendarsService
- Update controller to directly inject and use CalendarsCacheService
- Extract TTL constant and remove manual JSON.stringify in cache service

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: update IcsFeedService to use CalendarsCacheService directly

- Inject CalendarsCacheService into IcsFeedService constructor
- Replace calendarsService.deleteCalendarCache with calendarsCacheService.deleteConnectedAndDestinationCalendarsCache
- Completes the refactoring to eliminate all old cache method references

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: update destination-calendars and selected-calendars services to use CalendarsCacheService directly

- Inject CalendarsCacheService in both service constructors
- Replace calendarsService.deleteCalendarCache calls with calendarsCacheService.deleteConnectedAndDestinationCalendarsCache
- Add CalendarsCacheService to module providers for proper dependency injection
- Completes migration of all cache method references to use dedicated cache service

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* chore: use CalendarsCacheService

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-08 21:21:55 +00:00
Rajiv Sahal f5bb734441 fix: platform onboarding (#23581)
* fix: platform onboarding

* chore: better function naming

* fix: implement PR feedback
2025-09-08 19:04:33 +00:00
emrysal e33560ef82 chore: release v5.6.15
Post release / release (push) Canceled after 0s
2025-09-08 12:44:40 +00:00
devin-ai-integration[bot]andmorgan@cal.com 84f34b2244 feat: add 5000ms staleTime to useConnectedCalendars hook (#23674)
- Add staleTime: 5000 to useQuery configuration in useConnectedCalendars
- Prevents unnecessary refetches for 5 seconds after data becomes stale
- Improves performance by reducing redundant API calls

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
2025-09-08 12:19:22 +00:00
Benny Joo 086bae03d3 refactor: move a constant to a dedicated file in App store package (#23671)
* define DailyLocationType in app-store/locations.ts

* update imports
2025-09-08 08:38:46 -03:00
4a0a429774 refactor: Migrate getRoutedUrl from @calcom/lib to @calcom/features (#23666)
* mkdir

* update imports

* chore: update platform libraries

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
2025-09-08 11:28:22 +00:00
Benny Joo c4142ab4e7 refactor: Move handlePayment to Features Package (#23670)
* mv

* update imports
2025-09-08 07:32:29 -03:00
Benny Joo 95f8046db9 refactor: Error code - remove circular dep between app store <-> lib (#23669)
* remove stripe.types.ts

* use stripe-error defined in packages/lib

* refactor
2025-09-08 07:31:28 -03:00
Lauris Skraucis d8a87e9f04 fix: v2 seated bookings (#23514)
* fix: email manage link missing seat uid

* fix: toggle seated booking attendees

* fix: email manage link seat uid

* chore: update platform libraries
2025-09-08 09:50:36 +00:00
Keith Williams 86dcd6f0ff perf: Create 'heavy' eventTypes router (#23664)
* perf: Create 'heavy' eventTypes router

* Fixing type checks

* Fixing platform libraries imports
2025-09-08 08:24:30 +00:00
Anik Dhabal Babu 411a14dd3d fix: unit test flakiness (#23668)
* fix: unit test flakyness

* Update CalendarService.test.ts
2025-09-08 03:47:40 +00:00
Benny Joo 03bc8db64a refactor: Remove StripeService from @calcom/lib and define it in @calcom/trpc (#23655)
* remove packages/lib/server/stripe

* simplify metadata

* define business logic directly in trpc handler
2025-09-08 02:02:56 +00:00
Benny Joo fe7fd8bc79 chore: add an eslint rule to warn importing trpc package in app store package (#23665) 2025-09-08 10:36:34 +09:00
Benny Joo 92eb41c117 refactor: Migrate trpc routers in App store package to Trpc package (#23536)
* migrate trpc routers in app package to trpc package

* delete

* delete

* delete

* fix

* fix

* fix

* format

* format

* fix

* fix

* fix

* fix

* remove unused file

* fix

* fix

* wip
2025-09-07 22:17:33 -03:00
Benny Joo a1fca56aac refactor: Remove App Store's Type Dependency on Trpc Package (#23640)
* remove trpc type interface

* refactors

* refactors
2025-09-08 00:59:51 +00:00
212 changed files with 2435 additions and 3009 deletions
+3 -8
View File
@@ -103,19 +103,14 @@ NEXT_PUBLIC_INTERCOM_APP_ID=
# Secret to enable Intercom Identity Verification
INTERCOM_SECRET=
# Intercom api token for free users
INTERCOM_API_TOKEN=
# Posthog Config
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=
# plain.com config
PLAIN_API_KEY=
PLAIN_API_URL=https://api.plain.com/v1
PLAIN_HMAC_SECRET_KEY=
NEXT_PUBLIC_PLAIN_CHAT_ID=
PLAIN_CHAT_HMAC_SECRET_KEY=
NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS=
# Dub Config
+12
View File
@@ -19,5 +19,17 @@ module.exports = {
],
},
},
{
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"warn",
{
paths: ["@calcom/trpc"],
patterns: ["@calcom/trpc/*"],
},
],
},
},
],
};
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
View File
+1 -1
View File
@@ -43,7 +43,7 @@ export const bookingCancelSchema = z.object({
id: z.number(),
allRemainingBookings: z.boolean().optional(),
cancelSubsequentBookings: z.boolean().optional(),
cancellationReason: z.string().optional().default("Not Provided"),
cancellationReason: z.string().min(1).optional(),
seatReferenceUid: z.string().optional(),
cancelledBy: z.string().email({ message: "Invalid email" }).optional(),
internalNote: z
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.344",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.347",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -1,6 +1,7 @@
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
@@ -41,6 +42,7 @@ import { Module } from "@nestjs/common";
OAuthClientUsersService,
PlatformBookingsService,
CalendarsService,
CalendarsCacheService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
@@ -10,6 +10,7 @@ import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
@@ -71,6 +72,7 @@ import { Module } from "@nestjs/common";
ApiKeysRepository,
PlatformBookingsService,
CalendarsService,
CalendarsCacheService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
@@ -117,6 +117,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
slug: seatedEventTypeSlug,
length: 60,
seatsPerTimeSlot: 3,
seatsShowAttendees: true,
},
user.id
);
@@ -53,8 +53,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
let apiKeyString: string;
let seatedEventTypeId: number;
let seatedEventTypeIdAttendeesDisabledId: number;
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
const seatedEventSlugAttendeesDisabled = `seated-bookings-event-type-attendees-disabled-${randomString()}`;
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
@@ -121,6 +123,28 @@ describe("Bookings Endpoints 2024-08-13", () => {
);
seatedEventTypeId = seatedEvent.id;
const seatedEventAttendeesDisabled = await eventTypesRepositoryFixture.create(
{
title: `seated-bookings-2024-08-13-event-type-attendees-disabled-${randomString()}`,
slug: seatedEventSlugAttendeesDisabled,
length: 60,
seatsPerTimeSlot: 5,
seatsShowAttendees: false,
seatsShowAvailabilityCount: false,
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
metadata: {
disableStandardEmails: {
all: {
attendee: true,
host: true,
},
},
},
},
user.id
);
seatedEventTypeIdAttendeesDisabledId = seatedEventAttendeesDisabled.id;
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -414,6 +438,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should book an event type with attendees disabled", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
attendee: {
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: "Europe/Rome",
language: "it",
},
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.seatUid).toBeDefined();
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
expect(data.eventType).toEqual({
id: seatedEventTypeIdAttendeesDisabledId,
slug: seatedEventSlugAttendeesDisabled,
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected seated booking but received non array response"
);
}
});
});
describe("cancel seated booking", () => {
describe("cancel seated booking as attendee", () => {
it("should cancel seated booking", async () => {
@@ -576,10 +576,13 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputRecurringBooking(booking);
}
if (isRecurring && isSeated) {
return this.outputService.getOutputRecurringSeatedBooking(booking);
return this.outputService.getOutputRecurringSeatedBooking(
booking,
!!booking.eventType?.seatsShowAttendees
);
}
if (isSeated) {
return this.outputService.getOutputSeatedBooking(booking);
return this.outputService.getOutputSeatedBooking(booking, !!booking.eventType?.seatsShowAttendees);
}
return this.outputService.getOutputBooking(booking);
}
@@ -591,7 +594,10 @@ export class BookingsService_2024_08_13 {
const ids = recurringBooking.map((booking) => booking.id);
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
if (isRecurringSeated) {
return this.outputService.getOutputRecurringSeatedBookings(ids);
return this.outputService.getOutputRecurringSeatedBookings(
ids,
!!recurringBooking[0].eventType?.seatsShowAttendees
);
}
return this.outputService.getOutputRecurringBookings(ids);
@@ -657,9 +663,19 @@ export class BookingsService_2024_08_13 {
if (isRecurring && !isSeated) {
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
} else if (isRecurring && isSeated) {
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
formattedBookings.push(
this.outputService.getOutputRecurringSeatedBooking(
formatted,
!!formatted.eventType?.seatsShowAttendees
)
);
} else if (isSeated) {
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted));
formattedBookings.push(
await this.outputService.getOutputSeatedBooking(
formatted,
!!formatted.eventType?.seatsShowAttendees
)
);
} else {
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
}
@@ -68,6 +68,7 @@ type DatabaseBooking = Booking & {
eventType: {
id: number;
slug: string;
seatsShowAttendees?: boolean | null;
} | null;
attendees: {
name: string;
@@ -167,6 +168,7 @@ export class OutputBookingsService_2024_08_13 {
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
if (databaseMetadata === null) return {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
return userDefinedMetadata;
@@ -266,11 +268,14 @@ export class OutputBookingsService_2024_08_13 {
databaseBooking: DatabaseBooking,
seatUid: string
): Promise<CreateSeatedBookingOutput_2024_08_13> {
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking);
const getSeatedBookingOutput = await this.getOutputSeatedBooking(
databaseBooking,
!!databaseBooking.eventType?.seatsShowAttendees
);
return { ...getSeatedBookingOutput, seatUid };
}
async getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
async getOutputSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
@@ -317,41 +322,43 @@ export class OutputBookingsService_2024_08_13 {
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
parsed.attendees = databaseBooking.attendees.map((attendee) => {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData,
false
);
parsed.attendees = showAttendees
? databaseBooking.attendees.map((attendee) => {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData,
false
);
const attendeeData = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
seatUid: attendee.bookingSeat?.referenceUid,
bookingFieldsResponses: {},
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
const attendeeData = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
seatUid: attendee.bookingSeat?.referenceUid,
bookingFieldsResponses: {},
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
return attendeeParsed;
});
return attendeeParsed;
})
: [];
return parsed;
}
async getOutputRecurringSeatedBookings(bookingsIds: number[]) {
async getOutputRecurringSeatedBookings(bookingsIds: number[], showAttendees: boolean) {
const transformed = [];
for (const bookingId of bookingsIds) {
@@ -361,7 +368,7 @@ export class OutputBookingsService_2024_08_13 {
throw new Error(`Booking with id=${bookingId} was not found in the database`);
}
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking));
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking, showAttendees));
}
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
@@ -386,11 +393,14 @@ export class OutputBookingsService_2024_08_13 {
databaseBooking: DatabaseBooking,
seatUid: string
): CreateRecurringSeatedBookingOutput_2024_08_13 {
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(databaseBooking);
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(
databaseBooking,
!!databaseBooking.eventType?.seatsShowAttendees
);
return { ...getRecurringSeatedBookingOutput, seatUid };
}
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking) {
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking, showAttendees: boolean) {
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
const duration = dateEnd.diff(dateStart, "minutes").minutes;
@@ -430,35 +440,37 @@ export class OutputBookingsService_2024_08_13 {
});
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
parsed.attendees = databaseBooking.attendees.map((attendee) => {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData,
false
);
parsed.attendees = showAttendees
? databaseBooking.attendees.map((attendee) => {
const { responses } = safeParse(
seatedBookingDataSchema,
attendee.bookingSeat?.data,
defaultSeatedBookingData,
false
);
const attendeeData = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
seatUid: attendee.bookingSeat?.referenceUid,
bookingFieldsResponses: {},
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
return attendeeParsed;
});
const attendeeData = {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: !!attendee.noShow,
seatUid: attendee.bookingSeat?.referenceUid,
bookingFieldsResponses: {},
};
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
attendeeParsed.bookingFieldsResponses = responses || {};
attendeeParsed.metadata = safeParse(
seatedBookingMetadataSchema,
attendee.bookingSeat?.metadata,
defaultSeatedBookingMetadata,
false
);
// note(Lauris): as of now email is not returned for privacy
delete attendeeParsed.bookingFieldsResponses.email;
return attendeeParsed;
})
: [];
return parsed;
}
@@ -3,6 +3,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
@@ -10,16 +11,18 @@ import { OutlookService } from "@/ee/calendars/services/outlook.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, UsersModule, TokensModule],
imports: [PrismaModule, UsersModule, TokensModule, RedisModule],
providers: [
CredentialsRepository,
CalendarsService,
CalendarsCacheService,
OutlookService,
GoogleCalendarService,
AppleCalendarService,
@@ -9,6 +9,7 @@ import {
DeletedCalendarCredentialsOutputDto,
} from "@/ee/calendars/outputs/delete-calendar-credentials.output";
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service";
import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
@@ -79,6 +80,7 @@ const calendarStateSchema = z.object({
export class CalendarsController {
constructor(
private readonly calendarsService: CalendarsService,
private readonly calendarsCacheService: CalendarsCacheService,
private readonly outlookService: OutlookService,
private readonly googleCalendarService: GoogleCalendarService,
private readonly appleCalendarService: AppleCalendarService,
@@ -307,6 +309,8 @@ export class CalendarsController {
credentialId
);
this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(user.id);
return {
status: SUCCESS_STATUS,
data: plainToClass(
@@ -0,0 +1,36 @@
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable } from "@nestjs/common";
import type { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/platform-libraries";
export const REDIS_CALENDARS_CACHE_KEY = (userId: number) => `apiv2:user:${userId}:calendars`;
export const CALENDARS_CACHE_TTL_MS = 10_000;
type ConnectedDestinationCalendars = Awaited<
ReturnType<typeof getConnectedDestinationCalendarsAndEnsureDefaultsInDb>
>;
@Injectable()
export class CalendarsCacheService {
constructor(private readonly redisService: RedisService) {}
async deleteConnectedAndDestinationCalendarsCache(userId: number) {
await this.redisService.del(REDIS_CALENDARS_CACHE_KEY(userId));
}
async getConnectedAndDestinationCalendarsCache(userId: number) {
const cachedResult = await this.redisService.get<ConnectedDestinationCalendars>(
REDIS_CALENDARS_CACHE_KEY(userId)
);
return cachedResult;
}
async setConnectedAndDestinationCalendarsCache(
userId: number,
calendars: ConnectedDestinationCalendars
): Promise<void> {
await this.redisService.set<ConnectedDestinationCalendars>(REDIS_CALENDARS_CACHE_KEY(userId), calendars, {
ttl: CALENDARS_CACHE_TTL_MS,
});
}
}
@@ -1,4 +1,5 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import {
CredentialsRepository,
@@ -37,7 +38,8 @@ export class CalendarsService {
private readonly appsRepository: AppsRepository,
private readonly calendarsRepository: CalendarsRepository,
private readonly dbWrite: PrismaWriteService,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly calendarsCacheService: CalendarsCacheService
) {}
private buildNonDelegationCredentials<TCredential>(credentials: TCredential[]) {
@@ -52,11 +54,17 @@ export class CalendarsService {
}
async getCalendars(userId: number) {
const cachedResult = await this.calendarsCacheService.getConnectedAndDestinationCalendarsCache(userId);
if (cachedResult) {
return cachedResult;
}
const userWithCalendars = await this.usersRepository.findByIdWithCalendars(userId);
if (!userWithCalendars) {
throw new NotFoundException("User not found");
}
return getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
const result = await getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
user: {
...userWithCalendars,
allSelectedCalendars: userWithCalendars.selectedCalendars,
@@ -68,6 +76,10 @@ export class CalendarsService {
eventTypeId: null,
prisma: this.dbWrite.prisma as unknown as PrismaClient,
});
console.log("saving cache", JSON.stringify(result));
await this.calendarsCacheService.setConnectedAndDestinationCalendarsCache(userId, result);
return result;
}
async getBusyTimes(
@@ -187,6 +199,8 @@ export class CalendarsService {
userId,
calendarType
);
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);
}
async checkCalendarCredentialValidity(userId: number, credentialId: number, type: string) {
@@ -1,7 +1,9 @@
import { ICSFeedCalendarApp } from "@/ee/calendars/calendars.interface";
import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
@@ -13,7 +15,9 @@ import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
export class IcsFeedService implements ICSFeedCalendarApp {
constructor(
private readonly calendarsService: CalendarsService,
private readonly credentialRepository: CredentialsRepository
private readonly calendarsCacheService: CalendarsCacheService,
private readonly credentialRepository: CredentialsRepository,
private readonly redisService: RedisService
) {}
private logger = new Logger("IcsFeedService");
@@ -58,6 +62,9 @@ export class IcsFeedService implements ICSFeedCalendarApp {
data.key,
userId
);
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);
return {
status: SUCCESS_STATUS,
data: {
@@ -1,4 +1,5 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
@@ -11,6 +12,7 @@ import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersService } from "@/modules/users/services/users.service";
@@ -18,7 +20,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule],
imports: [PrismaModule, MembershipsModule, TokensModule, SelectedCalendarsModule, RedisModule],
providers: [
EventTypesRepository_2024_06_14,
EventTypesService_2024_06_14,
@@ -29,6 +31,7 @@ import { Module } from "@nestjs/common";
SchedulesRepository_2024_06_11,
EventTypeResponseTransformPipe,
CalendarsService,
CalendarsCacheService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
+4 -1
View File
@@ -1,4 +1,5 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GcalController } from "@/ee/gcal/gcal.controller";
import { AppsRepository } from "@/modules/apps/apps.repository";
@@ -6,6 +7,7 @@ import { GCalService } from "@/modules/apps/services/gcal.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersRepository } from "@/modules/users/users.repository";
@@ -13,7 +15,7 @@ import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Module({
imports: [PrismaModule, TokensModule, OAuthClientModule],
imports: [PrismaModule, TokensModule, OAuthClientModule, RedisModule],
providers: [
AppsRepository,
ConfigService,
@@ -21,6 +23,7 @@ import { ConfigService } from "@nestjs/config";
SelectedCalendarsRepository,
GCalService,
CalendarsService,
CalendarsCacheService,
UsersRepository,
CalendarsRepository,
],
@@ -1,5 +1,6 @@
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
@@ -8,13 +9,14 @@ import { GoogleCalendarService } from "@/modules/cal-unified-calendars/services/
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [TokensModule],
imports: [TokensModule, RedisModule],
providers: [
GCalService,
GoogleCalendarService,
@@ -22,6 +24,7 @@ import { Module } from "@nestjs/common";
BookingReferencesRepository_2024_08_13,
CredentialsRepository,
CalendarsService,
CalendarsCacheService,
SelectedCalendarsRepository,
PrismaReadService,
PrismaWriteService,
@@ -1,4 +1,5 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
@@ -6,15 +7,17 @@ import { DestinationCalendarsController } from "@/modules/destination-calendars/
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
imports: [PrismaModule, RedisModule],
providers: [
CalendarsRepository,
CalendarsService,
CalendarsCacheService,
DestinationCalendarsService,
DestinationCalendarsRepository,
UsersRepository,
@@ -1,4 +1,5 @@
import { ConnectedCalendar, Calendar } from "@/ee/calendars/outputs/connected-calendars.output";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
@@ -7,6 +8,7 @@ import { Injectable, NotFoundException } from "@nestjs/common";
export class DestinationCalendarsService {
constructor(
private readonly calendarsService: CalendarsService,
private readonly calendarsCacheService: CalendarsCacheService,
private readonly destinationCalendarsRepository: DestinationCalendarsRepository
) {}
@@ -60,6 +62,7 @@ export class DestinationCalendarsService {
delegatedCalendar ? delegationCredentialId : undefined
);
this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(userId);
return {
userId: updatedCalendarUserId,
integration: updatedCalendarIntegration,
@@ -1,4 +1,5 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
@@ -49,6 +50,7 @@ import { JwtService } from "@nestjs/jwt";
TokensRepository,
OAuthFlowService,
CalendarsService,
CalendarsCacheService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
@@ -1,4 +1,5 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
@@ -7,6 +8,7 @@ import { OrganizationsMembershipRepository } from "@/modules/organizations/membe
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsController } from "@/modules/selected-calendars/controllers/selected-calendars.controller";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { SelectedCalendarsService } from "@/modules/selected-calendars/services/selected-calendars.service";
@@ -14,11 +16,12 @@ import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
imports: [PrismaModule, RedisModule],
providers: [
SelectedCalendarsRepository,
CalendarsRepository,
CalendarsService,
CalendarsCacheService,
UsersRepository,
CredentialsRepository,
AppsRepository,
@@ -1,3 +1,4 @@
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
@@ -23,6 +24,7 @@ type SelectedCalendarsInputDelegationCredential = SelectedCalendarsInputDto & {
export class SelectedCalendarsService {
constructor(
private readonly calendarsService: CalendarsService,
private readonly calendarsCacheService: CalendarsCacheService,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly organizationsMembershipService: OrganizationsMembershipService,
private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository
@@ -50,6 +52,8 @@ export class SelectedCalendarsService {
credentialId
);
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(user.id);
return userSelectedCalendar;
}
@@ -122,7 +126,7 @@ export class SelectedCalendarsService {
externalId,
delegationCredentialId
);
await this.calendarsCacheService.deleteConnectedAndDestinationCalendarsCache(user.id);
return removedCalendarEntry;
} catch (error) {
if (error instanceof Error) {
+6 -5
View File
@@ -9132,8 +9132,6 @@
{
"name": "credentialId",
"in": "query",
"description": "The ID of the user's calendar credential used to access the calendar. Retrieve this from your connected calendar credentials via the `/v2/credentials` endpoint.",
"example": 135,
"required": true,
"schema": {
"type": "number"
@@ -9142,8 +9140,6 @@
{
"name": "externalId",
"in": "query",
"description": "The external calendar identifier from the calendar provider (e.g., email address for Google Calendar). This is returned when listing connected calendars.",
"example": "user@gmail.com",
"required": true,
"schema": {
"type": "string"
@@ -23642,7 +23638,12 @@
"afterEvent",
"rescheduleEvent",
"afterHostsCalVideoNoShow",
"afterGuestsCalVideoNoShow"
"afterGuestsCalVideoNoShow",
"bookingRejected",
"bookingRequested",
"bookingPaymentInitiated",
"bookingPaid",
"bookingNoShowUpdated"
]
],
"description": "Trigger type for the workflow"
@@ -2,8 +2,6 @@ import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { z } from "zod";
import { StripeService } from "@calcom/lib/server/service/stripe";
import VerifyPage from "~/auth/verify-view";
const querySchema = z.object({
@@ -14,18 +12,10 @@ const querySchema = z.object({
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const p = { ...(await params), ...(await searchParams) };
const { sessionId, stripeCustomerId } = querySchema.parse(p);
const data = await StripeService.getCheckoutSession({
stripeCustomerId,
checkoutSessionId: sessionId,
});
const { hasPaymentFailed } = data;
const { sessionId } = querySchema.parse(p);
return await _generateMetadata(
() =>
hasPaymentFailed ? "Your payment failed" : sessionId ? "Payment successful!" : `Verify your email`,
() => (sessionId ? "Payment Page" : `Verify your email`),
() => "",
undefined,
undefined,
+6 -1
View File
@@ -15,6 +15,7 @@ const resolveEndpoint = (links: any) => {
// to the correct API endpoints.
// - viewer.me - 2 segment paths like this are for logged in requests
// - viewer.public.i18n - 3 segments paths can be public or authed
// - viewer.eventTypes.heavy.create - 4 segments paths for heavy sub-router
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ctx: any) => {
const parts = ctx.op.path.split(".");
@@ -23,7 +24,11 @@ const resolveEndpoint = (links: any) => {
if (parts.length == 2) {
endpoint = parts[0] as keyof typeof links;
path = parts[1];
} else {
} else if (parts.length >= 3 && parts[2] === "heavy") {
endpoint = parts[1] + "/heavy" as keyof typeof links;
path = parts[3];
}
else {
endpoint = parts[1] as keyof typeof links;
path = parts.splice(2, parts.length - 2).join(".");
}
-546
View File
@@ -1,546 +0,0 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { createHmac } from "crypto";
import { headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma from "@calcom/prisma";
import { userMetadata } from "@calcom/prisma/zod-utils";
import { CardComponent } from "@lib/plain/card-components";
const customerCardDisplay = (
name: string,
email: string,
id: string,
username: string,
timeZone: string,
emailVerified: Date | null,
twoFactorEnabled: boolean | null,
identityProvider: string | null,
lastActiveAt: Date | null,
teamName: string | null,
teamSlug: string | null,
isOrganization: boolean | null,
stripeCustomerId: string | null
): Card => {
return {
key: "customer-card",
timeToLiveSeconds: null,
components: [
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentText: {
text: "Basic Information",
textColor: "NORMAL",
textSize: "L",
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Name",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: name || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Email",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: email || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Username",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: username || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "User ID",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: id || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentDivider: {
dividerSpacingSize: "M",
},
},
// Account Settings Section Title
{
componentText: {
text: "Account Settings",
textColor: "NORMAL",
textSize: "L",
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Time Zone",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: timeZone || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Identity Provider",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: identityProvider || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Last Active At",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: lastActiveAt ? dayjs(lastActiveAt).fromNow() : "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentDivider: {
dividerSpacingSize: "M",
},
},
// Team Information Section Title
{
componentText: {
text: "Team Information",
textColor: "NORMAL",
textSize: "L",
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Team Name",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: teamName || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Team Slug",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: teamSlug || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Is Organization",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentBadge: {
badgeLabel: isOrganization ? "Yes" : "No",
badgeColor: isOrganization ? "GREEN" : "RED",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentDivider: {
dividerSpacingSize: "M",
},
},
{
componentText: {
text: "Security & Billing",
textColor: "NORMAL",
textSize: "M",
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Stripe Customer ID",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: stripeCustomerId || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Email Verified?",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentBadge: {
badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No",
badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Two Factor Enabled?",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentBadge: {
badgeLabel: twoFactorEnabled === null ? "No" : twoFactorEnabled ? "Yes" : "No",
badgeColor: twoFactorEnabled === null ? "RED" : twoFactorEnabled ? "GREEN" : "RED",
},
},
],
},
},
],
};
};
const Card = z.object({
key: z.string(),
timeToLiveSeconds: z.number().int().min(0).nullish().default(null),
components: z.array(CardComponent).nullable(),
});
type Card = z.infer<typeof Card>;
const cardExamples: ((
name: string,
email: string,
id: string,
username: string,
timeZone: string,
emailVerified: Date | null,
twoFactorEnabled: boolean | null,
identityProvider: string | null,
lastActiveAt: Date | null,
teamName: string | null,
teamSlug: string | null,
isOrganization: boolean | null,
stripeCustomerId: string | null
) => Card)[] = [customerCardDisplay];
const inputSchema = z.object({
customer: z.object({
name: z.string().optional(),
email: z.string().email(),
username: z.string().optional(),
timeZone: timeZoneSchema.optional(),
emailVerified: z.boolean().optional(),
identityProvider: z.string().optional(),
twoFactorEnabled: z.boolean().optional(),
lastActiveAt: z.string().optional(),
teamName: z.string().optional(),
teamSlug: z.string().optional(),
isOrganization: z.boolean().optional(),
stripeCustomerId: z.string().optional(),
}),
cardKeys: z.array(z.string()),
});
async function handler(request: NextRequest) {
const headersList = await headers();
const requestBody = await request.json();
// HMAC verification
const incomingSignature = headersList.get("plain-request-signature");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const expectedSignature = createHmac("sha-256", process.env.PLAIN_HMAC_SECRET_KEY!)
.update(JSON.stringify(requestBody))
.digest("hex");
if (incomingSignature !== expectedSignature) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Validate request body
const { cardKeys, customer } = inputSchema.parse(requestBody);
const userRepo = new UserRepository(prisma);
const user = await userRepo.findByEmail({ email: customer.email });
if (!user) {
return NextResponse.json({
cards: [
{
key: "customer-card",
timeToLiveSeconds: null,
components: [
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentText: {
textSize: "L",
textColor: "MUTED",
text: "User does not exist!",
},
},
],
},
],
});
}
// Fetch team details including userId and team name
const teamMemberships = await userRepo.findTeamsByUserId({ userId: user.id });
const firstTeam = teamMemberships.teams[0] ?? null;
// Parse user metadata
const parsedMetadata = userMetadata.parse(user.metadata);
const cards = await Promise.all(
cardExamples.map(async (cardFn) => {
return cardFn(
user.name || "Unknown",
user.email,
user.id.toString(),
user.username || "Unknown",
user.timeZone,
user.emailVerified,
user.twoFactorEnabled,
user.identityProvider,
user.lastActiveAt,
firstTeam?.name || "Unknown",
firstTeam?.slug || "Unknown",
firstTeam?.isOrganization || false,
(user.metadata as { stripeCustomerId?: string })?.stripeCustomerId || "Unknown"
);
})
);
const filteredCards = cards.filter((card) => {
return cardKeys.length === 0 || cardKeys.includes(card.key);
});
return NextResponse.json({
cards: filteredCards,
user: {
...user,
metadata: parsedMetadata,
},
});
}
export const POST = defaultResponderForAppDir(handler);
-88
View File
@@ -1,88 +0,0 @@
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
import { createHmac } from "crypto";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { IS_PLAIN_CHAT_ENABLED } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const responseSchema = z.object({
hash: z.string(),
email: z.string().email(),
shortName: z.string(),
appId: z.string(),
fullName: z.string(),
chatAvatarUrl: z.string(),
userTier: z.enum(["free", "teams", "enterprise"]),
});
async function handler() {
// Early return if Plain Chat is not enabled
if (!IS_PLAIN_CHAT_ENABLED) {
return NextResponse.json({ error: "Plain Chat is not enabled" }, { status: 404 });
}
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized - No session email found" }, { status: 401 });
}
const secret = process.env.PLAIN_CHAT_HMAC_SECRET_KEY;
if (!secret) {
return NextResponse.json({ error: "Missing Plain Chat secret" }, { status: 500 });
}
// Get user's team membership info
const user = await prisma.user.findUnique({
where: { id: session.user.id },
include: {
teams: {
select: {
team: {
select: {
isOrganization: true,
},
},
},
},
},
});
// Check if user is part of a team and determine tier
let userTier = "free";
if (user?.teams.length) {
const teamMemberships = user.teams;
const isEnterprise = teamMemberships.some((membership) => membership.team.isOrganization === true);
const isTeams = user?.teams.length > 0;
userTier = isEnterprise ? "enterprise" : isTeams ? "teams" : "free";
}
const hmac = createHmac("sha256", secret);
hmac.update(session.user.email.toLowerCase().trim());
const hash = hmac.digest("hex");
const shortName =
(session.user.name?.split(" ")[0] || session.user.email).charAt(0).toUpperCase() +
(session.user.name?.split(" ")[0] || session.user.email).slice(1) || "User";
const response = responseSchema.parse({
hash,
email: session.user.email || "user@example.com",
shortName,
appId: process.env.NEXT_PUBLIC_PLAIN_CHAT_ID,
fullName: session.user.name || "User",
chatAvatarUrl: session.user.avatarUrl || "",
userTier,
});
return NextResponse.json(response);
}
export const POST = defaultResponderForAppDir(handler);
@@ -1,358 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { POST } from "../route";
vi.mock("@calcom/features/auth/lib/getServerSession", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@calcom/lib/constants", async () => {
const actual = (await vi.importActual("@calcom/lib/constants")) as typeof import("@calcom/lib/constants");
return {
...actual,
IS_PRODUCTION: true,
IS_PLAIN_CHAT_ENABLED: true,
};
});
vi.mock("@lib/buildLegacyCtx", () => ({
buildLegacyRequest: vi.fn(() => ({ headers: {}, cookies: {} })),
}));
vi.mock("next/headers", () => ({
headers: vi.fn(() => new Map()),
cookies: vi.fn(() => ({ getAll: () => [] })),
}));
vi.mock("next/server", () => ({
NextResponse: {
json: vi.fn((data, options) => ({
json: () => Promise.resolve(data),
status: options?.status || 200,
})),
},
}));
vi.mock("@lib/plain/plain", () => {
const mockGetCustomerByEmail = vi.fn();
const mockCreateThread = vi.fn();
const mockUpsertPlainCustomer = vi.fn();
return {
plain: {
getCustomerByEmail: mockGetCustomerByEmail,
createThread: mockCreateThread,
},
upsertPlainCustomer: mockUpsertPlainCustomer,
};
});
const mockGetServerSession = vi.mocked(getServerSession);
describe("/api/support", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.PLAIN_API_KEY = "test-api-key";
});
it("should return 404 when Plain Chat is disabled", async () => {
expect(true).toBe(true);
});
it("should return 401 when user is not authenticated", async () => {
mockGetServerSession.mockResolvedValue(null);
const request = new Request("http://localhost:3000/api/support", {
method: "POST",
body: JSON.stringify({
message: "Test message",
attachmentIds: [],
}),
});
const response = await POST(request);
expect(response.status).toBe(401);
});
it("should return 500 for invalid form data", async () => {
mockGetServerSession.mockResolvedValue({
hasValidLicense: true,
upId: "test-up-id",
expires: "2025-12-31T23:59:59.999Z",
user: { id: 123, email: "test@example.com" },
});
const request = new Request("http://localhost:3000/api/support", {
method: "POST",
body: JSON.stringify({
message: "",
attachmentIds: [],
}),
});
const response = await POST(request);
expect(response.status).toBe(500);
});
it("should return 500 when Plain API key is not configured", async () => {
delete process.env.PLAIN_API_KEY;
mockGetServerSession.mockResolvedValue({
hasValidLicense: true,
upId: "test-up-id",
expires: "2025-12-31T23:59:59.999Z",
user: { id: 123, email: "test@example.com" },
});
const request = new Request("http://localhost:3000/api/support", {
method: "POST",
body: JSON.stringify({
message: "Test message",
attachmentIds: [],
}),
});
const response = await POST(request);
expect(response.status).toBe(500);
});
it("should successfully create customer and thread", async () => {
mockGetServerSession.mockResolvedValue({
hasValidLicense: true,
upId: "test-up-id",
expires: "2025-12-31T23:59:59.999Z",
user: { id: 123, email: "test@example.com" },
});
const { plain } = vi.mocked(await import("@lib/plain/plain"));
vi.mocked(plain.getCustomerByEmail).mockResolvedValue({
data: {
id: "customer-123",
__typename: "Customer",
fullName: "Test User",
shortName: "Test",
externalId: "123",
email: {
email: "test@example.com",
isVerified: true,
verifiedAt: {
__typename: "DateTime",
iso8601: "2025-01-01T00:00:00Z",
unixTimestamp: "1735689600",
},
},
company: null,
createdBy: { __typename: "SystemActor" as const, systemId: "system" },
updatedAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
createdAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
markedAsSpamAt: null,
},
});
vi.mocked(plain.createThread).mockResolvedValue({
data: {
id: "thread-123",
__typename: "Thread",
externalId: null,
title: "Test message",
description: null,
status: "Todo" as any,
statusChangedAt: {
__typename: "DateTime",
iso8601: "2025-01-01T00:00:00Z",
unixTimestamp: "1735689600",
},
statusDetail: null,
customer: { id: "customer-123" },
priority: 0,
previewText: "Test message",
updatedAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
createdAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
} as any,
});
const request = new Request("http://localhost:3000/api/support", {
method: "POST",
body: JSON.stringify({
message: "Test message",
attachmentIds: [],
}),
});
const response = await POST(request);
expect(response.status).toBe(200);
const responseData = await response.json();
expect(responseData).toBeDefined();
expect(vi.mocked(plain.getCustomerByEmail)).toHaveBeenCalledWith({ email: "test@example.com" });
expect(vi.mocked(plain.createThread)).toHaveBeenCalled();
});
it("should handle form submission with file attachments", async () => {
mockGetServerSession.mockResolvedValue({
hasValidLicense: true,
upId: "test-up-id",
expires: "2025-12-31T23:59:59.999Z",
user: { id: 123, email: "test@example.com" },
});
const { plain, upsertPlainCustomer } = vi.mocked(await import("@lib/plain/plain"));
vi.mocked(plain.getCustomerByEmail).mockResolvedValue({
data: null,
});
vi.mocked(upsertPlainCustomer).mockResolvedValue({
data: {
result: "Created" as any,
customer: {
id: "customer-456",
__typename: "Customer",
fullName: "Test User",
shortName: "Test",
externalId: "123",
email: {
email: "test@example.com",
isVerified: true,
verifiedAt: {
__typename: "DateTime",
iso8601: "2025-01-01T00:00:00Z",
unixTimestamp: "1735689600",
},
},
company: null,
createdBy: { __typename: "SystemActor" as const, systemId: "system" },
updatedAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
createdAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
markedAsSpamAt: null,
},
},
});
vi.mocked(plain.createThread).mockResolvedValue({
data: {
id: "thread-456",
__typename: "Thread",
externalId: null,
title: "Test message",
description: null,
status: "Todo" as any,
statusChangedAt: {
__typename: "DateTime",
iso8601: "2025-01-01T00:00:00Z",
unixTimestamp: "1735689600",
},
statusDetail: null,
customer: { id: "customer-456" },
priority: 0,
previewText: "Test message",
updatedAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
createdAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
} as any,
});
const request = new Request("http://localhost:3000/api/support", {
method: "POST",
body: JSON.stringify({
message: "Test message",
attachmentIds: ["attachment-123"],
}),
});
const response = await POST(request);
expect(response.status).toBe(200);
const responseData = await response.json();
expect(responseData).toBeDefined();
expect(vi.mocked(plain.getCustomerByEmail)).toHaveBeenCalledWith({ email: "test@example.com" });
expect(vi.mocked(upsertPlainCustomer)).toHaveBeenCalled();
expect(vi.mocked(plain.createThread)).toHaveBeenCalled();
});
it("should handle Plain customer creation error", async () => {
mockGetServerSession.mockResolvedValue({
hasValidLicense: true,
upId: "test-up-id",
expires: "2025-12-31T23:59:59.999Z",
user: { id: 123, email: "test@example.com" },
});
const { plain, upsertPlainCustomer } = vi.mocked(await import("@lib/plain/plain"));
vi.mocked(plain.getCustomerByEmail).mockResolvedValue({
data: null,
});
vi.mocked(upsertPlainCustomer).mockResolvedValue({
error: {
type: "unknown",
message: "Customer creation failed",
},
});
const request = new Request("http://localhost:3000/api/support", {
method: "POST",
body: JSON.stringify({
message: "Test message",
attachmentIds: [],
}),
});
const response = await POST(request);
expect(response.status).toBe(500);
});
it("should handle Plain thread creation error", async () => {
mockGetServerSession.mockResolvedValue({
hasValidLicense: true,
upId: "test-up-id",
expires: "2025-12-31T23:59:59.999Z",
user: { id: 123, email: "test@example.com" },
});
const { plain } = vi.mocked(await import("@lib/plain/plain"));
vi.mocked(plain.getCustomerByEmail).mockResolvedValue({
data: {
id: "customer-123",
__typename: "Customer",
fullName: "Test User",
shortName: "Test",
externalId: "123",
email: {
email: "test@example.com",
isVerified: true,
verifiedAt: {
__typename: "DateTime",
iso8601: "2025-01-01T00:00:00Z",
unixTimestamp: "1735689600",
},
},
company: null,
createdBy: { __typename: "SystemActor" as const, systemId: "system" },
updatedAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
createdAt: { __typename: "DateTime", iso8601: "2025-01-01T00:00:00Z", unixTimestamp: "1735689600" },
markedAsSpamAt: null,
},
});
vi.mocked(plain.createThread).mockResolvedValue({
error: {
type: "unknown",
message: "Thread creation failed",
},
});
const request = new Request("http://localhost:3000/api/support", {
method: "POST",
body: JSON.stringify({
message: "Test message",
attachmentIds: [],
}),
});
const response = await POST(request);
expect(response.status).toBe(500);
});
});
@@ -0,0 +1,122 @@
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { Contact } from "@calcom/features/ee/support/lib/intercom/intercom";
import { intercom } from "@calcom/features/ee/support/lib/intercom/intercom";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const log = logger.getSubLogger({ prefix: [`/api/support/conversation`] });
export async function POST(req: NextRequest) {
const headersList = await headers();
const cookiesList = await cookies();
const legacyReq = buildLegacyRequest(headersList, cookiesList);
const session = await getServerSession({ req: legacyReq });
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized - No session found" }, { status: 401 });
}
const intercomApiKey = process.env.INTERCOM_API_TOKEN;
if (!intercomApiKey) {
return NextResponse.json({ error: "Intercom API key not configured" }, { status: 500 });
}
let contact: Contact;
const existingContact = await intercom.getContactByEmail(session.user.email);
if (existingContact.error) {
return NextResponse.json(
{ error: existingContact?.error ?? "Error fetching intercom contact for user" },
{ status: 502 }
);
}
const { user } = session;
if (!existingContact.data) {
const additionalUserInfo = await new UserRepository(prisma).getUserStats({ userId: session.user.id });
const sumOfTeamEventTypes = additionalUserInfo?.teams.reduce(
(sum, team) => sum + team.team.eventTypes.length,
0
);
const newContact = await intercom.createContact({
email: session.user.email,
external_id: session.user.id.toString(),
name: session.user.name ?? session.user.email,
type: "contact",
custom_attributes: {
user_name: user?.username,
link: `${WEBSITE_URL}/${encodeURIComponent(user?.username ?? "")}`,
admin_link: `${WEBAPP_URL}/settings/admin/users/${user?.id}/edit`,
impersonate_user: `${WEBAPP_URL}/settings/admin/impersonation?username=${encodeURIComponent(
user?.email ?? user?.username ?? ""
)}`,
locale: user?.locale,
completed_onboarding: user?.completedOnboarding,
is_logged_in: !!user,
has_orgs_plan: !!user?.org,
organization: user?.org?.slug,
sum_of_bookings: additionalUserInfo?._count?.bookings,
sum_of_calendars: additionalUserInfo?._count?.userLevelSelectedCalendars,
sum_of_teams: additionalUserInfo?._count?.teams,
sum_of_event_types: additionalUserInfo?._count?.eventTypes,
sum_of_team_event_types: sumOfTeamEventTypes,
},
});
if (newContact.error || !newContact.data) {
return NextResponse.json(
{ error: newContact.error ?? "Error creating contact from email" },
{ status: 500 }
);
}
contact = newContact.data;
} else {
contact = existingContact.data;
}
if (!contact) {
return NextResponse.json({ error: "No contactId found" }, { status: 404 });
}
try {
const { message } = await req.json();
if (!message.trim()) {
return NextResponse.json({ error: "Cannot start a conversation without message" }, { status: 400 });
}
const conversation = await intercom.createConversation({
body: message,
from: {
id: contact.id,
type: "user",
},
});
if (conversation.error) {
return NextResponse.json(
{ error: conversation.error ?? "Error creating conversation" },
{ status: 400 }
);
}
return NextResponse.json({ message: "Created conversation in Intercom" }, { status: 200 });
} catch (err) {
console.error(err);
log.error(`Error creating Intercom conversation:`, safeStringify(err));
return NextResponse.json({ error: "Unexpected error occurred" }, { status: 500 });
}
}
-95
View File
@@ -1,95 +0,0 @@
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { IS_PLAIN_CHAT_ENABLED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { plain, upsertPlainCustomer } from "@lib/plain/plain";
const contactFormSchema = z.object({
message: z.string().min(1, "Message is required"),
attachmentIds: z.array(z.string()).optional(),
});
const log = logger.getSubLogger({ prefix: [`/api/support`] });
export async function POST(req: Request) {
if (!IS_PLAIN_CHAT_ENABLED) {
return NextResponse.json({ error: "Plain Chat is not enabled" }, { status: 404 });
}
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized - No session found" }, { status: 401 });
}
try {
const body = await req.json();
const { message, attachmentIds } = contactFormSchema.parse(body);
const plainApiKey = process.env.PLAIN_API_KEY;
if (!plainApiKey) {
return NextResponse.json({ error: "Plain API key not configured" }, { status: 500 });
}
let plainCustomerId: string | null = null;
const plainCustomer = await plain.getCustomerByEmail({ email: session.user.email });
if (plainCustomer.data) {
plainCustomerId = plainCustomer.data.id;
} else {
const { data, error } = await upsertPlainCustomer({
email: session.user.email,
id: session.user.id,
name: session.user.name,
});
if (error) {
log.error(`Error submitting plain contact form: `, safeStringify(error));
return NextResponse.json(
{
message: error.message,
},
{ status: 500 }
);
}
if (data) {
plainCustomerId = data.customer.id;
}
}
if (!plainCustomerId) {
return NextResponse.json({ message: "Plain customer not found" }, { status: 404 });
}
const { data, error } = await plain.createThread({
customerIdentifier: {
customerId: plainCustomerId,
},
components: [
{
componentText: {
text: message,
},
},
],
attachmentIds,
});
if (error) {
log.error("Error creating plain contact form thread: ", safeStringify(error));
return NextResponse.json({ message: error.message }, { status: 500 });
}
return NextResponse.json(data);
} catch (err) {
log.error(`Error submitting plain contact form: `, safeStringify(err));
return NextResponse.json({ message: "Unexpected error occured" }, { status: 500 });
}
}
-84
View File
@@ -1,84 +0,0 @@
import { AttachmentType } from "@team-plain/typescript-sdk";
import { cookies, headers } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { IS_PLAIN_CHAT_ENABLED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { plain, upsertPlainCustomer } from "@lib/plain/plain";
const log = logger.getSubLogger({ prefix: ["/api/support/upload"] });
/**
* Returns a signed url from plain to upload the attachment
*/
export async function GET(req: NextRequest) {
if (!IS_PLAIN_CHAT_ENABLED) {
return NextResponse.json({ error: "Plain Chat is not enabled" }, { status: 404 });
}
const searchParams = req.nextUrl.searchParams;
const name = searchParams.get("name");
const size = searchParams.get("size");
if (!name || !size) {
return NextResponse.json({ error: "Missing required parameters: name and size" }, { status: 400 });
}
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized - No session found" }, { status: 401 });
}
let plainCustomerId: string | null = null;
const plainCustomer = await plain.getCustomerByEmail({
email: session.user.email,
});
if (plainCustomer.data) {
plainCustomerId = plainCustomer.data.id;
} else {
const { data, error } = await upsertPlainCustomer({
name: session.user.name,
email: session.user.email,
id: session.user.id,
});
if (error) {
log.error("Error getting customer info: ", safeStringify(error));
return NextResponse.json({ error: error.message }, { status: 500 });
}
if (data) {
plainCustomerId = data.customer.id;
}
}
if (!plainCustomerId) {
return NextResponse.json({ error: "Plain customer not found" }, { status: 404 });
}
const { data, error } = await plain.createAttachmentUploadUrl({
customerId: plainCustomerId,
fileName: name,
fileSizeBytes: parseInt(size),
attachmentType: AttachmentType.CustomTimelineEntry,
});
if (error) {
log.error(`Error getting signed url for attachment upload: `, safeStringify(error));
return NextResponse.json(
{
error: error.message,
},
{ status: 500 }
);
}
return NextResponse.json(data);
}
-5
View File
@@ -2,14 +2,12 @@
import { TrpcProvider } from "app/_trpc/trpc-provider";
import { SessionProvider } from "next-auth/react";
import { usePathname } from "next/navigation";
import CacheProvider from "react-inlinesvg/provider";
import { WebPushProvider } from "@calcom/features/notifications/WebPushContext";
import { NotificationSoundHandler } from "@calcom/web/components/notification-sound-handler";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
import PlainChat from "@lib/plain/dynamicProvider";
type ProvidersProps = {
isEmbed: boolean;
@@ -18,13 +16,10 @@ type ProvidersProps = {
};
export function Providers({ isEmbed, children, nonce }: ProvidersProps) {
const isBookingPage = useIsBookingPage();
const pathname = usePathname();
const isOnboardingPage = pathname?.startsWith("/getting-started");
return (
<SessionProvider>
<TrpcProvider>
{!isBookingPage && !isOnboardingPage ? <PlainChat nonce={nonce} /> : null}
{!isEmbed && !isBookingPage && <NotificationSoundHandler />}
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
<CacheProvider>
@@ -37,7 +37,7 @@ const UserProfile = ({ user }: UserProfileProps) => {
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
const utils = trpc.useUtils();
const router = useRouter();
const createEventType = trpc.viewer.eventTypes.create.useMutation();
const createEventType = trpc.viewer.eventTypes.heavy.create.useMutation();
const telemetry = useTelemetry();
const [firstRender, setFirstRender] = useState(true);
@@ -1,236 +0,0 @@
"use client";
import { Dispatch, SetStateAction, useState } from "react";
import { Button } from "@calcom/ui/components/button";
import { FileUploader, type FileData } from "@calcom/ui/components/file-uploader";
import { Label, TextArea } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Popover, PopoverContent, PopoverTrigger } from "@calcom/ui/components/popover";
import { showToast } from "@calcom/ui/components/toast";
const PlainContactForm = ({ open, setIsOpen }: { open: boolean, setIsOpen: Dispatch<SetStateAction<boolean>> }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [isUploadingImage, setIsUploadingImage] = useState(false);
const [data, setData] = useState<{
message: string;
attachmentIds: string[];
}>({
message: "",
attachmentIds: [],
});
const [uploads, setUploads] = useState<
{
attachmentId?: string;
uploading: boolean;
file: File;
id: string;
}[]
>([]);
const handleUpload = async (allFiles: FileData[], newFiles: FileData[], removedFiles: FileData[]) => {
if (newFiles.length > 0) {
const newFile = newFiles[0];
setUploads((prev) => [...prev, { file: newFile.file, uploading: true, id: newFile.id }]);
setIsUploadingImage(true);
const { file } = newFile;
const res = await fetch(`/api/support/upload?name=${file.name}&size=${file.size}`);
if (!res.ok) {
showToast("Error uploading attachment", "error");
setIsUploadingImage(false);
return;
}
const {
uploadFormUrl,
uploadFormData,
attachment: { id: attachmentId },
} = await res.json();
setIsUploadingImage(false);
const formData = new FormData();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
uploadFormData.forEach(({ key, value }: any) => {
formData.append(key, value);
});
formData.append("file", file);
const uploadRes = await fetch(uploadFormUrl, {
method: "POST",
body: formData,
});
if (!uploadRes.ok) {
showToast(`Failed while uploading file: ${uploadRes.text}`, "error");
setIsUploadingImage(false);
return;
}
setUploads((prev) =>
prev.map((upload) => (upload.file === file ? { ...upload, uploading: false, attachmentId } : upload))
);
setData((prev) => ({
...prev,
attachmentIds: [...prev.attachmentIds, attachmentId],
}));
setIsUploadingImage(false);
showToast("File uploaded successfully", "success");
} else if (removedFiles.length > 0) {
const removedFile = removedFiles[0];
const file = uploads.find((upload) => upload.id === removedFile.id);
if (!file) {
console.warn("File not found in uploads: ", removedFile.id);
return;
}
setData((prev) => ({
...prev,
attachmentIds: prev.attachmentIds.filter((id) => id !== file.attachmentId),
}));
setUploads((prev) => prev.filter((upload) => upload.id !== removedFile.id));
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
const response = await fetch("/api/support", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json();
showToast(errorData.message ?? "Failed to submit contact form", "error");
setIsSubmitting(false);
return;
}
setIsSubmitted(true);
setIsSubmitting(false);
setData({
message: "",
attachmentIds: [],
});
} catch (err) {
showToast(err instanceof Error ? err.message : "An error occurred", "error");
setIsSubmitting(false);
}
};
const resetForm = () => {
setIsSubmitted(false);
setData({
message: "",
attachmentIds: [],
});
setUploads([]);
};
return (
<div className="fixed bottom-[1rem] right-[1rem] z-50">
<Popover open={open} onOpenChange={setIsOpen}>
<PopoverTrigger asChild className="enabled:hover:bg-subtle bg-subtle shadow-none">
<Button
onClick={() => setIsOpen(true)}
className="bg-subtle text-emphasis flex h-12 w-12 items-center justify-center rounded-full border-none">
<Icon name="message-circle" className="h-6 w-6" />
</Button>
</PopoverTrigger>
<PopoverContent
style={{ maxWidth: "450px", maxHeight: "650px" }}
className="!bg-muted no-scrollbar mb-2 mr-8 w-[450px] overflow-hidden overflow-y-scroll px-6 py-4">
<div className="flex w-full justify-between">
<p className="mb-5 text-lg font-semibold">Contact support</p>
<Button
color="minimal"
variant="button"
StartIcon="x"
size="sm"
onClick={() => setIsOpen(false)}
/>
</div>
<div>
{isSubmitted ? (
<div className="py-4 text-center">
<h4 className="mb-2 text-lg font-medium ">Message Sent</h4>
<p className="text-subtle mb-4 text-sm">
Thank you for contacting us. We&apos;ll get back to you as soon as possible.
</p>
<Button color="primary" className="my-2" onClick={resetForm} variant="button" size="base">
Send Another Message
</Button>
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<Label htmlFor="message">Describe the issue</Label>
<TextArea
id="message"
name="message"
value={data.message}
onChange={(e) => setData((prev) => ({ ...prev, message: e.target.value }))}
placeholder="Please describe the issue you're facing, e.g. 'Busy slots are marked available', ..., etc."
required
rows={4}
/>
</div>
<div>
<Label>Attachments (optional)</Label>
<FileUploader
id="contact-attachments"
buttonMsg="Add Files"
onFilesChange={handleUpload}
acceptedFileTypes={["images", "videos"]}
multiple={false}
showFilesList
maxFiles={5}
maxFileSize={10 * 1024 * 1024}
disabled={isSubmitting || isUploadingImage}
testId="contact-form-file-upload"
/>
</div>
<div className="mt-4 flex w-full items-center">
<Button
color="secondary"
variant="button"
type="submit"
disabled={isSubmitting || isUploadingImage}
className="w-full">
<div className="flex w-full justify-center">
{isSubmitting ? (
<div className="flex items-center">
<Icon name="loader" className="mr-2 h-4 w-4 animate-spin rounded-full" />
Sending
</div>
) : (
<>
<Icon name="send" className="mr-2 h-4 w-4" />
Send Message
</>
)}
</div>
</Button>
</div>
</form>
)}
</div>
</PopoverContent>
</Popover>
</div>
);
};
export default PlainContactForm;
@@ -1,181 +0,0 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { useSession } from "next-auth/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { showToast } from "@calcom/ui/components/toast";
import PlainContactForm from "../PlainContactForm";
vi.mock("next-auth/react", () => ({
useSession: vi.fn(),
}));
vi.mock("@calcom/ui/components/toast", () => ({
showToast: vi.fn(),
}));
const mockUseSession = vi.mocked(useSession);
const mockShowToast = vi.mocked(showToast);
const mockFetch = vi.fn();
global.fetch = mockFetch as unknown as typeof fetch;
describe("PlainContactForm", () => {
let mockSetIsOpen: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
mockSetIsOpen = vi.fn();
mockUseSession.mockReturnValue({
data: {
hasValidLicense: true,
upId: "test-up-id",
expires: "2025-12-31T23:59:59.999Z",
user: {
id: 123,
name: "Test User",
email: "test@example.com",
},
},
status: "authenticated",
update: vi.fn(),
} as any);
});
it("renders the contact button when closed", () => {
render(<PlainContactForm open={false} setIsOpen={mockSetIsOpen} />);
const button = screen.getByRole("button");
expect(button).toBeInTheDocument();
});
it("opens when the launcher button is clicked (calls setIsOpen(true))", () => {
render(<PlainContactForm open={false} setIsOpen={mockSetIsOpen} />);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(mockSetIsOpen).toHaveBeenCalledWith(true);
});
it("shows the form when open=true", () => {
render(<PlainContactForm open={true} setIsOpen={mockSetIsOpen} />);
expect(screen.getByText("Contact support")).toBeInTheDocument();
expect(screen.getByLabelText("Describe the issue")).toBeInTheDocument();
expect(screen.getByText("Attachments (optional)")).toBeInTheDocument();
});
it("shows empty form initially", () => {
render(<PlainContactForm open={true} setIsOpen={mockSetIsOpen} />);
const messageInput = screen.getByLabelText("Describe the issue") as HTMLTextAreaElement;
expect(messageInput.value).toBe("");
});
it("closes when X button is clicked (calls setIsOpen(false))", () => {
render(<PlainContactForm open={true} setIsOpen={mockSetIsOpen} />);
const buttons = screen.getAllByRole("button");
const closeButton = buttons.find((button) => button.querySelector('svg use[href="#x"]'));
expect(closeButton).toBeDefined();
fireEvent.click(closeButton!);
expect(mockSetIsOpen).toHaveBeenCalledWith(false);
});
it("handles form submission successfully", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true }),
});
render(<PlainContactForm open={true} setIsOpen={mockSetIsOpen} />);
fireEvent.change(screen.getByLabelText("Describe the issue"), {
target: { value: "Test message" },
});
const submitButton = screen.getByRole("button", { name: /send message/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText("Message Sent")).toBeInTheDocument();
});
expect(mockFetch).toHaveBeenCalledWith("/api/support", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Test message",
attachmentIds: [],
}),
});
});
it("shows loading state during submission", async () => {
// never resolves → stays loading
mockFetch.mockImplementation(() => new Promise((_resolve) => {}));
render(<PlainContactForm open={true} setIsOpen={mockSetIsOpen} />);
fireEvent.change(screen.getByLabelText("Describe the issue"), {
target: { value: "Test message" },
});
const submitButton = screen.getByRole("button", { name: /send message/i });
fireEvent.click(submitButton);
expect(screen.getByText("Sending")).toBeInTheDocument();
await expect(submitButton).toBeDisabled();
});
it("resets form after successful submission", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true }),
});
render(<PlainContactForm open={true} setIsOpen={mockSetIsOpen} />);
fireEvent.change(screen.getByLabelText("Describe the issue"), {
target: { value: "Test message" },
});
fireEvent.click(screen.getByRole("button", { name: /send message/i }));
await waitFor(() => {
expect(screen.getByText("Message Sent")).toBeInTheDocument();
});
const sendAnotherButton = screen.getByRole("button", { name: /send another message/i });
fireEvent.click(sendAnotherButton);
await waitFor(() => {
const messageInput = screen.getByLabelText("Describe the issue") as HTMLTextAreaElement;
expect(messageInput.value).toBe("");
});
});
it("handles missing user session", () => {
mockUseSession.mockReturnValue({
data: null,
status: "unauthenticated",
update: vi.fn(),
} as any);
render(<PlainContactForm open={true} setIsOpen={mockSetIsOpen} />);
const messageInput = screen.getByLabelText("Describe the issue") as HTMLTextAreaElement;
expect(messageInput.value).toBe("");
});
it("requires message field", async () => {
render(<PlainContactForm open={true} setIsOpen={mockSetIsOpen} />);
await expect(screen.getByLabelText("Describe the issue")).toHaveAttribute("required");
});
});
-327
View File
@@ -1,327 +0,0 @@
"use client";
import { useSession } from "next-auth/react";
import { usePathname, useSearchParams } from "next/navigation";
import Script from "next/script";
import { useEffect, useState, useCallback, useMemo } from "react";
import { IS_PLAIN_CHAT_ENABLED } from "@calcom/lib/constants";
import PlainContactForm from "./PlainContactForm";
declare global {
interface Window {
Plain?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
init: (config: any) => void;
open: () => void;
};
plainScriptLoaded?: () => void;
__PLAIN_CONFIG__?: PlainChatConfig;
}
}
interface PlainChatConfig {
appId: string;
customerDetails: {
email: string;
emailHash: string;
fullName: string;
shortName: string;
chatAvatarUrl: string;
};
links: Array<{
icon: string;
text: string;
url: string;
}>;
chatButtons: Array<{
icon: string;
text: string;
threadDetails?: {
labelTypeIds: Array<string>;
tierIdentifier: {
externalId: string;
};
};
form?: {
fields: Array<{
type: string;
placeholder: string;
options: Array<{
icon: string;
text: string;
threadDetails: {
labelTypeIds: Array<string>;
tierIdentifier: {
externalId: string;
};
};
onClick?: () => void;
}>;
}>;
};
}>;
entryPoint: {
type: string;
};
hideBranding: boolean;
theme: string;
style: {
brandColor: string;
launcherBackgroundColor: string;
launcherIconColor: string;
};
position: {
zIndex: string;
bottom: string;
right: string;
};
}
const PlainChat = IS_PLAIN_CHAT_ENABLED
? ({ nonce }: { nonce: string | undefined }) => {
const [config, setConfig] = useState<PlainChatConfig | null>(null);
const [isSmallScreen, setIsSmallScreen] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const { data: session } = useSession();
const pathname = usePathname();
const searchParams = useSearchParams();
const shouldOpenPlain = pathname === "/event-types" && searchParams?.has("openPlain");
const userEmail = session?.user?.email;
const isPaidUser = session?.user.belongsToActiveTeam || !!session?.user.org;
const isAppDomain = useMemo(() => {
const restrictedPathsSet = new Set(
(process.env.NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS?.split(",") || []).map((path) => path.trim())
);
const pathSegments = pathname?.split("/").filter(Boolean) || [];
return (
typeof window !== "undefined" &&
window.location.origin === process.env.NEXT_PUBLIC_WEBAPP_URL &&
!pathSegments.some((segment) => restrictedPathsSet.has(segment))
);
}, [pathname]);
const checkScreenSize = useCallback(() => {
if (typeof window === "undefined") return;
const isSmall = window.innerWidth < 768;
setIsSmallScreen(isSmall);
if (isSmall && window.Plain) {
const plainElement = document.querySelector("#plain-container");
plainElement?.remove();
window.Plain = undefined;
} else if (!isSmall && window.Plain === undefined) {
window.plainScriptLoaded?.();
}
}, []);
const initConfig = useCallback(async () => {
if (!userEmail) return;
// Check if Plain Chat is enabled
if (!IS_PLAIN_CHAT_ENABLED) {
return;
}
try {
const response = await fetch("/api/plain-hash", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to generate hash: ${errorText}`);
}
const data = await response.json();
if (!data.hash || !data.email || !data.appId) {
throw new Error("Missing required fields in API response");
}
const plainChatConfig: PlainChatConfig = {
appId: data.appId,
customerDetails: {
email: data.email,
shortName: data.shortName,
fullName: data.fullName,
emailHash: data.hash,
chatAvatarUrl: data.chatAvatarUrl,
},
links: [
{
icon: "book",
text: "Documentation",
url: "https://cal.com/docs",
},
{
icon: "chat",
text: "Ask the community",
url: "https://github.com/calcom/cal.com/discussions",
},
],
chatButtons: [
{
icon: "bulb",
text: "Send feedback",
threadDetails: {
labelTypeIds: ["lt_01JFJWP3KECF1YQES6XF212RFW"],
tierIdentifier: { externalId: data.userTier },
},
},
{
icon: "error",
text: "Report an issue",
form: {
fields: [
{
type: "dropdown",
placeholder: "Select severity...",
options: [
{
icon: "support",
text: "I'm unable to use the app",
threadDetails: {
labelTypeIds: ["lt_01JFJWNWAC464N8DZ6YE71YJRF"],
tierIdentifier: { externalId: data.userTier },
},
},
{
icon: "error",
text: "Major functionality degraded",
threadDetails: {
labelTypeIds: ["lt_01JFJWP3KECF1YQES6XF212RFW"],
tierIdentifier: { externalId: data.userTier },
},
},
{
icon: "bug",
text: "Minor annoyance",
threadDetails: {
labelTypeIds: ["lt_01JFJWPC8ADW0PK28JHMJR6NSS"],
tierIdentifier: { externalId: data.userTier },
},
},
],
},
],
},
},
],
entryPoint: {
type: "chat",
},
hideBranding: true,
theme: "auto",
style: {
brandColor: "#FFFFFF",
launcherBackgroundColor: "#262626",
launcherIconColor: "#FFFFFF",
},
position: {
zIndex: "1",
bottom: "20px",
right: "20px",
},
};
plainChatConfig.chatButtons.push({
icon: "chat",
text: "Ask a question",
threadDetails: {
labelTypeIds: ["lt_01JFJWNWAC464N8DZ6YE71YJRF"],
tierIdentifier: { externalId: data.userTier },
},
});
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") {
window.__PLAIN_CONFIG__ = plainChatConfig;
}
setConfig(plainChatConfig);
if (shouldOpenPlain) {
const timer = setTimeout(() => {
if (window.Plain) {
window.Plain.open();
}
}, 100);
return () => clearTimeout(timer);
}
} catch (error) {
console.error("Failed to initialize Plain Chat:", error);
}
}, [userEmail, shouldOpenPlain]);
useEffect(() => {
if (!isAppDomain) return;
// Skip initialization if Plain Chat is not enabled
if (!IS_PLAIN_CHAT_ENABLED) return;
checkScreenSize();
window.addEventListener("resize", checkScreenSize);
if (isPaidUser) {
initConfig();
} else if (typeof window !== "undefined" && !window.Plain) {
window.Plain = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
init: () => {},
open: () => {
setIsOpen(true);
}
};
}
return () => window.removeEventListener("resize", checkScreenSize);
}, [isAppDomain, checkScreenSize, initConfig, userEmail, isPaidUser]);
const plainChatScript = `
window.plainScriptLoaded = function() {
if (window.Plain && ${Boolean(config)}) {
try {
Plain.init(${config ? JSON.stringify(config) : null});
} catch (error) {
console.error("Failed to initialize Plain:", error);
}
}
}
`;
if (!isAppDomain || isSmallScreen || typeof window === "undefined") return null;
if (!isPaidUser) {
return <PlainContactForm open={isOpen} setIsOpen={setIsOpen} />;
}
if (!config) return null;
return (
<>
<Script
nonce={nonce}
id="plain-chat"
src="https://chat.cdn-plain.com/index.js"
strategy="afterInteractive"
onLoad={() => window.plainScriptLoaded?.()}
/>
<Script
nonce={nonce}
id="plain-chat-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{ __html: plainChatScript }}
/>
</>
);
}
: () => null;
export default PlainChat;
@@ -3,11 +3,16 @@ import { useCheckTeamBilling } from "@calcom/web/lib/hooks/settings/platform/bil
import { usePlatformMe } from "./usePlatformMe";
export const useGetUserAttributes = () => {
const { data: platformUser, isLoading: isPlatformUserLoading } = usePlatformMe();
const { data: userBillingData, isFetching: isUserBillingDataLoading } = useCheckTeamBilling(
platformUser?.organizationId,
platformUser?.organization?.isPlatform ?? false
);
const {
data: platformUser,
isLoading: isPlatformUserLoading,
refetch: refetchPlatformUser,
} = usePlatformMe();
const {
data: userBillingData,
isFetching: isUserBillingDataLoading,
refetch: refetchTeamBilling,
} = useCheckTeamBilling(platformUser?.organizationId, platformUser?.organization?.isPlatform ?? false);
const isPlatformUser = platformUser?.organization?.isPlatform ?? false;
const isPaidUser = userBillingData?.valid;
const userOrgId = platformUser?.organizationId;
@@ -19,5 +24,7 @@ export const useGetUserAttributes = () => {
isPaidUser,
userBillingData,
userOrgId,
refetchTeamBilling,
refetchPlatformUser,
};
};
@@ -137,7 +137,7 @@ export const OAuthClientForm = ({
color="minimal"
variant="icon"
StartIcon="plus"
className="text-default mx-2 mb-2"
className="text-default mx-2"
disabled={isFormDisabled}
onClick={() => {
append({ uri: "" });
@@ -150,7 +150,7 @@ export const OAuthClientForm = ({
color="destructive"
variant="icon"
StartIcon="trash"
className="text-default mx-2 mb-2"
className="text-default mx-2"
disabled={isFormDisabled}
onClick={() => {
remove(index);
+8 -1
View File
@@ -10,6 +10,7 @@ import { usePathname, useSearchParams } from "next/navigation";
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks";
@@ -119,7 +120,13 @@ const AppProviders = (props: PageWrapperProps) => {
isThemeSupported={isThemeSupported}
isBookingPage={props.isBookingPage || isBookingPage}>
<FeatureFlagsProvider>
<OrgBrandProvider>{props.children}</OrgBrandProvider>
{props.isBookingPage || isBookingPage ? (
<OrgBrandProvider>{props.children}</OrgBrandProvider>
) : (
<DynamicIntercomProvider>
<OrgBrandProvider>{props.children}</OrgBrandProvider>
</DynamicIntercomProvider>
)}
</FeatureFlagsProvider>
</CalcomThemeProvider>
</TooltipProvider>
+12 -1
View File
@@ -15,6 +15,7 @@ import { useEffect } from "react";
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks";
@@ -271,6 +272,10 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) {
const AppProviders = (props: AppPropsWithChildren) => {
const isBookingPage = useIsBookingPage();
const _isBookingPage =
(typeof props.Component.isBookingPage === "function"
? props.Component.isBookingPage({ router: props.router })
: props.Component.isBookingPage) || isBookingPage;
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
@@ -282,7 +287,13 @@ const AppProviders = (props: AppPropsWithChildren) => {
isBookingPage={props.Component.isBookingPage || isBookingPage}
router={props.router}>
<FeatureFlagsProvider>
<OrgBrandProvider>{props.children}</OrgBrandProvider>
{_isBookingPage ? (
<OrgBrandProvider>{props.children}</OrgBrandProvider>
) : (
<DynamicIntercomProvider>
<OrgBrandProvider>{props.children}</OrgBrandProvider>
</DynamicIntercomProvider>
)}
</FeatureFlagsProvider>
</CalcomThemeProvider>
</TooltipProvider>
@@ -0,0 +1,12 @@
import { useEffect } from "react";
export function useExternalRedirectHandler(callback: (referrer: string) => void) {
useEffect(() => {
const referrer = document.referrer;
const isExternal = referrer && !referrer.includes(window.location.hostname);
if (isExternal) {
callback(referrer);
}
}, [callback]);
}
-86
View File
@@ -1,86 +0,0 @@
import { z } from "zod";
export const ComponentTextSize = z.enum(["S", "M", "L"]);
export type ComponentTextSize = z.infer<typeof ComponentTextSize>;
export const ComponentTextColor = z.enum(["NORMAL", "MUTED", "SUCCESS", "WARNING", "ERROR"]);
export type ComponentTextColor = z.infer<typeof ComponentTextColor>;
export const ComponentSpacerSize = z.enum(["XS", "S", "M", "L", "XL"]);
export type ComponentSpacerSize = z.infer<typeof ComponentSpacerSize>;
export const ComponentDividerSpacingSize = z.enum(["XS", "S", "M", "L", "XL"]);
export type ComponentDividerSpacingSize = z.infer<typeof ComponentDividerSpacingSize>;
export const ComponentBadgeColor = z.enum(["GREY", "GREEN", "YELLOW", "RED", "BLUE"]);
export type ComponentBadgeColor = z.infer<typeof ComponentBadgeColor>;
const Text = z.object({
textSize: ComponentTextSize.nullish(),
textColor: ComponentTextColor.nullish(),
text: z.string().min(1).max(5000),
});
const Divider = z.object({
dividerSpacingSize: ComponentDividerSpacingSize.nullish(),
});
const LinkButton = z.object({
linkButtonUrl: z.string().url(),
linkButtonLabel: z.string().max(500),
});
const Spacer = z.object({
spacerSize: ComponentSpacerSize,
});
const Badge = z.object({
badgeLabel: z.string().max(500),
badgeColor: ComponentBadgeColor.nullish(),
});
const CopyButton = z.object({
copyButtonValue: z.string().max(1000),
copyButtonTooltipLabel: z.string().max(500).nullish(),
});
const RowContentUnionInput = z.object({
componentText: Text.optional(),
componentDivider: Divider.optional(),
componentLinkButton: LinkButton.optional(),
componentSpacer: Spacer.optional(),
componentBadge: Badge.optional(),
componentCopyButton: CopyButton.optional(),
});
const Row = z.object({
rowMainContent: z.array(RowContentUnionInput),
rowAsideContent: z.array(RowContentUnionInput),
});
const ContainerContentUnionInput = z.object({
componentText: Text.optional(),
componentDivider: Divider.optional(),
componentLinkButton: LinkButton.optional(),
componentSpacer: Spacer.optional(),
componentBadge: Badge.optional(),
componentCopyButton: CopyButton.optional(),
componentRow: Row.optional(),
});
const Container = z.object({
containerContent: z.array(ContainerContentUnionInput).min(1),
});
export const CardComponent = z.object({
componentText: Text.optional(),
componentDivider: Divider.optional(),
componentLinkButton: LinkButton.optional(),
componentSpacer: Spacer.optional(),
componentBadge: Badge.optional(),
componentCopyButton: CopyButton.optional(),
componentRow: Row.optional(),
componentContainer: Container.optional(),
});
export type CardComponent = z.infer<typeof CardComponent>;
-7
View File
@@ -1,7 +0,0 @@
import dynamic from "next/dynamic";
import { Fragment } from "react";
// Preload caused by dynamic import doesn't seem to add nonce and thus preload fails but the functionality still works - https://github.com/vercel/next.js/issues/81260
export default process.env.NEXT_PUBLIC_PLAIN_CHAT_ID
? dynamic(() => import("../../components/plain/plainChat"))
: Fragment;
-32
View File
@@ -1,32 +0,0 @@
import { PlainClient } from "@team-plain/typescript-sdk";
export const plain = new PlainClient({
apiKey: process.env.PLAIN_API_KEY || "",
});
type PlainUser = {
name?: string | null;
email: string;
id: number;
};
export const upsertPlainCustomer = async (user: PlainUser) => {
const fullName = user.name ?? user.email;
const shortName = user.name ?? user.email?.split("@")[0];
const email = user.email;
return await plain.upsertCustomer({
identifier: {
emailAddress: email,
},
onCreate: {
fullName,
shortName,
email: {
email,
isVerified: true,
},
},
onUpdate: {},
});
};
@@ -177,7 +177,7 @@ const OnboardingPage = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventTypeGroups]);
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
const updateMutation = trpc.viewer.eventTypes.heavy.update.useMutation({
onSuccess: async (data) => {
showToast(t("event_type_updated_successfully", { eventTypeTitle: data.eventType?.title }), "success");
},
@@ -268,7 +268,7 @@ export const InfiniteEventTypeList = ({
},
});
const setHiddenMutation = trpc.viewer.eventTypes.update.useMutation({
const setHiddenMutation = trpc.viewer.eventTypes.heavy.update.useMutation({
onMutate: async (data) => {
await utils.viewer.eventTypes.getEventTypesFromGroup.cancel();
const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({
@@ -19,10 +19,9 @@ interface CtaRowProps {
declare global {
interface Window {
Plain?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
init: (config: any) => void;
Support?: {
open: () => void;
shouldShowTriggerButton: (showTrigger: boolean) => void;
};
}
}
@@ -71,8 +70,8 @@ const BillingView = () => {
: `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
const onContactSupportClick = async () => {
if (window.Plain) {
window.Plain.open();
if (window.Support) {
window.Support.open();
}
};
@@ -20,10 +20,9 @@ import { CtaRow } from "~/settings/billing/billing-view";
declare global {
interface Window {
Plain?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
init: (config: any) => void;
Support?: {
open: () => void;
shouldShowTriggerButton: (showTrigger: boolean) => void;
};
}
}
@@ -35,8 +34,8 @@ export default function PlatformBillingUpgrade() {
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
const onContactSupportClick = async () => {
if (window.Plain) {
window.Plain.open();
if (window.Support) {
window.Support.open();
}
};
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
@@ -1,12 +1,14 @@
"use client";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import { useState, useEffect, useCallback } from "react";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { showToast } from "@calcom/ui/components/toast";
import { useExternalRedirectHandler } from "@lib/hooks/settings/platform/billing/useExternalRedirectHandler";
import { useDeleteOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/useDeleteOAuthClient";
import { useOAuthClients } from "@lib/hooks/settings/platform/oauth-clients/useOAuthClients";
@@ -20,13 +22,22 @@ const queryClient = new QueryClient();
export default function Platform() {
const { t } = useLocale();
const [initialClientId, setInitialClientId] = useState("");
const [initialClientName, setInitialClientName] = useState("");
const [_initialClientId, setInitialClientId] = useState("");
const [_initialClientName, setInitialClientName] = useState("");
const pathname = usePathname();
const { data, isLoading: isOAuthClientLoading, refetch: refetchClients } = useOAuthClients();
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, isPaidUser, userBillingData, userOrgId } =
useGetUserAttributes();
const {
isUserLoading,
isUserBillingDataLoading,
isPlatformUser,
isPaidUser,
userBillingData,
userOrgId,
refetchTeamBilling,
refetchPlatformUser,
} = useGetUserAttributes();
const { mutateAsync, isPending: isDeleting } = useDeleteOAuthClient({
onSuccess: () => {
@@ -39,11 +50,24 @@ export default function Platform() {
await mutateAsync({ id: id });
};
const refetchBillingState = useCallback(() => {
refetchTeamBilling();
refetchPlatformUser();
}, [refetchTeamBilling, refetchPlatformUser]);
useEffect(() => {
setInitialClientId(data[0]?.id);
setInitialClientName(data[0]?.name);
}, [data]);
useEffect(() => {
refetchBillingState();
}, [pathname, refetchTeamBilling, refetchPlatformUser, refetchBillingState]);
useExternalRedirectHandler(() => {
refetchBillingState();
});
if (isUserLoading || isOAuthClientLoading) return <div className="m-5">{t("loading")}</div>;
if (isUserBillingDataLoading && !userBillingData) {
@@ -4,7 +4,7 @@ import type { DailyCall } from "@daily-co/daily-js";
import DailyIframe from "@daily-co/daily-js";
import { DailyProvider } from "@daily-co/daily-react";
import { useDailyEvent } from "@daily-co/daily-react";
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import dayjs from "@calcom/dayjs";
import { WEBSITE_URL } from "@calcom/lib/constants";
@@ -17,6 +17,7 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
import { Input } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import type { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
@@ -26,6 +27,7 @@ import { CalVideoPremiumFeatures } from "../cal-video-premium-features";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function JoinCall(props: PageProps) {
const { t } = useLocale();
const {
meetingUrl,
meetingPassword,
@@ -42,88 +44,144 @@ export default function JoinCall(props: PageProps) {
rediectAttendeeToOnExit,
} = props;
const [daily, setDaily] = useState<DailyCall | null>(null);
const [userNameForCall, setUserNameForCall] = useState<string | undefined>(
overrideName ?? loggedInUserName ?? undefined
);
const [isUserNameConfirmed, setIsUserNameConfirmed] = useState<boolean>(!displayLogInOverlay);
const [isCallFrameReady, setIsCallFrameReady] = useState<boolean>(false);
const createCallFrame = useCallback(
(userName?: string) => {
let callFrame: DailyCall | undefined;
try {
callFrame = DailyIframe.createFrame({
theme: {
colors: {
accent: "#FFF",
accentText: "#111111",
background: "#111111",
backgroundAccent: "#111111",
baseText: "#FFF",
border: "#292929",
mainAreaBg: "#111111",
mainAreaBgAccent: "#1A1A1A",
mainAreaText: "#FFF",
supportiveText: "#FFF",
},
},
showLeaveButton: true,
iframeStyle: {
position: "fixed",
width: "100%",
height: "100%",
},
url: meetingUrl,
userName: userName,
...(typeof meetingPassword === "string" && { token: meetingPassword }),
...(hasTeamPlan && {
customTrayButtons: {
...(showRecordingButton
? {
recording: {
label: t("record"),
tooltip: t("start_or_stop_recording"),
iconPath: RECORDING_DEFAULT_ICON,
iconPathDarkMode: RECORDING_DEFAULT_ICON,
},
}
: {}),
...(showTranscriptionButton
? {
transcription: {
label: t("transcribe"),
tooltip: t("transcription_powered_by_ai"),
iconPath: TRANSCRIPTION_STOPPED_ICON,
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
},
}
: {}),
},
}),
});
if (userName) {
callFrame.setUserName(userName);
}
return callFrame;
} catch (err) {
return DailyIframe.getCallInstance();
}
},
[meetingUrl, meetingPassword, hasTeamPlan, showRecordingButton, showTranscriptionButton, t]
);
useEffect(() => {
let callFrame: DailyCall | undefined;
try {
callFrame = DailyIframe.createFrame({
theme: {
colors: {
accent: "#FFF",
accentText: "#111111",
background: "#111111",
backgroundAccent: "#111111",
baseText: "#FFF",
border: "#292929",
mainAreaBg: "#111111",
mainAreaBgAccent: "#1A1A1A",
mainAreaText: "#FFF",
supportiveText: "#FFF",
},
},
showLeaveButton: true,
iframeStyle: {
position: "fixed",
width: "100%",
height: "100%",
},
url: meetingUrl,
userName: overrideName ?? loggedInUserName ?? undefined,
...(typeof meetingPassword === "string" && { token: meetingPassword }),
...(hasTeamPlan && {
customTrayButtons: {
...(showRecordingButton
? {
recording: {
label: "Record",
tooltip: "Start or stop recording",
iconPath: RECORDING_DEFAULT_ICON,
iconPathDarkMode: RECORDING_DEFAULT_ICON,
},
}
: {}),
...(showTranscriptionButton
? {
transcription: {
label: "Transcribe",
tooltip: "Transcription powered by AI",
iconPath: TRANSCRIPTION_STOPPED_ICON,
iconPathDarkMode: TRANSCRIPTION_STOPPED_ICON,
},
}
: {}),
},
}),
});
if (displayLogInOverlay && !loggedInUserName && !overrideName && !isUserNameConfirmed) {
return;
}
let callFrame: DailyCall | null = null;
try {
callFrame = createCallFrame(userNameForCall) ?? null;
setDaily(callFrame);
setIsCallFrameReady(true);
if (overrideName) {
callFrame.setUserName(overrideName);
}
} catch (err) {
callFrame = DailyIframe.getCallInstance();
} finally {
setDaily(callFrame ?? null);
callFrame?.join();
} catch (error) {
console.error("Failed to create or join call:", error);
}
return () => {
callFrame?.destroy();
if (callFrame) {
try {
callFrame.destroy();
} catch (error) {
console.error("Error destroying call frame:", error);
}
}
setDaily(null);
setIsCallFrameReady(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
displayLogInOverlay,
isUserNameConfirmed,
userNameForCall,
createCallFrame,
loggedInUserName,
overrideName,
]);
const handleJoinAsGuest = useCallback((guestName: string) => {
const trimmedName = guestName.trim();
if (!trimmedName) {
return;
}
setUserNameForCall(trimmedName);
setIsUserNameConfirmed(true);
}, []);
const handleUserNameConfirmed = useCallback(() => {
setIsUserNameConfirmed(true);
}, []);
return (
<DailyProvider callObject={daily}>
<div
className="mx-auto hidden sm:block"
style={{ zIndex: 2, left: "30%", position: "absolute", bottom: 100, width: "auto" }}>
<CalVideoPremiumFeatures
showRecordingButton={showRecordingButton}
enableAutomaticRecordingForOrganizer={enableAutomaticRecordingForOrganizer}
enableAutomaticTranscription={enableAutomaticTranscription}
showTranscriptionButton={showTranscriptionButton}
/>
</div>
{isCallFrameReady && (
<div
className="mx-auto hidden sm:block"
style={{ zIndex: 2, left: "30%", position: "absolute", bottom: 100, width: "auto" }}>
<CalVideoPremiumFeatures
showRecordingButton={showRecordingButton}
enableAutomaticRecordingForOrganizer={enableAutomaticRecordingForOrganizer}
enableAutomaticTranscription={enableAutomaticTranscription}
showTranscriptionButton={showTranscriptionButton}
/>
</div>
)}
<div style={{ zIndex: 2, position: "relative" }}>
{calVideoLogo ? (
<img
@@ -147,7 +205,16 @@ export default function JoinCall(props: PageProps) {
/>
)}
</div>
{displayLogInOverlay && <LogInOverlay isLoggedIn={!!loggedInUserName} bookingUid={booking.uid} />}
{displayLogInOverlay && !isUserNameConfirmed && (
<LogInOverlay
isLoggedIn={!!loggedInUserName}
bookingUid={booking.uid}
loggedInUserName={loggedInUserName ?? undefined}
overrideName={overrideName}
onJoinAsGuest={handleJoinAsGuest}
onUserNameConfirmed={handleUserNameConfirmed}
/>
)}
<VideoMeetingInfo booking={booking} rediectAttendeeToOnExit={rediectAttendeeToOnExit} />
</DailyProvider>
@@ -206,8 +273,7 @@ function ProgressBar(props: ProgressBarProps) {
intervalRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [startingTime]);
const prev = startDuration - duration;
const percentage = prev * (100 / startDuration);
@@ -227,49 +293,126 @@ function ProgressBar(props: ProgressBarProps) {
interface LogInOverlayProps {
isLoggedIn: boolean;
bookingUid: string;
loggedInUserName?: string;
overrideName?: string;
onJoinAsGuest: (guestName: string) => void;
onUserNameConfirmed?: () => void;
}
export function LogInOverlay(props: LogInOverlayProps) {
const { t } = useLocale();
const { isLoggedIn, bookingUid } = props;
const [open, setOpen] = useState(!isLoggedIn);
const { bookingUid, isLoggedIn, loggedInUserName, overrideName, onJoinAsGuest, onUserNameConfirmed } =
props;
const [isOpen, setIsOpen] = useState(!isLoggedIn);
const [userName, setUserName] = useState(overrideName ?? loggedInUserName ?? "");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleContinueAsGuest = useCallback(async () => {
const trimmedName = userName.trim();
if (!trimmedName) {
return;
}
setIsLoading(true);
setError(null);
try {
onJoinAsGuest(trimmedName);
onUserNameConfirmed?.();
setIsOpen(false);
} catch (error) {
console.error("Error joining as guest:", error);
const errorMessage = error instanceof Error ? error.message : t("failed_to_join_call");
setError(errorMessage);
} finally {
setIsLoading(false);
}
}, [userName, onJoinAsGuest, onUserNameConfirmed, t]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && userName.trim() && !isLoading) {
handleContinueAsGuest();
}
},
[userName, isLoading, handleContinueAsGuest]
);
const handleUserNameChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setUserName(e.target.value);
if (error) {
setError(null);
}
},
[error]
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent
title={t("join_video_call")}
description={t("choose_how_you_d_like_to_join_call")}
className="bg-black text-white sm:max-w-[480px]">
<div className="pb-8">
<div className="space-y-8">
<Button color="primary" className="mt-4 w-full justify-center " onClick={() => setOpen(false)}>
{t("continue_as_guest")}
</Button>
{/* Divider */}
<div className="relative py-2">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-gray-600" />
</div>
<div className="relative flex justify-center">
<span className="bg-black px-4 text-sm text-gray-400">{t("or")}</span>
</div>
description={t("choose_how_you_d_like_to_appear_on_the_call")}
className="bg-white text-black dark:bg-black dark:text-white sm:max-w-[480px]">
<div className="pb-4">
<div className="space-y-4">
<div>
<div className="font-semibold">{t("join_as_guest")}</div>
<p className="text-subtle text-sm">{t("ideal_for_one_time_calls")}</p>
</div>
<div className="space-y-3">
<h4 className="text-lg font-semibold text-white">{t("sign_in_to_cal_com")}</h4>
<p className="text-sm text-gray-300">{t("track_your_meetings")}</p>
<Button
color="primary"
className="mt-4 w-full justify-center"
onClick={() =>
(window.location.href = `${WEBAPP_URL}/auth/login?callbackUrl=${WEBAPP_URL}/video/${bookingUid}`)
}>
<Icon name="external-link" className="mr-2 h-4 w-4" />
{t("log_in_to_cal_com")}
<div className="flex gap-4">
<Input
type="text"
placeholder={t("your_name")}
className="w-full flex-1"
value={userName}
onChange={handleUserNameChange}
onKeyDown={handleKeyDown}
disabled={isLoading}
autoFocus
/>
<Button color="secondary" onClick={handleContinueAsGuest} loading={isLoading}>
{t("continue")}
</Button>
</div>
</div>
{error && (
<div className="rounded-md bg-red-50 p-3 dark:bg-red-900/20">
<div className="flex">
<div className="ml-3">
<p className="text-sm font-medium text-red-800 dark:text-red-200">{error}</p>
</div>
</div>
</div>
)}
{/* Divider */}
<hr className="my-5 h-0.5 border-t-0 bg-neutral-100 dark:bg-white/10" />
<div className="mt-5 space-y-4">
<div>
<h4 className="text-base font-semibold text-black dark:text-white">
{t("sign_in_to_cal_com")}
</h4>
<p className="text-sm text-[#6B7280] dark:text-gray-300">
{t("track_meetings_and_manage_schedule")}
</p>
</div>
<Button
color="primary"
className="w-full justify-center"
onClick={() =>
(window.location.href = `${WEBAPP_URL}/auth/login?callbackUrl=${WEBAPP_URL}/video/${bookingUid}`)
}>
{t("sign_in")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
+1 -1
View File
@@ -642,7 +642,7 @@ const nextConfig = (phase) => {
value: nextJsOrgRewriteConfig.orgHostPath,
},
],
destination: "/event-types?openPlain=true",
destination: "/event-types?openSupport=true",
permanent: true,
},
{
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.6.14",
"version": "5.6.16",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -74,7 +74,6 @@
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^5.17.15",
"@team-plain/typescript-sdk": "^5.9.0",
"@todesktop/client-core": "^1.20.0",
"@types/turndown": "^5.0.1",
"@upstash/redis": "^1.35.2",
+1 -1
View File
@@ -1,6 +1,6 @@
import { getRoutedUrl } from "@calcom/features/routing-forms/lib/getRoutedUrl";
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl";
export default defaultHandler({
OPTIONS: Promise.resolve({
@@ -1,4 +1,4 @@
import appBasecamp3 from "@calcom/app-store/basecamp3/trpc-router";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import appBasecamp3 from "@calcom/trpc/server/routers/apps/basecamp3/_router";
export default createNextApiHandler(appBasecamp3);
@@ -1,4 +1,4 @@
import appRoutingForms from "@calcom/app-store/routing-forms/trpc-router";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import appRoutingForms from "@calcom/trpc/server/routers/apps/routing-forms/_router";
export default createNextApiHandler(appRoutingForms);
@@ -0,0 +1,4 @@
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/_router";
export default createNextApiHandler(eventTypesRouter);
@@ -113,7 +113,7 @@ test("multiple duration selection updates event length correctly", async ({ page
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
await page.locator('[data-testid="update-eventtype"]').click();
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await page.waitForResponse("/api/trpc/eventTypes/heavy/update?batch=1");
});
await page.goto(`/${user.username}/multiple-duration`);
+5 -5
View File
@@ -131,7 +131,7 @@ test.describe("Event Types tests", () => {
expect(formTitle).toBe(firstTitle);
expect(formSlug).toContain(firstSlug);
const submitPromise = page.waitForResponse("/api/trpc/eventTypes/duplicate?batch=1");
const submitPromise = page.waitForResponse("/api/trpc/eventTypes/heavy/duplicate?batch=1");
await page.getByTestId("continue").click();
const response = await submitPromise;
expect(response.status()).toBe(200);
@@ -144,7 +144,7 @@ test.describe("Event Types tests", () => {
await page.waitForURL((url) => {
return !!url.pathname.match(/\/event-types\/.+/);
});
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
});
@@ -167,7 +167,7 @@ test.describe("Event Types tests", () => {
await page.locator("[data-testid=add-location]").click();
await fillLocation(page, locationData[2], 2);
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
@@ -314,7 +314,7 @@ test.describe("Event Types tests", () => {
const locationAddress = "New Delhi";
await fillLocation(page, locationAddress, 0, false);
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
@@ -429,7 +429,7 @@ test.describe("Event Types tests", () => {
await page.locator('[aria-label="Timezone Select"]').fill("New York");
await page.keyboard.press("Enter");
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
await page.goto("/event-types");
@@ -50,7 +50,7 @@ export function createBookingPageFixture(page: Page) {
await page.goto("/event-types");
},
updateEventType: async () => {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
},
+4 -4
View File
@@ -42,7 +42,7 @@ test.describe("private links creation and usage", () => {
const $url = await page.locator('[data-testid="private-link-url"]').inputValue();
// click update
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
// book using generated url hash
@@ -74,7 +74,7 @@ test.describe("private links creation and usage", () => {
await page.locator("[data-testid=event-slug]").first().fill("somethingrandom");
await expect(page.locator('[data-testid="event-slug"]').first()).toHaveValue("somethingrandom");
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
await page.locator("[data-testid=vertical-tab-event_advanced_tab_title]").click();
@@ -113,7 +113,7 @@ test.describe("private links creation and usage", () => {
await page.locator('[data-testid="private-link-expiration-settings-save"]').click();
await page.waitForLoadState("networkidle");
// click update
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
// book using generated url hash
@@ -164,7 +164,7 @@ test.describe("private links creation and usage", () => {
await page.locator('[data-testid="private-link-expiration-settings-save"]').click();
await page.waitForLoadState("networkidle");
// click update
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
// book using generated url hash
+1 -1
View File
@@ -476,7 +476,7 @@ export async function gotoBookingPage(page: Page) {
}
export async function saveEventType(page: Page) {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
}
@@ -677,7 +677,7 @@ async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"
}
async function saveEventType(page: Page) {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
}
@@ -239,5 +239,5 @@ async function gotoBookingPage(page: Page) {
}
async function saveAndWaitForResponse(page: Page) {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1");
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1");
}
@@ -407,7 +407,7 @@ test.describe("Bookings", () => {
await page.goto(`/event-types/${teamEvent.id}?tabName=advanced`);
await page.getByTestId("requires-confirmation").click();
await page.getByTestId("update-eventtype").click();
await page.waitForResponse((response) => response.url().includes("/api/trpc/eventTypes/update"));
await page.waitForResponse((response) => response.url().includes("/api/trpc/eventTypes/heavy/update"));
await doOnOrgDomain(
{
@@ -783,7 +783,7 @@ const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId:
const emailRequiredFiled = await page.locator('[data-testid="field-required"]').first();
await emailRequiredFiled.click();
await page.getByTestId("field-add-save").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
};
@@ -797,7 +797,7 @@ const markPhoneNumberAsRequiredField = async (page: Page, eventId: number) => {
const phoneRequiredFiled = await page.locator('[data-testid="field-required"]').first();
await phoneRequiredFiled.click();
await page.getByTestId("field-add-save").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
};
@@ -458,7 +458,7 @@ test.describe("Organization", () => {
await page.locator('[data-testid="fixed-hosts-select"]').click();
await page.locator(`text="${invitedUserEmail}"`).click();
await page.locator('[data-testid="update-eventtype"]').click();
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await page.waitForResponse("/api/trpc/eventTypes/heavy/update?batch=1");
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
+1 -1
View File
@@ -35,7 +35,7 @@ test.describe("Payment app", () => {
await page.getByPlaceholder("Price").click();
await page.getByPlaceholder("Price").fill("200");
await page.getByText("SatoshissatsCurrencyBTCPayment optionCollect payment on booking").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/heavy/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "السعر",
"paid": "مدفوعة",
"refunded": "تم الاسترداد",
"record": "تسجيل",
"transcribe": "نسخ",
"payment": "الدفعات",
"missing_card_fields": "بعض خانات البطاقة مفقودة",
"pay_now": "ادفع الآن",
"general_prompt": "التوجيه العام",
"transcription_powered_by_ai": "النسخ مدعوم بالذكاء الاصطناعي",
"start_or_stop_recording": "بدء أو إيقاف التسجيل",
"begin_message": "رسالة البداية",
"codebase_has_to_stay_opensource": "يجب أن يظل مصدر البرنامج مفتوحًا، سواء تم تعديله أم لا",
"cannot_repackage_codebase": "لا يمكنك بيع مصدر البرنامج أو تغييره للتصرف فيه (repackage)",
@@ -661,6 +665,8 @@
"somewhere_else": "مكان آخر",
"custom_attendee_location": "موقع الحضور المخصص",
"continue_as_guest": "المتابعة كضيف",
"join_as_guest": "الانضمام كضيف",
"ideal_for_one_time_calls": "مثالي للمكالمات لمرة واحدة",
"any_location": "أي موقع",
"yes": "نعم",
"no": "لا",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "بعد عدم انضمام المضيفين إلى فيديو Cal",
"after_guests_cal_video_no_show": "بعد عدم انضمام الضيوف إلى فيديو Cal",
"join_video_call": "الانضمام إلى مكالمة الفيديو",
"sign_in_to_cal_com": "تسجيل الدخول إلى حساب Cal.com الخاص بك",
"sign_in_to_cal_com": "تسجيل الدخول إلى Cal.com",
"log_in_to_cal_com": "تسجيل الدخول إلى Cal.com",
"track_your_meetings": "تتبع اجتماعاتك، والحصول على إشعارات، وإدارة جدولك بشكل أكثر فعالية.",
"track_meetings_and_manage_schedule": "تتبع اجتماعاتك وإدارة جدولك الزمني بشكل أكثر فعالية",
"failed_to_join_call": "فشل الانضمام إلى المكالمة. يرجى المحاولة مرة أخرى.",
"choose_how_you_d_like_to_join_call": "اختر كيف ترغب في الانضمام إلى هذه المكالمة",
"choose_how_you_d_like_to_appear_on_the_call": "اختر كيف ترغب في الظهور في المكالمة",
"new_event_trigger": "متى تم حجز الحدث الجديد",
"email_host_action": "إرسال رسالة إلكترونية إلى المضيف",
"email_attendee_action": "إرسال رسالة إلكترونية إلى الحضور",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "لديك فريق واحد",
"consider_consolidating_one_team_org": "فكر في إنشاء مؤسسة لتوحيد الفواتير وأدوات الإدارة والتحليلات عبر فريقك.",
"consider_consolidating_multi_team_org": "فكر في إنشاء مؤسسة لتوحيد الفواتير وأدوات الإدارة والتحليلات عبر فرقك.",
"message_sent": "تم إرسال الرسالة",
"support_message_sent_description": "شكرًا للتواصل معنا. سنرد عليك في أقرب وقت ممكن.",
"search_variables": "البحث عن المتغيرات",
"support_message_placeholder": "يرجى وصف المشكلة التي تواجهها، مثل: \"المواعيد المشغولة تظهر كمتاحة\"، ... إلخ.",
"send_another_message": "إرسال رسالة أخرى",
"sending": "جارٍ الإرسال",
"no_variables_found": "لم يتم العثور على متغيرات",
"api_key_name_too_long": "يجب ألا يتجاوز الاسم {{max}} حرفًا",
"auto_charge_for_last_minute_cancellation": "فرض رسوم التخلف عن الحضور تلقائياً للإلغاءات في اللحظة الأخيرة",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Qiymət",
"paid": "Ödənilib",
"refunded": "Geri qaytarılıb",
"record": "Qeyd et",
"transcribe": "Transkripsiya et",
"payment": "Ödəniş",
"missing_card_fields": "Kart məlumatları çatışmır",
"pay_now": "İndi ödə",
"general_prompt": "Ümumi sorğu",
"transcription_powered_by_ai": "Transkripsiya süni intellekt tərəfindən təmin edilir",
"start_or_stop_recording": "Qeydiyyatı başlat və ya dayandır",
"begin_message": "Mesaja başla",
"codebase_has_to_stay_opensource": "Kod bazası dəyişdirilsə də, açıq mənbə olaraq qalmalıdır",
"cannot_repackage_codebase": "Kod bazasını yenidən paketləyə və ya sata bilməzsiniz",
@@ -661,6 +665,8 @@
"somewhere_else": "Başqa yerdə",
"custom_attendee_location": "Xüsusi iştirakçı yeri",
"continue_as_guest": "Qonaq kimi davam et",
"join_as_guest": "Qonaq kimi qoşul",
"ideal_for_one_time_calls": "Birdəfəlik zənglər üçün ideal",
"any_location": "Hər hansı bir yer",
"yes": "Bəli",
"no": "Xeyr",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Təşkilatçılar Cal video görüşünə qoşulmadıqdan sonra",
"after_guests_cal_video_no_show": "Qonaqlar Cal video görüşünə qoşulmadıqdan sonra",
"join_video_call": "Video zəngə qoşulun",
"sign_in_to_cal_com": "Cal.com hesabınıza daxil olun",
"sign_in_to_cal_com": "Cal.com-a daxil ol",
"log_in_to_cal_com": "Cal.com-a daxil olun",
"track_your_meetings": "Görüşlərinizi izləyin, bildirişlər alın və cədvəlinizi daha effektiv idarə edin.",
"track_meetings_and_manage_schedule": "Görüşlərinizi izləyin və cədvəlinizi daha effektiv idarə edin",
"failed_to_join_call": "Zəngə qoşulmaq alınmadı. Zəhmət olmasa yenidən cəhd edin.",
"choose_how_you_d_like_to_join_call": "Bu zəngə necə qoşulmaq istədiyinizi seçin",
"choose_how_you_d_like_to_appear_on_the_call": "Zəngdə necə görünmək istədiyinizi seçin",
"new_event_trigger": "yeni tədbir sifariş edildikdə",
"email_host_action": "ev sahibinə e-poçt göndərin",
"email_attendee_action": "iştirakçılara e-poçt göndərin",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Bir komandanız var",
"consider_consolidating_one_team_org": "Komandanız üzrə ödənişləri, admin alətlərini və analitikanı birləşdirmək üçün təşkilat qurmağı nəzərdən keçirin.",
"consider_consolidating_multi_team_org": "Komandalarınız üzrə ödənişləri, admin alətlərini və analitikanı birləşdirmək üçün təşkilat qurmağı nəzərdən keçirin.",
"message_sent": "Mesaj göndərildi",
"support_message_sent_description": "Bizimlə əlaqə saxladığınız üçün təşəkkür edirik. Sizə mümkün qədər tez cavab verəcəyik.",
"search_variables": "Dəyişənləri axtar",
"support_message_placeholder": "Zəhmət olmasa, qarşılaşdığınız problemi təsvir edin, məsələn, \"Məşğul yerlər boş kimi göstərilir\", ..., və s.",
"send_another_message": "Başqa bir mesaj göndər",
"sending": "Göndərilir",
"no_variables_found": "Heç bir dəyişən tapılmadı",
"api_key_name_too_long": "Ad {{max}} simvolu keçməməlidir",
"auto_charge_for_last_minute_cancellation": "Son dəqiqə ləğvləri üçün avtomatik olaraq gəlməmə haqqını tutun",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Цена",
"paid": "Платено",
"refunded": "Възстановено",
"record": "Запис",
"transcribe": "Транскрибиране",
"payment": "Плащане",
"missing_card_fields": "Липсващи полета за картата",
"pay_now": "Плати сега",
"general_prompt": "Общ шаблон",
"transcription_powered_by_ai": "Транскрипцията се осъществява чрез ИИ",
"start_or_stop_recording": "Стартиране или спиране на записа",
"begin_message": "Начално съобщение",
"codebase_has_to_stay_opensource": "Кодовата база трябва да остане с отворен код, независимо дали е модифицирана или не",
"cannot_repackage_codebase": "Не можете да преопаковате или продавате кодовата база",
@@ -661,6 +665,8 @@
"somewhere_else": "Друго място",
"custom_attendee_location": "Персонализирана локация на участника",
"continue_as_guest": "Продължи като гост",
"join_as_guest": "Присъединяване като гост",
"ideal_for_one_time_calls": "Идеално за еднократни разговори",
"any_location": "Всяка локация",
"yes": "Да",
"no": "Не",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "След като домакините не се присъединиха към Cal видеото",
"after_guests_cal_video_no_show": "След като гостите не се присъединиха към Cal видеото",
"join_video_call": "Присъединете се към видео разговора",
"sign_in_to_cal_com": "Влезте във вашия Cal.com акаунт",
"sign_in_to_cal_com": "Вход в Cal.com",
"log_in_to_cal_com": "Влезте в Cal.com",
"track_your_meetings": "Проследявайте срещите си, получавайте известия и управлявайте графика си по-ефективно.",
"track_meetings_and_manage_schedule": "Проследявайте срещите си и управлявайте графика си по-ефективно",
"failed_to_join_call": "Неуспешно присъединяване към разговора. Моля, опитайте отново.",
"choose_how_you_d_like_to_join_call": "Изберете как искате да се присъедините към този разговор",
"choose_how_you_d_like_to_appear_on_the_call": "Изберете как искате да се появите в разговора",
"new_event_trigger": "когато е резервирано ново събитие",
"email_host_action": "изпращане на имейл до домакина",
"email_attendee_action": "изпращане на имейл до участниците",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Имате един екип",
"consider_consolidating_one_team_org": "Помислете за създаване на организация, за да обедините таксуването, административните инструменти и анализите за вашия екип.",
"consider_consolidating_multi_team_org": "Помислете за създаване на организация, за да обедините таксуването, административните инструменти и анализите за вашите екипи.",
"message_sent": "Съобщението е изпратено",
"support_message_sent_description": "Благодарим ви, че се свързахте с нас. Ще ви отговорим възможно най-скоро.",
"search_variables": "Търсене на променливи",
"support_message_placeholder": "Моля, опишете проблема, с който се сблъсквате, напр. \"Заетите часове са маркирани като свободни\", ..., и т.н.",
"send_another_message": "Изпратете друго съобщение",
"sending": "Изпращане",
"no_variables_found": "Не са намерени променливи",
"api_key_name_too_long": "Името не трябва да надвишава {{max}} символа",
"auto_charge_for_last_minute_cancellation": "Автоматично таксуване на такса за неявяване при отмяна в последния момент",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "দাম",
"paid": "প্রদত্ত",
"refunded": "ফেরত দেওয়া",
"record": "রেকর্ড",
"transcribe": "ট্রান্সক্রাইব",
"payment": "অর্থ প্রদান",
"missing_card_fields": "অনুপস্থিত কার্ড ক্ষেত্রগুলি",
"pay_now": "এখনই প্রদান করুন",
"general_prompt": "সাধারণ প্রম্পট",
"transcription_powered_by_ai": "এআই দ্বারা চালিত ট্রান্সক্রিপশন",
"start_or_stop_recording": "রেকর্ডিং শুরু বা বন্ধ করুন",
"begin_message": "বার্তা শুরু করুন",
"codebase_has_to_stay_opensource": "কোডবেসকে ওপেন সোর্স থাকতে হবে, এটি সংশোধন করা হয়েছিল বা না হোক",
"cannot_repackage_codebase": "আপনি কোডবেস পুনরায় বিক্রয় বা বিক্রয় করতে পারবেন না",
@@ -661,6 +665,8 @@
"somewhere_else": "অন্য কোথাও",
"custom_attendee_location": "কাস্টম উপস্থিতি অবস্থান",
"continue_as_guest": "অতিথি হিসাবে চালিয়ে যান",
"join_as_guest": "অতিথি হিসেবে যোগ দিন",
"ideal_for_one_time_calls": "একবার কলের জন্য আদর্শ",
"any_location": "কোন অবস্থান",
"yes": "হ্যাঁ",
"no": "না",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "স্বাগতিকদের পরে ক্যাল ভিডিওতে যোগ দেয়নি",
"after_guests_cal_video_no_show": "অতিথিরা ক্যাল ভিডিওতে যোগ না দেওয়ার পরে",
"join_video_call": "ভিডিও কলে যোগ দিন",
"sign_in_to_cal_com": "আপনার Cal.com অ্যাকাউন্টে সাইন ইন করুন",
"sign_in_to_cal_com": "Cal.com-এ সাইন ইন করুন",
"log_in_to_cal_com": "Cal.com-এ লগ ইন করুন",
"track_your_meetings": "আপনার মিটিং ট্র্যাক করুন, বিজ্ঞপ্তি পান, এবং আপনার সময়সূচী আরও কার্যকরভাবে পরিচালনা করুন।",
"track_meetings_and_manage_schedule": "আপনার মিটিংগুলি ট্র্যাক করুন এবং আপনার সময়সূচী আরও কার্যকরভাবে পরিচালনা করুন",
"failed_to_join_call": "কলে যোগ দিতে ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
"choose_how_you_d_like_to_join_call": "আপনি কিভাবে এই কলে যোগ দিতে চান তা বেছে নিন",
"choose_how_you_d_like_to_appear_on_the_call": "কলে আপনি কীভাবে দেখাতে চান তা বেছে নিন",
"new_event_trigger": "যখন নতুন ইভেন্ট বুক করা হয়",
"email_host_action": "হোস্টে ইমেল প্রেরণ করুন",
"email_attendee_action": "উপস্থিতদের ইমেল প্রেরণ করুন",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "আপনার একটি দল আছে",
"consider_consolidating_one_team_org": "আপনার দলের মধ্যে বিলিং, অ্যাডমিন টুলস এবং অ্যানালিটিক্স একত্রিত করতে একটি সংগঠন সেট আপ করার কথা বিবেচনা করুন।",
"consider_consolidating_multi_team_org": "আপনার দলগুলির মধ্যে বিলিং, অ্যাডমিন টুলস এবং অ্যানালিটিক্স একত্রিত করতে একটি সংগঠন সেট আপ করার কথা বিবেচনা করুন।",
"message_sent": "বার্তা পাঠানো হয়েছে",
"support_message_sent_description": "আমাদের সাথে যোগাযোগ করার জন্য ধন্যবাদ। আমরা যত তাড়াতাড়ি সম্ভব আপনার সাথে যোগাযোগ করব।",
"search_variables": "ভেরিয়েবল অনুসন্ধান করুন",
"support_message_placeholder": "অনুগ্রহ করে আপনি যে সমস্যার সম্মুখীন হচ্ছেন তা বর্ণনা করুন, যেমন 'ব্যস্ত স্লটগুলি উপলব্ধ হিসেবে চিহ্নিত করা হয়েছে', ..., ইত্যাদি।",
"send_another_message": "আরেকটি বার্তা পাঠান",
"sending": "পাঠানো হচ্ছে",
"no_variables_found": "কোন ভেরিয়েবল পাওয়া যায়নি",
"api_key_name_too_long": "নাম অবশ্যই {{max}} অক্ষরের বেশি হবে না",
"auto_charge_for_last_minute_cancellation": "শেষ মুহূর্তের বাতিলকরণের জন্য স্বয়ংক্রিয়ভাবে অনুপস্থিতি ফি চার্জ করুন",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Preu",
"paid": "Pagat",
"refunded": "Reembossat",
"record": "Enregistrar",
"transcribe": "Transcriure",
"payment": "Pagament",
"missing_card_fields": "Falten camps de la targeta",
"pay_now": "Paga ara",
"general_prompt": "Indicació general",
"transcription_powered_by_ai": "Transcripció impulsada per IA",
"start_or_stop_recording": "Iniciar o aturar l'enregistrament",
"begin_message": "Començar missatge",
"codebase_has_to_stay_opensource": "El codi font ha de seguir com codi obert, tant si s'ha modificat com si no",
"cannot_repackage_codebase": "No pots tornar a empaquetar ni vendre el codi font",
@@ -661,6 +665,8 @@
"somewhere_else": "Un altre lloc",
"custom_attendee_location": "Ubicació personalitzada de l'assistent",
"continue_as_guest": "Continua com a convidat",
"join_as_guest": "Unir-se com a convidat",
"ideal_for_one_time_calls": "Ideal per a trucades puntuals",
"any_location": "Qualsevol ubicació",
"yes": "Sí",
"no": "No",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Després que els amfitrions no s'hagin unit al vídeo de Cal",
"after_guests_cal_video_no_show": "Després que els convidats no s'hagin unit al vídeo de Cal",
"join_video_call": "Uneix-te a la videotrucada",
"sign_in_to_cal_com": "Inicia sessió al teu compte de Cal.com",
"sign_in_to_cal_com": "Iniciar sessió a Cal.com",
"log_in_to_cal_com": "Inicia sessió a Cal.com",
"track_your_meetings": "Fes seguiment de les teves reunions, rep notificacions i gestiona la teva agenda de manera més efectiva.",
"track_meetings_and_manage_schedule": "Fes un seguiment de les teves reunions i gestiona el teu horari de manera més efectiva",
"failed_to_join_call": "No s'ha pogut unir a la trucada. Si us plau, torna-ho a provar.",
"choose_how_you_d_like_to_join_call": "Tria com vols unir-te a aquesta trucada",
"choose_how_you_d_like_to_appear_on_the_call": "Tria com vols aparèixer a la trucada",
"new_event_trigger": "quan es reserva un nou esdeveniment",
"email_host_action": "enviar correu a l'amfitrió",
"email_attendee_action": "enviar correu als assistents",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Tens un equip",
"consider_consolidating_one_team_org": "Considera configurar una organització per unificar la facturació, les eines d'administració i les analítiques del teu equip.",
"consider_consolidating_multi_team_org": "Considera configurar una organització per unificar la facturació, les eines d'administració i les analítiques dels teus equips.",
"message_sent": "Missatge enviat",
"support_message_sent_description": "Gràcies per contactar-nos. Et respondrem tan aviat com sigui possible.",
"search_variables": "Cerca variables",
"support_message_placeholder": "Si us plau, descriu el problema que estàs experimentant, p. ex. 'Els espais ocupats apareixen com a disponibles', ..., etc.",
"send_another_message": "Envia un altre missatge",
"sending": "Enviant",
"no_variables_found": "No s'han trobat variables",
"api_key_name_too_long": "El nom no ha de superar els {{max}} caràcters",
"auto_charge_for_last_minute_cancellation": "Càrrec automàtic de la tarifa per no assistència en cas de cancel·lacions d'última hora",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Cena",
"paid": "Zaplaceno",
"refunded": "Platba vrácena",
"record": "Nahrávat",
"transcribe": "Přepsat",
"payment": "Platba",
"missing_card_fields": "Chybějící údaje karty",
"pay_now": "Zaplatit",
"general_prompt": "Obecná výzva",
"transcription_powered_by_ai": "Přepis poháněný umělou inteligencí",
"start_or_stop_recording": "Spustit nebo zastavit nahrávání",
"begin_message": "Zahájit zprávu",
"codebase_has_to_stay_opensource": "Zdrojové kódy musí zůstat jako open source, bez ohledu na to, jestli je upravujete nebo ne",
"cannot_repackage_codebase": "Zdrojové kódy nelze vydat upravené nebo přeprodávat",
@@ -661,6 +665,8 @@
"somewhere_else": "Jiné místo",
"custom_attendee_location": "Vlastní místo účastníka",
"continue_as_guest": "Pokračovat jako host",
"join_as_guest": "Připojit se jako host",
"ideal_for_one_time_calls": "Ideální pro jednorázové hovory",
"any_location": "Jakékoliv místo",
"yes": "Ano",
"no": "Ne",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Poté, co se hostitelé nepřipojili k video hovoru",
"after_guests_cal_video_no_show": "Poté, co se hosté nepřipojili k video hovoru",
"join_video_call": "Připojit se k videohovoru",
"sign_in_to_cal_com": "Přihlaste se ke svému účtu Cal.com",
"sign_in_to_cal_com": "Přihlásit se do Cal.com",
"log_in_to_cal_com": "Přihlásit se do Cal.com",
"track_your_meetings": "Sledujte své schůzky, dostávejte oznámení a efektivněji spravujte svůj rozvrh.",
"track_meetings_and_manage_schedule": "Sledujte své schůzky a efektivněji spravujte svůj rozvrh",
"failed_to_join_call": "Nepodařilo se připojit k hovoru. Zkuste to prosím znovu.",
"choose_how_you_d_like_to_join_call": "Vyberte, jak se chcete připojit k tomuto hovoru",
"choose_how_you_d_like_to_appear_on_the_call": "Vyberte, jak chcete být vidět během hovoru",
"new_event_trigger": "při rezervaci nové události",
"email_host_action": "odeslání e-mailu hostiteli",
"email_attendee_action": "odeslat e-mail účastníkům",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Máte jeden tým",
"consider_consolidating_one_team_org": "Zvažte založení organizace pro sjednocení fakturace, administrativních nástrojů a analytiky napříč vaším týmem.",
"consider_consolidating_multi_team_org": "Zvažte založení organizace pro sjednocení fakturace, administrativních nástrojů a analytiky napříč vašimi týmy.",
"message_sent": "Zpráva odeslána",
"support_message_sent_description": "Děkujeme za kontaktování. Ozveme se vám co nejdříve.",
"search_variables": "Hledat proměnné",
"support_message_placeholder": "Popište prosím problém, se kterým se potýkáte, např. \"Obsazené termíny jsou označeny jako dostupné\", ..., atd.",
"send_another_message": "Poslat další zprávu",
"sending": "Odesílání",
"no_variables_found": "Nebyly nalezeny žádné proměnné",
"api_key_name_too_long": "Název nesmí přesáhnout {{max}} znaků",
"auto_charge_for_last_minute_cancellation": "Automaticky účtovat poplatek za nedostavení se při zrušení na poslední chvíli",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Pris",
"paid": "Betalt",
"refunded": "Tilbagebetalt",
"record": "Optag",
"transcribe": "Transskriber",
"payment": "Betaling",
"missing_card_fields": "Manglende kortfelter",
"pay_now": "Betal nu",
"general_prompt": "Generel prompt",
"transcription_powered_by_ai": "Transskription drevet af AI",
"start_or_stop_recording": "Start eller stop optagelse",
"begin_message": "Begynd besked",
"codebase_has_to_stay_opensource": "Kodebasen skal forblive open source, uanset om den blev ændret eller ej",
"cannot_repackage_codebase": "Du kan ikke ompakke eller sælge kodebasen",
@@ -661,6 +665,8 @@
"somewhere_else": "Et andet sted",
"custom_attendee_location": "Brugerdefineret deltagerlokation",
"continue_as_guest": "Fortsæt som gæst",
"join_as_guest": "Deltag som gæst",
"ideal_for_one_time_calls": "Ideel til engangssamtaler",
"any_location": "Enhver lokation",
"yes": "Ja",
"no": "Nej",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Efter værterne ikke deltog i cal-videomødet",
"after_guests_cal_video_no_show": "Efter gæsterne ikke deltog i cal-videomødet",
"join_video_call": "Deltag i videoopkaldet",
"sign_in_to_cal_com": "Log ind på din Cal.com-konto",
"sign_in_to_cal_com": "Log ind på Cal.com",
"log_in_to_cal_com": "Log ind på Cal.com",
"track_your_meetings": "Følg dine møder, få notifikationer og administrer din tidsplan mere effektivt.",
"track_meetings_and_manage_schedule": "Følg dine møder og administrer din tidsplan mere effektivt",
"failed_to_join_call": "Kunne ikke deltage i opkaldet. Prøv venligst igen.",
"choose_how_you_d_like_to_join_call": "Vælg, hvordan du vil deltage i dette opkald",
"choose_how_you_d_like_to_appear_on_the_call": "Vælg, hvordan du vil fremstå i opkaldet",
"new_event_trigger": "når ny begivenhed er booket",
"email_host_action": "send e-mail til vært",
"email_attendee_action": "send e-mail til deltagere",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Du har ét team",
"consider_consolidating_one_team_org": "Overvej at oprette en organisation for at samle fakturering, adminværktøjer og analyser på tværs af dit team.",
"consider_consolidating_multi_team_org": "Overvej at oprette en organisation for at samle fakturering, adminværktøjer og analyser på tværs af dine teams.",
"message_sent": "Besked sendt",
"support_message_sent_description": "Tak for at kontakte os. Vi vender tilbage til dig så hurtigt som muligt.",
"search_variables": "Søg i variabler",
"support_message_placeholder": "Beskriv venligst det problem, du oplever, f.eks. 'Optagede tider er markeret som ledige', ..., osv.",
"send_another_message": "Send en ny besked",
"sending": "Sender",
"no_variables_found": "Ingen variable fundet",
"api_key_name_too_long": "Navnet må ikke overstige {{max}} tegn",
"auto_charge_for_last_minute_cancellation": "Automatisk opkrævning af gebyr for udeblivelse ved aflysning i sidste øjeblik",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Preis",
"paid": "Bezahlt",
"refunded": "Erstattet",
"record": "Aufnehmen",
"transcribe": "Transkribieren",
"payment": "Bezahlung",
"missing_card_fields": "Fehlender Eintrag für Karteninformationen",
"pay_now": "Jetzt bezahlen",
"general_prompt": "Allgemeine Aufforderung",
"transcription_powered_by_ai": "Transkription unterstützt durch KI",
"start_or_stop_recording": "Aufnahme starten oder stoppen",
"begin_message": "Nachricht starten",
"codebase_has_to_stay_opensource": "Die Codebasis muss Open Source bleiben, unabhängig davon, ob sie geändert wurde oder nicht",
"cannot_repackage_codebase": "Sie dürfen die Codebasis nicht weiter verkaufen",
@@ -661,6 +665,8 @@
"somewhere_else": "Anderer Ort",
"custom_attendee_location": "Benutzerdefinierter Teilnehmerort",
"continue_as_guest": "Als Gast fortfahren",
"join_as_guest": "Als Gast beitreten",
"ideal_for_one_time_calls": "Ideal für einmalige Anrufe",
"any_location": "Beliebiger Ort",
"yes": "Ja",
"no": "Nein",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Nachdem die Gastgeber nicht am Cal-Video teilgenommen haben",
"after_guests_cal_video_no_show": "Nachdem die Gäste nicht am Cal-Video teilgenommen haben",
"join_video_call": "Am Videoanruf teilnehmen",
"sign_in_to_cal_com": "Bei Ihrem Cal.com-Konto anmelden",
"sign_in_to_cal_com": "Bei Cal.com anmelden",
"log_in_to_cal_com": "Bei Cal.com anmelden",
"track_your_meetings": "Verfolgen Sie Ihre Meetings, erhalten Sie Benachrichtigungen und verwalten Sie Ihren Zeitplan effektiver.",
"track_meetings_and_manage_schedule": "Verfolge deine Meetings und verwalte deinen Zeitplan effektiver",
"failed_to_join_call": "Beitritt zum Anruf fehlgeschlagen. Bitte versuche es erneut.",
"choose_how_you_d_like_to_join_call": "Wählen Sie, wie Sie an diesem Anruf teilnehmen möchten",
"choose_how_you_d_like_to_appear_on_the_call": "Wähle, wie du im Anruf erscheinen möchtest",
"new_event_trigger": "wenn ein neuer Termin gebucht wird",
"email_host_action": "E-Mail an den Veranstalter senden",
"email_attendee_action": "E-Mail an Teilnehmende senden",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Sie haben ein Team",
"consider_consolidating_one_team_org": "Erwägen Sie die Einrichtung einer Organisation, um Abrechnung, Administrationstools und Analysen für Ihr Team zu vereinheitlichen.",
"consider_consolidating_multi_team_org": "Erwägen Sie die Einrichtung einer Organisation, um Abrechnung, Administrationstools und Analysen für Ihre Teams zu vereinheitlichen.",
"message_sent": "Nachricht gesendet",
"support_message_sent_description": "Vielen Dank für Ihre Kontaktaufnahme. Wir werden uns so schnell wie möglich bei Ihnen melden.",
"search_variables": "Variablen durchsuchen",
"support_message_placeholder": "Bitte beschreiben Sie das Problem, mit dem Sie konfrontiert sind, z.B. 'Belegte Zeitfenster werden als verfügbar angezeigt', ..., usw.",
"send_another_message": "Weitere Nachricht senden",
"sending": "Wird gesendet",
"no_variables_found": "Keine Variablen gefunden",
"api_key_name_too_long": "Der Name darf {{max}} Zeichen nicht überschreiten",
"auto_charge_for_last_minute_cancellation": "Automatische Berechnung der Ausfallgebühr für kurzfristige Stornierungen",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Τιμή",
"paid": "Πληρώθηκε",
"refunded": "Πιστώθηκε",
"record": "Εγγραφή",
"transcribe": "Απομαγνητοφώνηση",
"payment": "Πληρωμή",
"missing_card_fields": "Λείπουν τα πεδία της κάρτας",
"pay_now": "Πληρωμή τώρα",
"general_prompt": "Γενική προτροπή",
"transcription_powered_by_ai": "Απομαγνητοφώνηση με τη δύναμη της τεχνητής νοημοσύνης",
"start_or_stop_recording": "Έναρξη ή διακοπή εγγραφής",
"begin_message": "Έναρξη μηνύματος",
"codebase_has_to_stay_opensource": "Ο κώδικας πρέπει να παραμείνει ανοιχτού κώδικα, είτε έχει τροποποιηθεί είτε όχι",
"cannot_repackage_codebase": "Δεν μπορείτε να επανασυσκευάσετε ή να πωλήσετε τον κώδικα",
@@ -661,6 +665,8 @@
"somewhere_else": "Κάπου αλλού",
"custom_attendee_location": "Προσαρμοσμένη τοποθεσία συμμετέχοντα",
"continue_as_guest": "Συνέχεια ως επισκέπτης",
"join_as_guest": "Σύνδεση ως επισκέπτης",
"ideal_for_one_time_calls": "Ιδανικό για κλήσεις μίας φοράς",
"any_location": "Οποιαδήποτε τοποθεσία",
"yes": "Ναί",
"no": "Όχι",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Αφού οι διοργανωτές δεν συνδέθηκαν στο cal video",
"after_guests_cal_video_no_show": "Αφού οι καλεσμένοι δεν συνδέθηκαν στο cal video",
"join_video_call": "Συμμετοχή στη βιντεοκλήση",
"sign_in_to_cal_com": "Συνδεθείτε στον λογαριασμό σας στο Cal.com",
"sign_in_to_cal_com": "Σύνδεση στο Cal.com",
"log_in_to_cal_com": "Σύνδεση στο Cal.com",
"track_your_meetings": "Παρακολουθήστε τις συναντήσεις σας, λάβετε ειδοποιήσεις και διαχειριστείτε το πρόγραμμά σας πιο αποτελεσματικά.",
"track_meetings_and_manage_schedule": "Παρακολουθήστε τις συναντήσεις σας και διαχειριστείτε το πρόγραμμά σας πιο αποτελεσματικά",
"failed_to_join_call": "Αποτυχία σύνδεσης στην κλήση. Παρακαλώ δοκιμάστε ξανά.",
"choose_how_you_d_like_to_join_call": "Επιλέξτε πώς θέλετε να συμμετάσχετε σε αυτήν την κλήση",
"choose_how_you_d_like_to_appear_on_the_call": "Επιλέξτε πώς θέλετε να εμφανίζεστε στην κλήση",
"new_event_trigger": "όταν γίνει κράτηση νέου συμβάντος",
"email_host_action": "αποστολή email στον διοργανωτή",
"email_attendee_action": "αποστολή email στους συμμετέχοντες",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Έχετε μία ομάδα",
"consider_consolidating_one_team_org": "Σκεφτείτε να δημιουργήσετε έναν οργανισμό για να ενοποιήσετε τη χρέωση, τα εργαλεία διαχείρισης και τα αναλυτικά στοιχεία σε όλη την ομάδα σας.",
"consider_consolidating_multi_team_org": "Σκεφτείτε να δημιουργήσετε έναν οργανισμό για να ενοποιήσετε τη χρέωση, τα εργαλεία διαχείρισης και τα αναλυτικά στοιχεία σε όλες τις ομάδες σας.",
"message_sent": "Το μήνυμα εστάλη",
"support_message_sent_description": "Σας ευχαριστούμε που επικοινωνήσατε μαζί μας. Θα επικοινωνήσουμε μαζί σας το συντομότερο δυνατό.",
"search_variables": "Αναζήτηση μεταβλητών",
"support_message_placeholder": "Παρακαλώ περιγράψτε το πρόβλημα που αντιμετωπίζετε, π.χ. 'Οι κατειλημμένες χρονοθυρίδες εμφανίζονται ως διαθέσιμες', ..., κλπ.",
"send_another_message": "Αποστολή άλλου μηνύματος",
"sending": "Αποστολή",
"no_variables_found": "Δεν βρέθηκαν μεταβλητές",
"api_key_name_too_long": "Το όνομα δεν πρέπει να υπερβαίνει τους {{max}} χαρακτήρες",
"auto_charge_for_last_minute_cancellation": "Αυτόματη χρέωση τέλους μη εμφάνισης για ακυρώσεις τελευταίας στιγμής",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Price",
"paid": "Paid",
"refunded": "Refunded",
"record": "Record",
"transcribe": "Transcribe",
"payment": "Payment",
"missing_card_fields": "Missing card fields",
"pay_now": "Pay now",
"general_prompt": "General Prompt",
"transcription_powered_by_ai": "Transcription powered by AI",
"start_or_stop_recording": "Start or stop recording",
"begin_message": "Begin Message",
"codebase_has_to_stay_opensource": "The codebase has to stay open source, whether it was modified or not",
"cannot_repackage_codebase": "You can not repackage or sell the codebase",
@@ -661,6 +665,8 @@
"somewhere_else": "Somewhere Else",
"custom_attendee_location": "Custom attendee location",
"continue_as_guest": "Continue as Guest",
"join_as_guest": "Join as Guest",
"ideal_for_one_time_calls": "Ideal for one-time calls",
"any_location": "Any location",
"yes": "Yes",
"no": "No",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "After hosts didn't join cal video",
"after_guests_cal_video_no_show": "After guests didn't join cal video",
"join_video_call": "Join the video call",
"sign_in_to_cal_com": "Sign in to your Cal.com account",
"sign_in_to_cal_com": "Sign in to Cal.com",
"log_in_to_cal_com": "Log in to Cal.com",
"track_your_meetings": "Track your meetings, get notifications, and manage your schedule more effectively.",
"track_meetings_and_manage_schedule": "Track your meetings and manage your schedule more effectively",
"failed_to_join_call": "Failed to join the call. Please try again.",
"choose_how_you_d_like_to_join_call": "Choose how you'd like to join this call",
"choose_how_you_d_like_to_appear_on_the_call": "Choose how you'd like to appear on the call",
"new_event_trigger": "when new event is booked",
"email_host_action": "send email to host",
"email_attendee_action": "send email to attendees",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "You have one team",
"consider_consolidating_one_team_org": "Consider setting up an organization to unify billing, admin tools, and analytics across your team.",
"consider_consolidating_multi_team_org": "Consider setting up an organization to unify billing, admin tools, and analytics across your teams.",
"message_sent": "Message Sent",
"support_message_sent_description": "Thank you for contacting us. We'll get back to you as soon as possible.",
"search_variables": "Search variables",
"support_message_placeholder": "Please describe the issue you're facing, e.g. 'Busy slots are marked available', ..., etc.",
"send_another_message": "Send another message",
"sending": "Sending",
"no_variables_found": "No variables found",
"api_key_name_too_long": "Name must not exceed {{max}} characters",
"auto_charge_for_last_minute_cancellation": "Auto charge no-show fee for last minute cancellations",
@@ -125,10 +125,14 @@
"price": "Precio",
"paid": "Pagado",
"refunded": "Reembolsado",
"record": "Grabar",
"transcribe": "Transcribir",
"payment": "Pago",
"missing_card_fields": "Faltan campos de la tarjeta",
"pay_now": "Pagar ahora",
"general_prompt": "Mensaje general",
"transcription_powered_by_ai": "Transcripción impulsada por IA",
"start_or_stop_recording": "Iniciar o detener grabación",
"begin_message": "Iniciar mensaje",
"codebase_has_to_stay_opensource": "El código base debe permanecer de código abierto, ya sea modificado o no",
"cannot_repackage_codebase": "No puedes reempaquetar o vender el código base",
@@ -661,6 +665,8 @@
"somewhere_else": "En otro lugar",
"custom_attendee_location": "Ubicación personalizada del asistente",
"continue_as_guest": "Continuar como invitado",
"join_as_guest": "Unirse como invitado",
"ideal_for_one_time_calls": "Ideal para llamadas únicas",
"any_location": "Cualquier ubicación",
"yes": "Sí",
"no": "No",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Después de que los anfitriones no se unieron a la videollamada",
"after_guests_cal_video_no_show": "Después de que los invitados no se unieron a la videollamada",
"join_video_call": "Unirse a la videollamada",
"sign_in_to_cal_com": "Inicia sesión en tu cuenta de Cal.com",
"sign_in_to_cal_com": "Iniciar sesión en Cal.com",
"log_in_to_cal_com": "Iniciar sesión en Cal.com",
"track_your_meetings": "Rastrea tus reuniones, recibe notificaciones y administra tu agenda de manera más efectiva.",
"track_meetings_and_manage_schedule": "Rastrea tus reuniones y administra tu agenda de manera más efectiva",
"failed_to_join_call": "No se pudo unir a la llamada. Por favor, inténtalo de nuevo.",
"choose_how_you_d_like_to_join_call": "Elige cómo te gustaría unirte a esta llamada",
"choose_how_you_d_like_to_appear_on_the_call": "Elige cómo quieres aparecer en la llamada",
"new_event_trigger": "cuando se reserva un nuevo evento",
"email_host_action": "enviar correo electrónico al anfitrión",
"email_attendee_action": "enviar correo electrónico a los asistentes",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Tienes un equipo",
"consider_consolidating_one_team_org": "Considera configurar una organización para unificar la facturación, herramientas de administración y análisis en tu equipo.",
"consider_consolidating_multi_team_org": "Considera configurar una organización para unificar la facturación, herramientas de administración y análisis en tus equipos.",
"message_sent": "Mensaje enviado",
"support_message_sent_description": "Gracias por contactarnos. Te responderemos lo antes posible.",
"search_variables": "Buscar variables",
"support_message_placeholder": "Por favor describe el problema que estás enfrentando, por ejemplo: 'Los horarios ocupados aparecen como disponibles', ..., etc.",
"send_another_message": "Enviar otro mensaje",
"sending": "Enviando",
"no_variables_found": "No se encontraron variables",
"api_key_name_too_long": "El nombre no debe exceder los {{max}} caracteres",
"auto_charge_for_last_minute_cancellation": "Cobrar automáticamente la tarifa por no presentarse en caso de cancelaciones de último minuto",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Precio",
"paid": "Pagado",
"refunded": "Reembolsado",
"record": "Grabar",
"transcribe": "Transcribir",
"payment": "Pago",
"missing_card_fields": "Faltan campos de la tarjeta",
"pay_now": "Paga ahora",
"general_prompt": "Mensaje General",
"transcription_powered_by_ai": "Transcripción impulsada por IA",
"start_or_stop_recording": "Iniciar o detener grabación",
"begin_message": "Iniciar Mensaje",
"codebase_has_to_stay_opensource": "El código base debe permanecer en código abierto, si fue modificado o no",
"cannot_repackage_codebase": "No puede reempaquetar o vender el código base",
@@ -661,6 +665,8 @@
"somewhere_else": "Otro lugar",
"custom_attendee_location": "Ubicación personalizada del asistente",
"continue_as_guest": "Continuar como invitado",
"join_as_guest": "Unirse como invitado",
"ideal_for_one_time_calls": "Ideal para llamadas únicas",
"any_location": "Cualquier ubicación",
"yes": "Sí",
"no": "No",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Después de que los anfitriones no se unieron al video de Cal",
"after_guests_cal_video_no_show": "Después de que los invitados no se unieron al video de Cal",
"join_video_call": "Unirse a la videollamada",
"sign_in_to_cal_com": "Inicia sesión en tu cuenta de Cal.com",
"sign_in_to_cal_com": "Iniciar sesión en Cal.com",
"log_in_to_cal_com": "Iniciar sesión en Cal.com",
"track_your_meetings": "Haz seguimiento de tus reuniones, recibe notificaciones y gestiona tu agenda de manera más efectiva.",
"track_meetings_and_manage_schedule": "Haz seguimiento de tus reuniones y gestiona tu agenda de manera más efectiva",
"failed_to_join_call": "No se pudo unir a la llamada. Por favor, inténtalo de nuevo.",
"choose_how_you_d_like_to_join_call": "Elige cómo te gustaría unirte a esta llamada",
"choose_how_you_d_like_to_appear_on_the_call": "Elige cómo quieres aparecer en la llamada",
"new_event_trigger": "cuando se reserva un nuevo evento",
"email_host_action": "enviar correo electrónico al anfitrión",
"email_attendee_action": "enviar correo electrónico a los asistentes",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Tienes un equipo",
"consider_consolidating_one_team_org": "Considera configurar una organización para unificar la facturación, herramientas de administración y análisis en tu equipo.",
"consider_consolidating_multi_team_org": "Considera configurar una organización para unificar la facturación, herramientas de administración y análisis en tus equipos.",
"message_sent": "Mensaje enviado",
"support_message_sent_description": "Gracias por contactarnos. Te responderemos lo antes posible.",
"search_variables": "Buscar variables",
"support_message_placeholder": "Por favor describe el problema que estás enfrentando, por ejemplo: 'Los horarios ocupados aparecen como disponibles', ..., etc.",
"send_another_message": "Enviar otro mensaje",
"sending": "Enviando",
"no_variables_found": "No se encontraron variables",
"api_key_name_too_long": "El nombre no debe exceder los {{max}} caracteres",
"auto_charge_for_last_minute_cancellation": "Cobrar automáticamente la tarifa por no presentarse en caso de cancelaciones de último minuto",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Hind",
"paid": "Tasuline",
"refunded": "Tagastatud",
"record": "Salvesta",
"transcribe": "Transkribeeri",
"payment": "Makse",
"missing_card_fields": "Kaardiväljad puuduvad",
"pay_now": "Maksa nüüd",
"general_prompt": "Üldine viip",
"transcription_powered_by_ai": "Transkriptsioon tehisintellekti toel",
"start_or_stop_recording": "Alusta või lõpeta salvestamine",
"begin_message": "Alusta sõnumit",
"codebase_has_to_stay_opensource": "Koodibaas peab jääma avatud lähtekoodiga, olenemata sellest, kas seda muudeti või mitte",
"cannot_repackage_codebase": "Te ei tohi koodibaasi ümber pakkida ega müüa",
@@ -661,6 +665,8 @@
"somewhere_else": "Mujal",
"custom_attendee_location": "Kohandatud osaleja asukoht",
"continue_as_guest": "Jätka külalisena",
"join_as_guest": "Liitu külalisena",
"ideal_for_one_time_calls": "Ideaalne ühekordseteks kõnedeks",
"any_location": "Mis tahes asukoht",
"yes": "Jah",
"no": "Ei",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Pärast seda, kui võõrustajad ei liitunud cal videoga",
"after_guests_cal_video_no_show": "Pärast seda, kui külalised ei liitunud cal videoga",
"join_video_call": "Liitu videokõnega",
"sign_in_to_cal_com": "Logige sisse oma Cal.com kontole",
"sign_in_to_cal_com": "Logi sisse Cal.com-i",
"log_in_to_cal_com": "Logi sisse Cal.com-i",
"track_your_meetings": "Jälgige oma kohtumisi, saage teavitusi ja hallake oma ajakava tõhusamalt.",
"track_meetings_and_manage_schedule": "Jälgi oma kohtumisi ja halda oma ajakava tõhusamalt",
"failed_to_join_call": "Kõnega liitumine ebaõnnestus. Palun proovi uuesti.",
"choose_how_you_d_like_to_join_call": "Valige, kuidas soovite selle kõnega liituda",
"choose_how_you_d_like_to_appear_on_the_call": "Vali, kuidas soovid kõnes osaleda",
"new_event_trigger": "kui uus sündmus on broneeritud",
"email_host_action": "saada e-kiri hostile",
"email_attendee_action": "saada osalejatele e-kiri",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Teil on üks meeskond",
"consider_consolidating_one_team_org": "Kaaluge organisatsiooni loomist, et ühtlustada arveldamist, haldustööriistu ja analüütikat oma meeskonnas.",
"consider_consolidating_multi_team_org": "Kaaluge organisatsiooni loomist, et ühtlustada arveldamist, haldustööriistu ja analüütikat oma meeskondade vahel.",
"message_sent": "Sõnum saadetud",
"support_message_sent_description": "Täname, et võtsite meiega ühendust. Vastame teile esimesel võimalusel.",
"search_variables": "Otsi muutujaid",
"support_message_placeholder": "Palun kirjeldage probleemi, millega silmitsi seisate, nt 'Hõivatud ajad on märgitud saadavaks', ..., jne.",
"send_another_message": "Saada uus sõnum",
"sending": "Saatmine",
"no_variables_found": "Muutujaid ei leitud",
"api_key_name_too_long": "Nimi ei tohi ületada {{max}} tähemärki",
"auto_charge_for_last_minute_cancellation": "Viimase hetke tühistamiste eest puudumise tasu automaatne arvestamine",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Prezioa",
"paid": "Ordainduta",
"refunded": "Itzulita",
"record": "Grabatu",
"transcribe": "Transkribatu",
"payment": "Ordainketa",
"missing_card_fields": "Txartelaren eremuak falta dira",
"pay_now": "Ordaindu orain",
"general_prompt": "Prompt orokorra",
"transcription_powered_by_ai": "Transkripzioa adimen artifizialak bultzatuta",
"start_or_stop_recording": "Hasi edo gelditu grabaketa",
"begin_message": "Hasi mezua",
"codebase_has_to_stay_opensource": "Kode-basea kode irekia izan behar da, aldatuta egon ala ez",
"cannot_repackage_codebase": "Ezin duzu kode-basea berriz paketatu edo saldu",
@@ -661,6 +665,8 @@
"somewhere_else": "Beste nonbait",
"custom_attendee_location": "Parte-hartzailearen kokapen pertsonalizatua",
"continue_as_guest": "Jarraitu gonbidatu gisa",
"join_as_guest": "Sartu gonbidatu gisa",
"ideal_for_one_time_calls": "Ezin hobea aldi bakarreko deietarako",
"any_location": "Edozein kokapen",
"yes": "Bai",
"no": "Ez",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Ostalariak Cal bideora batu ez direnean",
"after_guests_cal_video_no_show": "Gonbidatuak Cal bideora batu ez direnean",
"join_video_call": "Batu bideo-deira",
"sign_in_to_cal_com": "Hasi saioa zure Cal.com kontuan",
"sign_in_to_cal_com": "Hasi saioa Cal.com-en",
"log_in_to_cal_com": "Hasi saioa Cal.com-en",
"track_your_meetings": "Jarraitu zure bilerak, jaso jakinarazpenak, eta kudeatu zure egutegia modu eraginkorragoan.",
"track_meetings_and_manage_schedule": "Jarraitu zure bilerak eta kudeatu zure egutegia modu eraginkorragoan",
"failed_to_join_call": "Ezin izan da deira batu. Saiatu berriro.",
"choose_how_you_d_like_to_join_call": "Aukeratu nola nahi duzun dei honetara batzea",
"choose_how_you_d_like_to_appear_on_the_call": "Aukeratu nola agertu nahi duzun deian",
"new_event_trigger": "gertaera berri bat erreserbatzen denean",
"email_host_action": "bidali emaila anfitrioiari",
"email_attendee_action": "bidali emaila partaideei",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Talde bat duzu",
"consider_consolidating_one_team_org": "Pentsatu erakunde bat sortzeaz zure taldearen fakturazioa, administrazio-tresnak eta analitika bateratzeko.",
"consider_consolidating_multi_team_org": "Pentsatu erakunde bat sortzeaz zure taldeen fakturazioa, administrazio-tresnak eta analitika bateratzeko.",
"message_sent": "Mezua bidali da",
"support_message_sent_description": "Eskerrik asko gurekin harremanetan jartzeagatik. Ahalik eta lasterren erantzungo dizugu.",
"search_variables": "Bilatu aldagaiak",
"support_message_placeholder": "Mesedez, deskribatu duzun arazoa, adibidez: 'Okupatuta dauden tarteak libre gisa agertzen dira', ..., etab.",
"send_another_message": "Bidali beste mezu bat",
"sending": "Bidaltzen",
"no_variables_found": "Ez da aldagairik aurkitu",
"api_key_name_too_long": "Izenak ezin ditu {{max}} karaktere baino gehiago izan",
"auto_charge_for_last_minute_cancellation": "Azken minutuko ezeztapenengatik ez agertzeko tasa automatikoki kobratu",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Hinta",
"paid": "Maksettu",
"refunded": "Palautettu",
"record": "Tallenna",
"transcribe": "Litteroi",
"payment": "Maksu",
"missing_card_fields": "Kortin tiedot puuttuvat",
"pay_now": "Maksa nyt",
"general_prompt": "Yleinen kehote",
"transcription_powered_by_ai": "Litterointi tekoälyn avulla",
"start_or_stop_recording": "Aloita tai lopeta tallennus",
"begin_message": "Aloita viesti",
"codebase_has_to_stay_opensource": "Lähdekoodin on pysyttävä avoimena, riippumatta siitä onko sitä muokattu vai ei",
"cannot_repackage_codebase": "Et voi paketoida tai myydä lähdekoodia uudelleen",
@@ -661,6 +665,8 @@
"somewhere_else": "Jossain muualla",
"custom_attendee_location": "Mukautettu osallistujan sijainti",
"continue_as_guest": "Jatka vieraana",
"join_as_guest": "Liity vieraana",
"ideal_for_one_time_calls": "Ihanteellinen kertaluontoisiin puheluihin",
"any_location": "Mikä tahansa sijainti",
"yes": "Kyllä",
"no": "Ei",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Kun järjestäjät eivät liittyneet cal-videoon",
"after_guests_cal_video_no_show": "Kun vieraat eivät liittyneet cal-videoon",
"join_video_call": "Liity videopuheluun",
"sign_in_to_cal_com": "Kirjaudu Cal.com-tilillesi",
"sign_in_to_cal_com": "Kirjaudu Cal.comiin",
"log_in_to_cal_com": "Kirjaudu Cal.comiin",
"track_your_meetings": "Seuraa tapaamisiasi, saa ilmoituksia ja hallitse aikatauluasi tehokkaammin.",
"track_meetings_and_manage_schedule": "Seuraa tapaamisiasi ja hallitse aikatauluasi tehokkaammin",
"failed_to_join_call": "Puheluun liittyminen epäonnistui. Yritä uudelleen.",
"choose_how_you_d_like_to_join_call": "Valitse, miten haluat liittyä tähän puheluun",
"choose_how_you_d_like_to_appear_on_the_call": "Valitse, miten haluat näkyä puhelussa",
"new_event_trigger": "kun uusi tapahtuma varataan",
"email_host_action": "lähetä sähköposti järjestäjälle",
"email_attendee_action": "lähetä sähköposti osallistujille",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Sinulla on yksi tiimi",
"consider_consolidating_one_team_org": "Harkitse organisaation perustamista yhdistääksesi laskutuksen, hallintatyökalut ja analytiikan tiimisi kesken.",
"consider_consolidating_multi_team_org": "Harkitse organisaation perustamista yhdistääksesi laskutuksen, hallintatyökalut ja analytiikan tiimiesi kesken.",
"message_sent": "Viesti lähetetty",
"support_message_sent_description": "Kiitos yhteydenotostasi. Palaamme asiaan mahdollisimman pian.",
"search_variables": "Etsi muuttujia",
"support_message_placeholder": "Kuvaile kohtaamasi ongelma, esim. 'Varatut ajat näkyvät vapaina', ..., jne.",
"send_another_message": "Lähetä toinen viesti",
"sending": "Lähetetään",
"no_variables_found": "Muuttujia ei löytynyt",
"api_key_name_too_long": "Nimi ei saa ylittää {{max}} merkkiä",
"auto_charge_for_last_minute_cancellation": "Veloita automaattisesti peruutusmaksu viime hetken peruutuksista",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Prix",
"paid": "Payé",
"refunded": "Remboursé",
"record": "Enregistrer",
"transcribe": "Transcrire",
"payment": "Paiement",
"missing_card_fields": "Champs de carte manquants",
"pay_now": "Payer maintenant",
"general_prompt": "Invite générale",
"transcription_powered_by_ai": "Transcription alimentée par l'IA",
"start_or_stop_recording": "Démarrer ou arrêter l'enregistrement",
"begin_message": "Commencer le message",
"codebase_has_to_stay_opensource": "La base de code doit rester open source, qu'elle ait été modifiée ou non",
"cannot_repackage_codebase": "Vous ne pouvez pas restructurer ou vendre la base de code",
@@ -661,6 +665,8 @@
"somewhere_else": "Autre lieu",
"custom_attendee_location": "Lieu personnalisé du participant",
"continue_as_guest": "Continuer en tant qu'invité",
"join_as_guest": "Rejoindre en tant qu'invité",
"ideal_for_one_time_calls": "Idéal pour les appels ponctuels",
"any_location": "N'importe quel lieu",
"yes": "Oui",
"no": "Non",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Après que les hôtes ne se sont pas connectés à l'appel vidéo",
"after_guests_cal_video_no_show": "Après que les invités ne se sont pas connectés à l'appel vidéo",
"join_video_call": "Rejoindre l'appel vidéo",
"sign_in_to_cal_com": "Connectez-vous à votre compte Cal.com",
"sign_in_to_cal_com": "Se connecter à Cal.com",
"log_in_to_cal_com": "Connectez-vous à Cal.com",
"track_your_meetings": "Suivez vos réunions, recevez des notifications et gérez votre emploi du temps plus efficacement.",
"track_meetings_and_manage_schedule": "Suivez vos réunions et gérez votre emploi du temps plus efficacement",
"failed_to_join_call": "Échec de la connexion à l'appel. Veuillez réessayer.",
"choose_how_you_d_like_to_join_call": "Choisissez comment vous souhaitez rejoindre cet appel",
"choose_how_you_d_like_to_appear_on_the_call": "Choisissez comment vous souhaitez apparaître lors de l'appel",
"new_event_trigger": "lorsqu'un nouvel événement est réservé",
"email_host_action": "envoyer un e-mail à l'hôte",
"email_attendee_action": "envoyer un e-mail aux participants",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Vous avez une équipe",
"consider_consolidating_one_team_org": "Envisagez de créer une organisation pour unifier la facturation, les outils d'administration et les analyses pour votre équipe.",
"consider_consolidating_multi_team_org": "Envisagez de créer une organisation pour unifier la facturation, les outils d'administration et les analyses pour vos équipes.",
"message_sent": "Message envoyé",
"support_message_sent_description": "Merci de nous avoir contactés. Nous vous répondrons dès que possible.",
"search_variables": "Rechercher des variables",
"support_message_placeholder": "Veuillez décrire le problème que vous rencontrez, par ex. \"Les créneaux occupés sont marqués comme disponibles\", ..., etc.",
"send_another_message": "Envoyer un autre message",
"sending": "Envoi en cours",
"no_variables_found": "Aucune variable trouvée",
"api_key_name_too_long": "Le nom ne doit pas dépasser {{max}} caractères",
"auto_charge_for_last_minute_cancellation": "Facturer automatiquement des frais de non-présentation pour les annulations de dernière minute",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "מחיר",
"paid": "שולם",
"refunded": "ההחזר הכספי נשלח",
"record": "הקלט",
"transcribe": "תמלל",
"payment": "תשלום",
"missing_card_fields": "שדות פרטי כרטיס חסרים",
"pay_now": "לשלם עכשיו",
"general_prompt": "בקשה כללית",
"transcription_powered_by_ai": "תמלול מופעל על ידי בינה מלאכותית",
"start_or_stop_recording": "התחל או הפסק הקלטה",
"begin_message": "התחלת הודעה",
"codebase_has_to_stay_opensource": "בין אם בוצעו שינויים בבסיס הקוד ובין אם לא, הקוד חייב להישאר פתוח",
"cannot_repackage_codebase": "לא ניתן לארוז מחדש או למכור את בסיס הקוד",
@@ -661,6 +665,8 @@
"somewhere_else": "מקום אחר",
"custom_attendee_location": "מיקום מותאם אישית של המשתתף",
"continue_as_guest": "המשך כאורח",
"join_as_guest": "הצטרף כאורח",
"ideal_for_one_time_calls": "אידיאלי לשיחות חד-פעמיות",
"any_location": "כל מיקום",
"yes": "כן",
"no": "לא",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "לאחר שהמארחים לא הצטרפו לשיחת הוידאו",
"after_guests_cal_video_no_show": "לאחר שהאורחים לא הצטרפו לשיחת הוידאו",
"join_video_call": "הצטרף לשיחת וידאו",
"sign_in_to_cal_com": "התחבר לחשבון Cal.com שלך",
"sign_in_to_cal_com": "התחבר ל-Cal.com",
"log_in_to_cal_com": "התחבר ל-Cal.com",
"track_your_meetings": "עקוב אחר הפגישות שלך, קבל התראות, ונהל את לוח הזמנים שלך ביעילות רבה יותר.",
"track_meetings_and_manage_schedule": "עקוב אחר הפגישות שלך ונהל את לוח הזמנים שלך ביעילות רבה יותר",
"failed_to_join_call": "ההצטרפות לשיחה נכשלה. אנא נסה שוב.",
"choose_how_you_d_like_to_join_call": "בחר כיצד תרצה להצטרף לשיחה זו",
"choose_how_you_d_like_to_appear_on_the_call": "בחר כיצד תרצה להופיע בשיחה",
"new_event_trigger": "כאשר אירוע חדש מוזמן",
"email_host_action": "שלח מייל למארח",
"email_attendee_action": "שליחת דוא\"ל למשתתפים",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "יש לך צוות אחד",
"consider_consolidating_one_team_org": "שקול להקים ארגון כדי לאחד חיובים, כלי ניהול ואנליטיקה עבור הצוות שלך.",
"consider_consolidating_multi_team_org": "שקול להקים ארגון כדי לאחד חיובים, כלי ניהול ואנליטיקה עבור הצוותים שלך.",
"message_sent": "ההודעה נשלחה",
"support_message_sent_description": "תודה שפנית אלינו. נחזור אליך בהקדם האפשרי.",
"search_variables": "חפש משתנים",
"support_message_placeholder": "אנא תאר את הבעיה שאתה נתקל בה, לדוגמה: 'משבצות זמן תפוסות מסומנות כזמינות', ..., וכו'.",
"send_another_message": "שלח הודעה נוספת",
"sending": "שולח",
"no_variables_found": "לא נמצאו משתנים",
"api_key_name_too_long": "השם לא יכול לעלות על {{max}} תווים",
"auto_charge_for_last_minute_cancellation": "חיוב אוטומטי של דמי אי-הופעה עבור ביטולים ברגע האחרון",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Ár",
"paid": "Fizetett",
"refunded": "Visszatérítve",
"record": "Felvétel",
"transcribe": "Átírás",
"payment": "Fizetés",
"missing_card_fields": "Hiányzó kártya adatok",
"pay_now": "Fizetés most",
"general_prompt": "Általános Prompt",
"transcription_powered_by_ai": "Az átírást mesterséges intelligencia biztosítja",
"start_or_stop_recording": "Felvétel indítása vagy leállítása",
"begin_message": "Üzenet indítása",
"codebase_has_to_stay_opensource": "A kódbázisnak nyílt forráskódúnak kell maradnia, akár módosították, akár nem",
"cannot_repackage_codebase": "A kódbázist nem lehet újracsomagolni vagy eladni",
@@ -661,6 +665,8 @@
"somewhere_else": "Valahol máshol",
"custom_attendee_location": "Egyéni résztvevő helyszíne",
"continue_as_guest": "Folytatás vendégként",
"join_as_guest": "Csatlakozás vendégként",
"ideal_for_one_time_calls": "Ideális egyszeri hívásokhoz",
"any_location": "Bármely helyszín",
"yes": "Igen",
"no": "Nem",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Miután a házigazdák nem csatlakoztak a Cal videóhoz",
"after_guests_cal_video_no_show": "Miután a vendégek nem csatlakoztak a Cal videóhoz",
"join_video_call": "Csatlakozás a videóhíváshoz",
"sign_in_to_cal_com": "Jelentkezzen be Cal.com fiókjába",
"sign_in_to_cal_com": "Bejelentkezés a Cal.com-ba",
"log_in_to_cal_com": "Bejelentkezés a Cal.com-ba",
"track_your_meetings": "Kövesse nyomon találkozóit, kapjon értesítéseket és kezelje hatékonyabban a naptárát.",
"track_meetings_and_manage_schedule": "Kövesse nyomon megbeszéléseit és kezelje hatékonyabban időbeosztását",
"failed_to_join_call": "Nem sikerült csatlakozni a híváshoz. Kérjük, próbálja újra.",
"choose_how_you_d_like_to_join_call": "Válassza ki, hogyan szeretne csatlakozni ehhez a híváshoz",
"choose_how_you_d_like_to_appear_on_the_call": "Válassza ki, hogyan szeretne megjelenni a hívásban",
"new_event_trigger": "új esemény lefoglalásakor",
"email_host_action": "küldjön e-mailt a házigazdának",
"email_attendee_action": "küldjön e-mailt a résztvevőknek",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Egy csapatod van",
"consider_consolidating_one_team_org": "Fontold meg egy szervezet létrehozását a számlázás, az adminisztrációs eszközök és az elemzések egyesítéséhez a csapatodban.",
"consider_consolidating_multi_team_org": "Fontold meg egy szervezet létrehozását a számlázás, az adminisztrációs eszközök és az elemzések egyesítéséhez a csapataidban.",
"message_sent": "Üzenet elküldve",
"support_message_sent_description": "Köszönjük, hogy kapcsolatba lépett velünk. A lehető leghamarabb válaszolni fogunk.",
"search_variables": "Változók keresése",
"support_message_placeholder": "Kérjük, írja le a problémát, amivel szembesül, pl. \"A foglalt időpontok szabadként vannak jelölve\", ..., stb.",
"send_another_message": "Másik üzenet küldése",
"sending": "Küldés folyamatban",
"no_variables_found": "Nem találhatók változók",
"api_key_name_too_long": "A név nem haladhatja meg a {{max}} karaktert",
"auto_charge_for_last_minute_cancellation": "Automatikus lemondási díj felszámolása az utolsó pillanatban történő lemondásokért",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Prezzo",
"paid": "Pagato",
"refunded": "Rimborsato",
"record": "Registra",
"transcribe": "Trascrivi",
"payment": "Pagamento",
"missing_card_fields": "Campi della carta mancanti",
"pay_now": "Paga ora",
"general_prompt": "General Prompt",
"transcription_powered_by_ai": "Trascrizione alimentata dall'IA",
"start_or_stop_recording": "Avvia o interrompi la registrazione",
"begin_message": "Inizia messaggio",
"codebase_has_to_stay_opensource": "Il codebase deve rimanere open source, che sia stato modificato o meno",
"cannot_repackage_codebase": "Non è possibile rimontare o vendere il codebase",
@@ -661,6 +665,8 @@
"somewhere_else": "Altrove",
"custom_attendee_location": "Posizione personalizzata del partecipante",
"continue_as_guest": "Continua come ospite",
"join_as_guest": "Partecipa come ospite",
"ideal_for_one_time_calls": "Ideale per chiamate occasionali",
"any_location": "Qualsiasi posizione",
"yes": "Sì",
"no": "No",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Dopo che gli host non si sono uniti alla videochiamata",
"after_guests_cal_video_no_show": "Dopo che gli ospiti non si sono uniti alla videochiamata",
"join_video_call": "Partecipa alla videochiamata",
"sign_in_to_cal_com": "Accedi al tuo account Cal.com",
"sign_in_to_cal_com": "Accedi a Cal.com",
"log_in_to_cal_com": "Accedi a Cal.com",
"track_your_meetings": "Tieni traccia dei tuoi incontri, ricevi notifiche e gestisci il tuo calendario in modo più efficace.",
"track_meetings_and_manage_schedule": "Tieni traccia dei tuoi incontri e gestisci il tuo calendario in modo più efficace",
"failed_to_join_call": "Impossibile partecipare alla chiamata. Riprova.",
"choose_how_you_d_like_to_join_call": "Scegli come vuoi partecipare a questa chiamata",
"choose_how_you_d_like_to_appear_on_the_call": "Scegli come desideri apparire nella chiamata",
"new_event_trigger": "quando viene prenotato un nuovo evento",
"email_host_action": "invia e-mail all'organizzatore",
"email_attendee_action": "invia e-mail ai partecipanti",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Hai un team",
"consider_consolidating_one_team_org": "Considera la creazione di un'organizzazione per unificare fatturazione, strumenti di amministrazione e analisi per il tuo team.",
"consider_consolidating_multi_team_org": "Considera la creazione di un'organizzazione per unificare fatturazione, strumenti di amministrazione e analisi per i tuoi team.",
"message_sent": "Messaggio inviato",
"support_message_sent_description": "Grazie per averci contattato. Ti risponderemo il prima possibile.",
"search_variables": "Cerca variabili",
"support_message_placeholder": "Per favore descrivi il problema che stai riscontrando, ad esempio 'Gli slot occupati sono segnati come disponibili', ..., ecc.",
"send_another_message": "Invia un altro messaggio",
"sending": "Invio in corso",
"no_variables_found": "Nessuna variabile trovata",
"api_key_name_too_long": "Il nome non deve superare i {{max}} caratteri",
"auto_charge_for_last_minute_cancellation": "Addebita automaticamente la penale per cancellazioni dell'ultimo minuto",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "料金",
"paid": "支払い済み",
"refunded": "返金済み",
"record": "録音",
"transcribe": "文字起こし",
"payment": "お支払い",
"missing_card_fields": "カード情報のフィールドが未記入です",
"pay_now": "今すぐ支払う",
"general_prompt": "一般的なプロンプト",
"transcription_powered_by_ai": "AIによる文字起こし",
"start_or_stop_recording": "録音の開始または停止",
"begin_message": "メッセージを開始",
"codebase_has_to_stay_opensource": "コードベースは、変更されたかどうかを問わず、オープンソースでなくてはいけません",
"cannot_repackage_codebase": "コードベースは、再パッケージ化することも、販売することもできません",
@@ -661,6 +665,8 @@
"somewhere_else": "他の場所",
"custom_attendee_location": "カスタム参加者の場所",
"continue_as_guest": "ゲストとして続ける",
"join_as_guest": "ゲストとして参加",
"ideal_for_one_time_calls": "一回限りの通話に最適",
"any_location": "任意の場所",
"yes": "はい",
"no": "いいえ",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "ホストがCalビデオに参加しなかった後",
"after_guests_cal_video_no_show": "ゲストがCalビデオに参加しなかった後",
"join_video_call": "ビデオ通話に参加する",
"sign_in_to_cal_com": "Cal.comアカウントにサインイン",
"sign_in_to_cal_com": "Cal.comにサインイン",
"log_in_to_cal_com": "Cal.comにログイン",
"track_your_meetings": "会議を追跡し、通知を受け取り、より効果的にスケジュールを管理しましょう。",
"track_meetings_and_manage_schedule": "ミーティングを追跡し、スケジュールをより効果的に管理",
"failed_to_join_call": "通話に参加できませんでした。もう一度お試しください。",
"choose_how_you_d_like_to_join_call": "この通話にどのように参加するか選択してください",
"choose_how_you_d_like_to_appear_on_the_call": "通話でどのように表示されたいかを選択してください",
"new_event_trigger": "新しいイベント予約時に",
"email_host_action": "ホストにメールを送信",
"email_attendee_action": "出席者にメールを送る",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "あなたは1つのチームを持っています",
"consider_consolidating_one_team_org": "組織を設定して、チーム全体の請求、管理ツール、分析を統合することを検討してください。",
"consider_consolidating_multi_team_org": "組織を設定して、チーム全体の請求、管理ツール、分析を統合することを検討してください。",
"message_sent": "メッセージを送信しました",
"support_message_sent_description": "お問い合わせありがとうございます。できるだけ早くご返信いたします。",
"search_variables": "変数を検索",
"support_message_placeholder": "直面している問題について説明してください。例:「予約済みの枠が利用可能と表示されている」、...など。",
"send_another_message": "別のメッセージを送信する",
"sending": "送信中",
"no_variables_found": "変数が見つかりません",
"api_key_name_too_long": "名前は{{max}}文字を超えてはいけません",
"auto_charge_for_last_minute_cancellation": "直前のキャンセルに対して自動的に無断キャンセル料を請求する",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "តម្លៃ",
"paid": "បង់ប្រាក់",
"refunded": "សងប្រាក់វិញ។",
"record": "ថត",
"transcribe": "ចម្លង",
"payment": "ការទូទាត់",
"missing_card_fields": "ប្រអប់ កាត ត្រូវតែបំពេញ",
"pay_now": "បង់ប្រាក់ឥឡូវនេះ",
"general_prompt": "ការជំរុញទូទៅ",
"transcription_powered_by_ai": "ការចម្លងដោយប្រើបញ្ញាសិប្បនិម្មិត",
"start_or_stop_recording": "ចាប់ផ្តើម ឬបញ្ឈប់ការថត",
"begin_message": "ចាប់ផ្តើមសារ",
"codebase_has_to_stay_opensource": "មូលដ្ឋានកូដត្រូវតែរក្សាប្រភពបើកចំហ ទោះបីជាវាត្រូវបានកែប្រែឬអត់ក៏ដោយ។",
"cannot_repackage_codebase": "អ្នកមិនអាចវេចខ្ចប់ឡើងវិញ ឬលក់មូលដ្ឋានកូដបានទេ។",
@@ -661,6 +665,8 @@
"somewhere_else": "កន្លែងផ្សេងទៀត",
"custom_attendee_location": "ទីតាំងអ្នកចូលរួមផ្ទាល់ខ្លួន",
"continue_as_guest": "បន្តជាភ្ញៀវ",
"join_as_guest": "ចូលរួមជាភ្ញៀវ",
"ideal_for_one_time_calls": "ល្អបំផុតសម្រាប់ការហៅទូរស័ព្ទតែម្តង",
"any_location": "ទីតាំងណាមួយ",
"yes": "បាទ/ចាស",
"no": "ទេ",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "បន្ទាប់ពីម្ចាស់ផ្ទះមិនបានចូលរួមក្នុងវីដេអូ Cal",
"after_guests_cal_video_no_show": "បន្ទាប់ពីភ្ញៀវមិនបានចូលរួមក្នុងវីដេអូ Cal",
"join_video_call": "ចូលរួមការហៅវីដេអូ",
"sign_in_to_cal_com": "ចូលគណនី Cal.com របស់អ្នក",
"sign_in_to_cal_com": "ចូលគណនីទៅកាន់ Cal.com",
"log_in_to_cal_com": "ចូលទៅកាន់ Cal.com",
"track_your_meetings": "តាមដានការប្រជុំរបស់អ្នក ទទួលបានការជូនដំណឹង និងគ្រប់គ្រងកាលវិភាគរបស់អ្នកកាន់តែមានប្រសិទ្ធភាព។",
"track_meetings_and_manage_schedule": "តាមដានការប្រជុំរបស់អ្នក និងគ្រប់គ្រងកាលវិភាគរបស់អ្នកឱ្យកាន់តែមានប្រសិទ្ធភាព",
"failed_to_join_call": "បរាជ័យក្នុងការចូលរួមការហៅទូរស័ព្ទ។ សូមព្យាយាមម្តងទៀត។",
"choose_how_you_d_like_to_join_call": "ជ្រើសរើសរបៀបដែលអ្នកចង់ចូលរួមការហៅនេះ",
"choose_how_you_d_like_to_appear_on_the_call": "ជ្រើសរើសរបៀបដែលអ្នកចង់បង្ហាញខ្លួននៅលើការហៅទូរស័ព្ទ",
"new_event_trigger": "ពេលមានការកក់ព្រឹត្តិការណ៍ថ្មី",
"email_host_action": "ផ្ញើអ៊ីមែលទៅម្ចាស់",
"email_attendee_action": "ផ្ញើអ៊ីមែលទៅអ្នកចូលរួម",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "អ្នកមានក្រុមមួយ",
"consider_consolidating_one_team_org": "សូមពិចារណាបង្កើតអង្គភាពមួយដើម្បីបង្រួបបង្រួមការចេញវិក្កយបត្រ ឧបករណ៍គ្រប់គ្រង និងការវិភាគនៅក្នុងក្រុមរបស់អ្នក។",
"consider_consolidating_multi_team_org": "សូមពិចារណាបង្កើតអង្គភាពមួយដើម្បីបង្រួបបង្រួមការចេញវិក្កយបត្រ ឧបករណ៍គ្រប់គ្រង និងការវិភាគនៅក្នុងក្រុមរបស់អ្នក។",
"message_sent": "បានផ្ញើសារ",
"support_message_sent_description": "សូមអរគុណសម្រាប់ការទាក់ទងមកយើង។ យើងនឹងឆ្លើយតបទៅអ្នកឱ្យបានឆាប់តាមដែលអាចធ្វើបាន។",
"search_variables": "ស្វែងរកអថេរ",
"support_message_placeholder": "សូមពណ៌នាអំពីបញ្ហាដែលអ្នកកំពុងជួបប្រទះ ឧទាហរណ៍ 'ពេលវេលាដែលរវល់ត្រូវបានសម្គាល់ថាអាចប្រើបាន', ..., ជាដើម។",
"send_another_message": "ផ្ញើសារមួយទៀត",
"sending": "កំពុងផ្ញើ",
"no_variables_found": "រកមិនឃើញអថេរទេ",
"api_key_name_too_long": "ឈ្មោះមិនត្រូវលើសពី {{max}} តួអក្សរទេ",
"auto_charge_for_last_minute_cancellation": "គិតថ្លៃសេវាដោយស្វ័យប្រវត្តិសម្រាប់ការលុបចោលនៅនាទីចុងក្រោយ",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "가격",
"paid": "결제완료",
"refunded": "환불완료",
"record": "녹음",
"transcribe": "텍스트 변환",
"payment": "결제",
"missing_card_fields": "카드 항목 누락",
"pay_now": "지금 결제하십시오.",
"general_prompt": "일반 프롬프트",
"transcription_powered_by_ai": "AI 기반 텍스트 변환",
"start_or_stop_recording": "녹음 시작 또는 중지",
"begin_message": "메시지 시작",
"codebase_has_to_stay_opensource": "코드베이스는 수정여부와 상관 없이 오픈 소스로 유지되어야 합니다.",
"cannot_repackage_codebase": "코드베이스를 재배포 혹은 재판매할 수 없습니다.",
@@ -661,6 +665,8 @@
"somewhere_else": "다른 장소",
"custom_attendee_location": "사용자 지정 참석자 위치",
"continue_as_guest": "게스트로 계속하기",
"join_as_guest": "게스트로 참여",
"ideal_for_one_time_calls": "일회성 통화에 이상적",
"any_location": "어느 위치든",
"yes": "네",
"no": "아니요",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "호스트가 Cal 비디오에 참여하지 않았습니다",
"after_guests_cal_video_no_show": "게스트가 Cal 비디오에 참여하지 않았습니다",
"join_video_call": "화상 통화 참여하기",
"sign_in_to_cal_com": "Cal.com 계정에 로그인하기",
"sign_in_to_cal_com": "Cal.com에 로그인",
"log_in_to_cal_com": "Cal.com에 로그인하기",
"track_your_meetings": "회의를 추적하고, 알림을 받고, 일정을 더 효과적으로 관리하세요.",
"track_meetings_and_manage_schedule": "회의를 추적하고 일정을 더 효과적으로 관리하세요",
"failed_to_join_call": "통화 참여에 실패했습니다. 다시 시도해 주세요.",
"choose_how_you_d_like_to_join_call": "이 통화에 참여하는 방법을 선택하세요",
"choose_how_you_d_like_to_appear_on_the_call": "통화에서 어떻게 표시될지 선택하세요",
"new_event_trigger": "새 이벤트 예약 시",
"email_host_action": "호스트에게 이메일 발송",
"email_attendee_action": "참석자들에게 이메일 전송",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "팀이 하나 있습니다",
"consider_consolidating_one_team_org": "팀 전체의 결제, 관리 도구 및 분석을 통합하기 위해 조직 설정을 고려해 보세요.",
"consider_consolidating_multi_team_org": "여러 팀 전체의 결제, 관리 도구 및 분석을 통합하기 위해 조직 설정을 고려해 보세요.",
"message_sent": "메시지 전송됨",
"support_message_sent_description": "문의해 주셔서 감사합니다. 최대한 빨리 답변 드리겠습니다.",
"search_variables": "변수 검색",
"support_message_placeholder": "겪고 계신 문제를 설명해 주세요. 예: '사용 중인 시간대가 가능한 것으로 표시됨', ... 등",
"send_another_message": "다른 메시지 보내기",
"sending": "전송 중",
"no_variables_found": "변수를 찾을 수 없음",
"api_key_name_too_long": "이름은 {{max}}자를 초과할 수 없습니다",
"auto_charge_for_last_minute_cancellation": "마지막 순간 취소에 대해 노쇼 수수료 자동 청구",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Prijs",
"paid": "Betaald",
"refunded": "Terugbetaald",
"record": "Opnemen",
"transcribe": "Transcriberen",
"payment": "Betaling",
"missing_card_fields": "Velden voor creditcard ontbreken",
"pay_now": "Nu betalen",
"general_prompt": "Algemene Prompt",
"transcription_powered_by_ai": "Transcriptie aangedreven door AI",
"start_or_stop_recording": "Opname starten of stoppen",
"begin_message": "Begin Bericht",
"codebase_has_to_stay_opensource": "De codebase moet open source blijven, ongeacht of deze is gewijzigd of niet",
"cannot_repackage_codebase": "U kunt de codebase niet opnieuw verpakken of verkopen",
@@ -661,6 +665,8 @@
"somewhere_else": "Elders",
"custom_attendee_location": "Aangepaste locatie deelnemer",
"continue_as_guest": "Doorgaan als gast",
"join_as_guest": "Deelnemen als gast",
"ideal_for_one_time_calls": "Ideaal voor eenmalige gesprekken",
"any_location": "Elke locatie",
"yes": "Ja",
"no": "Nee",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Nadat hosts niet bij de cal video zijn gekomen",
"after_guests_cal_video_no_show": "Nadat gasten niet bij de cal video zijn gekomen",
"join_video_call": "Deelnemen aan de videogesprek",
"sign_in_to_cal_com": "Aanmelden bij je Cal.com-account",
"sign_in_to_cal_com": "Inloggen bij Cal.com",
"log_in_to_cal_com": "Inloggen bij Cal.com",
"track_your_meetings": "Houd je afspraken bij, ontvang meldingen en beheer je agenda effectiever.",
"track_meetings_and_manage_schedule": "Volg je afspraken en beheer je agenda effectiever",
"failed_to_join_call": "Kon niet deelnemen aan het gesprek. Probeer het opnieuw.",
"choose_how_you_d_like_to_join_call": "Kies hoe je aan dit gesprek wilt deelnemen",
"choose_how_you_d_like_to_appear_on_the_call": "Kies hoe je wilt verschijnen in het gesprek",
"new_event_trigger": "wanneer een nieuwe gebeurtenis is geboekt",
"email_host_action": "e-mail naar organisator versturen",
"email_attendee_action": "e-mail naar deelnemers versturen",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Je hebt één team",
"consider_consolidating_one_team_org": "Overweeg een organisatie op te zetten om facturering, beheerderstools en analyses voor je team te verenigen.",
"consider_consolidating_multi_team_org": "Overweeg een organisatie op te zetten om facturering, beheerderstools en analyses voor je teams te verenigen.",
"message_sent": "Bericht verzonden",
"support_message_sent_description": "Bedankt voor je bericht. We nemen zo snel mogelijk contact met je op.",
"search_variables": "Variabelen zoeken",
"support_message_placeholder": "Beschrijf het probleem dat je ondervindt, bijv. 'Bezette tijdslots worden als beschikbaar weergegeven', ..., etc.",
"send_another_message": "Nog een bericht versturen",
"sending": "Bezig met verzenden",
"no_variables_found": "Geen variabelen gevonden",
"api_key_name_too_long": "Naam mag niet langer zijn dan {{max}} tekens",
"auto_charge_for_last_minute_cancellation": "Automatisch no-show kosten in rekening brengen voor last-minute annuleringen",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Pris",
"paid": "Betalt",
"refunded": "Tilbakebetalt",
"record": "Ta opp",
"transcribe": "Transkriber",
"payment": "Betaling",
"missing_card_fields": "Manglende kortfelt",
"pay_now": "Betal nå",
"general_prompt": "Generell forespørsel",
"transcription_powered_by_ai": "Transkripsjon drevet av AI",
"start_or_stop_recording": "Start eller stopp opptak",
"begin_message": "Start melding",
"codebase_has_to_stay_opensource": "Kodebasen må forbli åpen kildekode, uansett om den ble endret eller ikke",
"cannot_repackage_codebase": "Du kan ikke pakke om eller selge kodebasen",
@@ -661,6 +665,8 @@
"somewhere_else": "Et annet sted",
"custom_attendee_location": "Tilpasset deltakerlokasjon",
"continue_as_guest": "Fortsett som gjest",
"join_as_guest": "Delta som gjest",
"ideal_for_one_time_calls": "Ideelt for engangsmøter",
"any_location": "Enhver lokasjon",
"yes": "Ja",
"no": "Nei",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Etter at vertene ikke møtte opp til cal-videoen",
"after_guests_cal_video_no_show": "Etter at gjestene ikke møtte opp til cal-videoen",
"join_video_call": "Delta i videosamtalen",
"sign_in_to_cal_com": "Logg inn på Cal.com-kontoen din",
"sign_in_to_cal_com": "Logg inn på Cal.com",
"log_in_to_cal_com": "Logg inn på Cal.com",
"track_your_meetings": "Følg med på møtene dine, få varsler og administrer timeplanen din mer effektivt.",
"track_meetings_and_manage_schedule": "Følg møtene dine og administrer timeplanen din mer effektivt",
"failed_to_join_call": "Kunne ikke delta i samtalen. Vennligst prøv igjen.",
"choose_how_you_d_like_to_join_call": "Velg hvordan du vil delta i denne samtalen",
"choose_how_you_d_like_to_appear_on_the_call": "Velg hvordan du vil vises i samtalen",
"new_event_trigger": "når en ny hendelse er booket",
"email_host_action": "send e-post til vert",
"email_attendee_action": "send e-post til deltakere",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Du har ett team",
"consider_consolidating_one_team_org": "Vurder å opprette en organisasjon for å samle fakturering, adminverktøy og analyser for teamet ditt.",
"consider_consolidating_multi_team_org": "Vurder å opprette en organisasjon for å samle fakturering, adminverktøy og analyser for teamene dine.",
"message_sent": "Melding sendt",
"support_message_sent_description": "Takk for at du kontaktet oss. Vi vil svare deg så snart som mulig.",
"search_variables": "Søk i variabler",
"support_message_placeholder": "Vennligst beskriv problemet du opplever, f.eks. 'Opptatte tidspunkter vises som tilgjengelige', ..., osv.",
"send_another_message": "Send en ny melding",
"sending": "Sender",
"no_variables_found": "Ingen variabler funnet",
"api_key_name_too_long": "Navnet må ikke overstige {{max}} tegn",
"auto_charge_for_last_minute_cancellation": "Auto-belast gebyr for uteblivelse ved avbestillinger i siste liten",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Cena",
"paid": "Zapłacono",
"refunded": "Zwrócono środki",
"record": "Nagrywaj",
"transcribe": "Transkrybuj",
"payment": "Płatność",
"missing_card_fields": "Uzupełnij brakujące dane",
"pay_now": "Zapłać teraz",
"general_prompt": "Ogólne zapytanie",
"transcription_powered_by_ai": "Transkrypcja wspierana przez AI",
"start_or_stop_recording": "Rozpocznij lub zatrzymaj nagrywanie",
"begin_message": "Rozpocznij wiadomość",
"codebase_has_to_stay_opensource": "Baza kodu musi pozostać otwartym źródłem, niezależnie od tego, czy została zmodyfikowana czy nie",
"cannot_repackage_codebase": "Nie możesz przepakować ani sprzedać tego projektu",
@@ -661,6 +665,8 @@
"somewhere_else": "Gdzie indziej",
"custom_attendee_location": "Niestandardowa lokalizacja uczestnika",
"continue_as_guest": "Kontynuuj jako gość",
"join_as_guest": "Dołącz jako gość",
"ideal_for_one_time_calls": "Idealne do jednorazowych rozmów",
"any_location": "Dowolna lokalizacja",
"yes": "Tak",
"no": "Nie",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Po tym, jak gospodarze nie dołączyli do rozmowy wideo",
"after_guests_cal_video_no_show": "Po tym, jak goście nie dołączyli do rozmowy wideo",
"join_video_call": "Dołącz do wideorozmowy",
"sign_in_to_cal_com": "Zaloguj się na swoje konto Cal.com",
"sign_in_to_cal_com": "Zaloguj się do Cal.com",
"log_in_to_cal_com": "Zaloguj się do Cal.com",
"track_your_meetings": "Śledź swoje spotkania, otrzymuj powiadomienia i zarządzaj swoim harmonogramem bardziej efektywnie.",
"track_meetings_and_manage_schedule": "Śledź swoje spotkania i zarządzaj harmonogramem bardziej efektywnie",
"failed_to_join_call": "Nie udało się dołączyć do rozmowy. Spróbuj ponownie.",
"choose_how_you_d_like_to_join_call": "Wybierz, jak chcesz dołączyć do tego połączenia",
"choose_how_you_d_like_to_appear_on_the_call": "Wybierz, jak chcesz się pojawić na rozmowie",
"new_event_trigger": "gdy nowe wydarzenie zostanie zarezerwowane",
"email_host_action": "wyślij wiadomość e-mail do hosta",
"email_attendee_action": "wyślij wiadomość e-mail do uczestników",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Masz jeden zespół",
"consider_consolidating_one_team_org": "Rozważ utworzenie organizacji, aby zjednoczyć rozliczenia, narzędzia administracyjne i analitykę w swoim zespole.",
"consider_consolidating_multi_team_org": "Rozważ utworzenie organizacji, aby zjednoczyć rozliczenia, narzędzia administracyjne i analitykę w swoich zespołach.",
"message_sent": "Wiadomość wysłana",
"support_message_sent_description": "Dziękujemy za kontakt. Odezwiemy się do Ciebie tak szybko, jak to możliwe.",
"search_variables": "Szukaj zmiennych",
"support_message_placeholder": "Proszę opisać problem, z którym się zmagasz, np. 'Zajęte terminy są oznaczone jako dostępne', ..., itp.",
"send_another_message": "Wyślij kolejną wiadomość",
"sending": "Wysyłanie",
"no_variables_found": "Nie znaleziono zmiennych",
"api_key_name_too_long": "Nazwa nie może przekraczać {{max}} znaków",
"auto_charge_for_last_minute_cancellation": "Automatyczne naliczanie opłaty za niepojawienie się w przypadku odwołania w ostatniej chwili",
@@ -125,10 +125,14 @@
"price": "Preço",
"paid": "Pago",
"refunded": "Reembolsado",
"record": "Gravar",
"transcribe": "Transcrever",
"payment": "Pagamento",
"missing_card_fields": "Faltam campos do cartão",
"pay_now": "Pagar agora",
"general_prompt": "Solicitação Geral",
"transcription_powered_by_ai": "Transcrição alimentada por IA",
"start_or_stop_recording": "Iniciar ou parar gravação",
"begin_message": "Inicie a Mensagem",
"codebase_has_to_stay_opensource": "Todo o código deverá permanecer em código aberto, quer seja modificado ou não",
"cannot_repackage_codebase": "Você não pode redistribuir ou vender o código",
@@ -661,6 +665,8 @@
"somewhere_else": "Outro Lugar",
"custom_attendee_location": "Localização personalizada do participante",
"continue_as_guest": "Continuar como convidado",
"join_as_guest": "Entrar como convidado",
"ideal_for_one_time_calls": "Ideal para chamadas únicas",
"any_location": "Qualquer localização",
"yes": "Sim",
"no": "Não",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Após os anfitriões não entrarem na videochamada do Cal",
"after_guests_cal_video_no_show": "Após os convidados não entrarem na videochamada do Cal",
"join_video_call": "Entrar na videochamada",
"sign_in_to_cal_com": "Entre na sua conta Cal.com",
"sign_in_to_cal_com": "Entrar no Cal.com",
"log_in_to_cal_com": "Entrar no Cal.com",
"track_your_meetings": "Acompanhe suas reuniões, receba notificações e gerencie sua agenda com mais eficiência.",
"track_meetings_and_manage_schedule": "Acompanhe suas reuniões e gerencie sua agenda com mais eficiência",
"failed_to_join_call": "Falha ao entrar na chamada. Por favor, tente novamente.",
"choose_how_you_d_like_to_join_call": "Escolha como você gostaria de entrar nesta chamada",
"choose_how_you_d_like_to_appear_on_the_call": "Escolha como você gostaria de aparecer na chamada",
"new_event_trigger": "quando um novo evento for reservado",
"email_host_action": "enviar e-mail para a(o) anfitriã(o)",
"email_attendee_action": "enviar e-mail aos participantes",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Você tem uma equipe",
"consider_consolidating_one_team_org": "Considere configurar uma organização para unificar faturamento, ferramentas administrativas e análises em sua equipe.",
"consider_consolidating_multi_team_org": "Considere configurar uma organização para unificar faturamento, ferramentas administrativas e análises em suas equipes.",
"message_sent": "Mensagem enviada",
"support_message_sent_description": "Obrigado por entrar em contato conosco. Responderemos o mais breve possível.",
"search_variables": "Pesquisar variáveis",
"support_message_placeholder": "Por favor, descreva o problema que você está enfrentando, por exemplo, 'Horários ocupados estão marcados como disponíveis', ..., etc.",
"send_another_message": "Enviar outra mensagem",
"sending": "Enviando",
"no_variables_found": "Nenhuma variável encontrada",
"api_key_name_too_long": "O nome não deve exceder {{max}} caracteres",
"auto_charge_for_last_minute_cancellation": "Cobrar automaticamente taxa de não comparecimento para cancelamentos de última hora",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Preço",
"paid": "Pago",
"refunded": "Reembolsado",
"record": "Gravar",
"transcribe": "Transcrever",
"payment": "Pagamento",
"missing_card_fields": "Faltam campos do cartão",
"pay_now": "Pagar agora",
"general_prompt": "Pedido geral",
"transcription_powered_by_ai": "Transcrição alimentada por IA",
"start_or_stop_recording": "Iniciar ou parar gravação",
"begin_message": "Iniciar mensagem",
"codebase_has_to_stay_opensource": "Todo o código deverá permanecer em código aberto, quer seja modificado ou não",
"cannot_repackage_codebase": "Não pode redistribuir ou vender o código",
@@ -661,6 +665,8 @@
"somewhere_else": "Outro Lugar",
"custom_attendee_location": "Localização personalizada do participante",
"continue_as_guest": "Continuar como convidado",
"join_as_guest": "Entrar como convidado",
"ideal_for_one_time_calls": "Ideal para chamadas únicas",
"any_location": "Qualquer localização",
"yes": "Sim",
"no": "Não",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Após os anfitriões não terem participado do vídeo do Cal",
"after_guests_cal_video_no_show": "Após os convidados não terem participado do vídeo do Cal",
"join_video_call": "Juntar-se à videochamada",
"sign_in_to_cal_com": "Iniciar sessão na sua conta Cal.com",
"sign_in_to_cal_com": "Entrar no Cal.com",
"log_in_to_cal_com": "Iniciar sessão no Cal.com",
"track_your_meetings": "Acompanhe as suas reuniões, receba notificações e gira a sua agenda de forma mais eficaz.",
"track_meetings_and_manage_schedule": "Acompanhe suas reuniões e gerencie sua agenda com mais eficiência",
"failed_to_join_call": "Falha ao entrar na chamada. Por favor, tente novamente.",
"choose_how_you_d_like_to_join_call": "Escolha como gostaria de se juntar a esta chamada",
"choose_how_you_d_like_to_appear_on_the_call": "Escolha como você gostaria de aparecer na chamada",
"new_event_trigger": "quando um novo evento for reservado",
"email_host_action": "enviar email ao anfitrião",
"email_attendee_action": "enviar e-mail aos participantes",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Tem uma equipa",
"consider_consolidating_one_team_org": "Considere configurar uma organização para unificar faturação, ferramentas de administração e análises na sua equipa.",
"consider_consolidating_multi_team_org": "Considere configurar uma organização para unificar faturação, ferramentas de administração e análises nas suas equipas.",
"message_sent": "Mensagem enviada",
"support_message_sent_description": "Obrigado por entrar em contato conosco. Responderemos o mais breve possível.",
"search_variables": "Pesquisar variáveis",
"support_message_placeholder": "Por favor, descreva o problema que está enfrentando, por exemplo, 'Horários ocupados estão marcados como disponíveis', ..., etc.",
"send_another_message": "Enviar outra mensagem",
"sending": "Enviando",
"no_variables_found": "Nenhuma variável encontrada",
"api_key_name_too_long": "O nome não deve exceder {{max}} caracteres",
"auto_charge_for_last_minute_cancellation": "Cobrar automaticamente taxa de não comparecimento para cancelamentos de última hora",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Prețul",
"paid": "Plătit",
"refunded": "Rambursat",
"record": "Înregistrează",
"transcribe": "Transcrie",
"payment": "Plată",
"missing_card_fields": "Lipsesc câmpuri de card",
"pay_now": "Plătește acum",
"general_prompt": "Prompt General",
"transcription_powered_by_ai": "Transcriere realizată cu ajutorul AI",
"start_or_stop_recording": "Pornește sau oprește înregistrarea",
"begin_message": "Începe Mesajul",
"codebase_has_to_stay_opensource": "Codul trebuie să rămână open source, indiferent dacă a fost modificat sau nu",
"cannot_repackage_codebase": "Nu puteți reface sau vinde baza de cod",
@@ -661,6 +665,8 @@
"somewhere_else": "Altundeva",
"custom_attendee_location": "Locație personalizată pentru participant",
"continue_as_guest": "Continuă ca oaspete",
"join_as_guest": "Alătură-te ca invitat",
"ideal_for_one_time_calls": "Ideal pentru apeluri unice",
"any_location": "Orice locație",
"yes": "Da",
"no": "Nu",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "După ce gazdele nu s-au alăturat la cal video",
"after_guests_cal_video_no_show": "După ce oaspeții nu s-au alăturat la cal video",
"join_video_call": "Participă la apelul video",
"sign_in_to_cal_com": "Conectați-vă la contul dvs. Cal.com",
"sign_in_to_cal_com": "Conectează-te la Cal.com",
"log_in_to_cal_com": "Autentificați-vă pe Cal.com",
"track_your_meetings": "Urmăriți întâlnirile, primiți notificări și gestionați-vă programul mai eficient.",
"track_meetings_and_manage_schedule": "Urmărește întâlnirile și gestionează-ți programul mai eficient",
"failed_to_join_call": "Nu s-a reușit conectarea la apel. Te rugăm să încerci din nou.",
"choose_how_you_d_like_to_join_call": "Alegeți cum doriți să vă alăturați acestui apel",
"choose_how_you_d_like_to_appear_on_the_call": "Alege cum dorești să apari în apel",
"new_event_trigger": "când este rezervat noul eveniment",
"email_host_action": "trimiteți e-mail la gazdă",
"email_attendee_action": "trimiteți e-mail participanților",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Aveți o echipă",
"consider_consolidating_one_team_org": "Luați în considerare configurarea unei organizații pentru a unifica facturarea, instrumentele de administrare și analizele pentru echipa dvs.",
"consider_consolidating_multi_team_org": "Luați în considerare configurarea unei organizații pentru a unifica facturarea, instrumentele de administrare și analizele pentru echipele dvs.",
"message_sent": "Mesaj trimis",
"support_message_sent_description": "Vă mulțumim că ne-ați contactat. Vă vom răspunde cât mai curând posibil.",
"search_variables": "Caută variabile",
"support_message_placeholder": "Vă rugăm să descrieți problema cu care vă confruntați, de exemplu: 'Intervalele ocupate sunt marcate ca disponibile', ..., etc.",
"send_another_message": "Trimiteți un alt mesaj",
"sending": "Se trimite",
"no_variables_found": "Nu s-au găsit variabile",
"api_key_name_too_long": "Numele nu trebuie să depășească {{max}} caractere",
"auto_charge_for_last_minute_cancellation": "Taxare automată a taxei de neprezentare pentru anulările de ultim moment",
+15 -1
View File
@@ -125,10 +125,14 @@
"price": "Цена",
"paid": "Оплачено",
"refunded": "Возвращено",
"record": "Запись",
"transcribe": "Транскрибировать",
"payment": "Платеж",
"missing_card_fields": "Пропущены поля карты",
"pay_now": "Оплатить сейчас",
"general_prompt": "Общий запрос",
"transcription_powered_by_ai": "Транскрипция с использованием ИИ",
"start_or_stop_recording": "Начать или остановить запись",
"begin_message": "Начать сообщение",
"codebase_has_to_stay_opensource": "Код должен оставаться открытым исходным кодом, независимо от того, был ли он изменен или нет",
"cannot_repackage_codebase": "Вы не можете переупаковывать или продавать код",
@@ -661,6 +665,8 @@
"somewhere_else": "Другое место",
"custom_attendee_location": "Пользовательское местоположение участника",
"continue_as_guest": "Продолжить как гость",
"join_as_guest": "Присоединиться как гость",
"ideal_for_one_time_calls": "Идеально для разовых звонков",
"any_location": "Любое место",
"yes": "да",
"no": "нет",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "После того, как организаторы не присоединились к видеовстрече cal",
"after_guests_cal_video_no_show": "После того, как гости не присоединились к видеовстрече cal",
"join_video_call": "Присоединиться к видеозвонку",
"sign_in_to_cal_com": "Войдите в свою учетную запись Cal.com",
"sign_in_to_cal_com": "Войти в Cal.com",
"log_in_to_cal_com": "Войти в Cal.com",
"track_your_meetings": "Отслеживайте свои встречи, получайте уведомления и управляйте своим расписанием более эффективно.",
"track_meetings_and_manage_schedule": "Отслеживайте встречи и управляйте расписанием более эффективно",
"failed_to_join_call": "Не удалось присоединиться к звонку. Пожалуйста, попробуйте снова.",
"choose_how_you_d_like_to_join_call": "Выберите, как вы хотите присоединиться к звонку",
"choose_how_you_d_like_to_appear_on_the_call": "Выберите, как вы хотите появляться на звонке",
"new_event_trigger": "когда будет забронировано новое событие",
"email_host_action": "отправить письмо организатору",
"email_attendee_action": "отправить письмо участникам",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "У вас одна команда",
"consider_consolidating_one_team_org": "Рассмотрите возможность создания организации для объединения выставления счетов, административных инструментов и аналитики в вашей команде.",
"consider_consolidating_multi_team_org": "Рассмотрите возможность создания организации для объединения выставления счетов, административных инструментов и аналитики в ваших командах.",
"message_sent": "Сообщение отправлено",
"support_message_sent_description": "Спасибо, что связались с нами. Мы ответим вам как можно скорее.",
"search_variables": "Поиск переменных",
"support_message_placeholder": "Пожалуйста, опишите проблему, с которой вы столкнулись, например: 'Занятые слоты отмечены как доступные', ..., и т. д.",
"send_another_message": "Отправить ещё одно сообщение",
"sending": "Отправка",
"no_variables_found": "Переменные не найдены",
"api_key_name_too_long": "Имя не должно превышать {{max}} символов",
"auto_charge_for_last_minute_cancellation": "Автоматическое списание платы за неявку при отмене в последний момент",
@@ -125,10 +125,14 @@
"price": "Cena",
"paid": "Zaplatené",
"refunded": "Vrátené",
"record": "Nahrávať",
"transcribe": "Prepísať",
"payment": "Platba",
"missing_card_fields": "Chýbajúce polia karty",
"pay_now": "Zaplatiť teraz",
"general_prompt": "Všeobecná výzva",
"transcription_powered_by_ai": "Prepis pomocou umelej inteligencie",
"start_or_stop_recording": "Spustiť alebo zastaviť nahrávanie",
"begin_message": "Začať správu",
"codebase_has_to_stay_opensource": "Základ kódu musí zostať open source, či už bol upravený alebo nie",
"cannot_repackage_codebase": "Nemôžete prebalovať alebo predávať základ kódu",
@@ -661,6 +665,8 @@
"somewhere_else": "Niekde inde",
"custom_attendee_location": "Vlastné miesto účastníka",
"continue_as_guest": "Pokračovať ako hosť",
"join_as_guest": "Pripojiť sa ako hosť",
"ideal_for_one_time_calls": "Ideálne pre jednorazové hovory",
"any_location": "Ľubovoľné miesto",
"yes": "Áno",
"no": "Nie",
@@ -1415,10 +1421,13 @@
"after_hosts_cal_video_no_show": "Po tom, čo sa hostitelia nepripojili na cal video",
"after_guests_cal_video_no_show": "Po tom, čo sa hostia nepripojili na cal video",
"join_video_call": "Pripojiť sa k videohovoru",
"sign_in_to_cal_com": "Prihláste sa do svojho Cal.com účtu",
"sign_in_to_cal_com": "Prihlásiť sa do Cal.com",
"log_in_to_cal_com": "Prihlásiť sa do Cal.com",
"track_your_meetings": "Sledujte svoje stretnutia, dostávajte upozornenia a efektívnejšie spravujte svoj rozvrh.",
"track_meetings_and_manage_schedule": "Sledujte svoje stretnutia a efektívnejšie spravujte svoj rozvrh",
"failed_to_join_call": "Nepodarilo sa pripojiť k hovoru. Skúste to znova.",
"choose_how_you_d_like_to_join_call": "Vyberte, ako sa chcete pripojiť k tomuto hovoru",
"choose_how_you_d_like_to_appear_on_the_call": "Vyberte si, ako chcete byť zobrazený v hovore",
"new_event_trigger": "keď je rezervovaná nová udalosť",
"email_host_action": "poslať e-mail hostiteľovi",
"email_attendee_action": "poslať e-mail účastníkom",
@@ -3603,7 +3612,12 @@
"you_have_one_team": "Máte jeden tím",
"consider_consolidating_one_team_org": "Zvážte vytvorenie organizácie na zjednotenie fakturácie, administratívnych nástrojov a analytiky v rámci vášho tímu.",
"consider_consolidating_multi_team_org": "Zvážte vytvorenie organizácie na zjednotenie fakturácie, administratívnych nástrojov a analytiky v rámci vašich tímov.",
"message_sent": "Správa odoslaná",
"support_message_sent_description": "Ďakujeme, že ste nás kontaktovali. Ozveme sa vám čo najskôr.",
"search_variables": "Vyhľadať premenné",
"support_message_placeholder": "Prosím, opíšte problém, ktorému čelíte, napr. 'Obsadené termíny sú označené ako dostupné', ..., atď.",
"send_another_message": "Poslať ďalšiu správu",
"sending": "Odosielanie",
"no_variables_found": "Neboli nájdené žiadne premenné",
"api_key_name_too_long": "Názov nesmie presiahnuť {{max}} znakov",
"auto_charge_for_last_minute_cancellation": "Automaticky účtovať poplatok za neúčasť pri zrušení na poslednú chvíľu",

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