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 2434 additions and 3008 deletions

No files matched your search

+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
Whitespace-only changes.
+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;
if (displayLogInOverlay && !loggedInUserName && !overrideName && !isUserNameConfirmed) {
return;
}
let callFrame: DailyCall | null = null;
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,
},
}
: {}),
},
}),
});
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",
Loaded 100 of 212 files, more files were not shown because too many files have changed in this diff. Show more