Merge branch 'main' into feat/organizations
This commit is contained in:
commit
893b1f19a0
|
@ -26,24 +26,24 @@ export default function TwoFactor({ center = true }) {
|
|||
|
||||
return (
|
||||
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||
<Label className="mt-4">{t("2fa_code")}</Label>
|
||||
|
||||
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
|
||||
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
||||
|
||||
<input type="hidden" value={value} {...methods.register("totpCode")} />
|
||||
|
||||
<div className="flex flex-row justify-between">
|
||||
<Input
|
||||
className={className}
|
||||
name="2fa1"
|
||||
inputMode="decimal"
|
||||
{...digits[0]}
|
||||
autoFocus
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
<Input className={className} name="2fa2" inputMode="decimal" {...digits[1]} />
|
||||
<Input className={className} name="2fa3" inputMode="decimal" {...digits[2]} />
|
||||
<Input className={className} name="2fa4" inputMode="decimal" {...digits[3]} />
|
||||
<Input className={className} name="2fa5" inputMode="decimal" {...digits[4]} />
|
||||
<Input className={className} name="2fa6" inputMode="decimal" {...digits[5]} />
|
||||
{digits.map((digit, index) => (
|
||||
<Input
|
||||
key={`2fa${index}`}
|
||||
className={className}
|
||||
name={`2fa${index + 1}`}
|
||||
inputMode="decimal"
|
||||
{...digit}
|
||||
autoFocus={index === 0}
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
|
@ -23,7 +24,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||
|
||||
return (
|
||||
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
|
||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||
<Avatar
|
||||
size="md"
|
||||
alt={member.name || ""}
|
||||
|
@ -36,7 +37,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||
<>
|
||||
<div
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: md.render(markdownToSafeHTML(member.bio)) }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
@ -372,8 +372,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
throw new Error(t("seats_and_no_show_fee_error"));
|
||||
}
|
||||
|
||||
const { availability, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
...input,
|
||||
...rest,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
|
@ -453,9 +454,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { availability, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
...input,
|
||||
...rest,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
|
|
|
@ -12,6 +12,7 @@ import classNames from "@calcom/lib/classNames";
|
|||
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { ChevronRight } from "@calcom/ui/components/icon";
|
||||
|
@ -244,7 +245,7 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
|||
|
||||
<div
|
||||
className="prose-sm prose prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: booking.description }}
|
||||
dangerouslySetInnerHTML={{ __html: markdownToSafeHTML(booking.description) }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
{
|
||||
"identity_provider": "Identidikazioaren hornitzailea",
|
||||
"trial_days_left": "$t(day, {\"count\": {{days}} }) geratzen zaizkizu zure PRO frogan",
|
||||
"day_one": "egun {{count}}",
|
||||
"day_other": "{{count}} egun",
|
||||
"second_one": "segundo {{count}}",
|
||||
"second_other": "{{count}} segundo",
|
||||
"upgrade_now": "Eguneratu orain",
|
||||
"accept_invitation": "Onartu gonbidapena",
|
||||
"have_any_questions": "Galderarik? Laguntzeko gaude.",
|
||||
"reset_password_subject": "{{appName}}: Pasahitza berrezartzeko argibideak",
|
||||
"verify_email_subject": "{{appName}}: egiaztatu zure kontua",
|
||||
"check_your_email": "Begiratu zure emaila",
|
||||
"verify_email_email_header": "Egiaztatu zure email helbidea",
|
||||
"verify_email_email_button": "Egiaztatu emaila",
|
||||
"event_declined_subject": "Baztertua: {{title}} {{date}}(e)an",
|
||||
"event_cancelled_subject": "Bertan behera: {{title}} {{date}}(e)an",
|
||||
"event_request_declined": "Zure gertaera-eskaera baztertua izan da",
|
||||
|
|
|
@ -386,7 +386,7 @@
|
|||
"full_name": "Nom complet",
|
||||
"browse_api_documentation": "Parcourez la documentation de l'API",
|
||||
"leverage_our_api": "Profitez de notre API pour tout personnaliser et contrôler.",
|
||||
"create_webhook": "Créer un Webhook",
|
||||
"create_webhook": "Créer un webhook",
|
||||
"booking_cancelled": "Réservation annulée",
|
||||
"booking_rescheduled": "Réservation replanifiée",
|
||||
"recording_ready": "Lien de téléchargement d'enregistrement prêt",
|
||||
|
@ -920,7 +920,7 @@
|
|||
"offer_seats": "Proposer des places",
|
||||
"offer_seats_description": "Proposez des places de réservation. Cela désactive automatiquement les réservations d'invités et d'opt-in.",
|
||||
"seats_available_one": "Place disponible",
|
||||
"seats_available_other": "places disponibles",
|
||||
"seats_available_other": "Places disponibles",
|
||||
"number_of_seats": "Nombre de places par réservation",
|
||||
"enter_number_of_seats": "Saisir le nombre de sièges",
|
||||
"you_can_manage_your_schedules": "Vous pouvez gérer vos disponibilités sur la page Disponibilité.",
|
||||
|
@ -1178,10 +1178,10 @@
|
|||
"connect_your_calendar_and_link": "Vous pouvez connecter votre calendrier depuis <1>ici</1>.",
|
||||
"default_calendar_selected": "Calendrier par défaut",
|
||||
"hide_from_profile": "Masquer du profil",
|
||||
"event_setup_tab_title": "Configuration de l'événement",
|
||||
"event_setup_tab_title": "Configuration",
|
||||
"event_limit_tab_title": "Limites",
|
||||
"event_limit_tab_description": "Fréquence de réservation",
|
||||
"event_advanced_tab_description": "Paramètres du calendrier et plus...",
|
||||
"event_advanced_tab_description": "Paramètres du calendrier, etc.",
|
||||
"event_advanced_tab_title": "Avancé",
|
||||
"event_setup_multiple_duration_error": "Configuration de l'événement : plusieurs durées requièrent au moins une option.",
|
||||
"event_setup_multiple_duration_default_error": "Configuration de l'événement : veuillez sélectionner une durée par défaut valide.",
|
||||
|
@ -1345,7 +1345,7 @@
|
|||
"triggers_when": "Se déclenche quand",
|
||||
"test_webhook": "Veuillez faire un test de ping avant de le créer.",
|
||||
"enable_webhook": "Activer le webhook",
|
||||
"add_webhook": "Ajouter un Webhook",
|
||||
"add_webhook": "Ajouter un webhook",
|
||||
"webhook_edited_successfully": "Webhook enregistré",
|
||||
"api_keys_description": "Générez des clés API pour accéder à votre propre compte.",
|
||||
"new_api_key": "Nouvelle clé API",
|
||||
|
@ -1541,7 +1541,7 @@
|
|||
"send_code": "Envoyer le code",
|
||||
"number_verified": "Numéro vérifié",
|
||||
"create_your_first_team_webhook_description": "Créez votre premier webhook pour ce type d'événement d'équipe",
|
||||
"create_webhook_team_event_type": "Créer un webhook pour ce type d'événement d'équipe",
|
||||
"create_webhook_team_event_type": "Créez un webhook pour ce type d'événement d'équipe.",
|
||||
"disable_success_page": "Désactiver la page de validation (fonctionne uniquement si vous avez un lien de redirection)",
|
||||
"invalid_admin_password": "Vous êtes administrateur, mais votre mot de passe ne fait pas au moins 15 caractères ou la double authentification n'est pas définie",
|
||||
"change_password_admin": "Changez le mot de passe pour obtenir l'accès administrateur",
|
||||
|
|
|
@ -299,6 +299,18 @@
|
|||
"success": "Uspešno",
|
||||
"failed": "Neuspešno",
|
||||
"password_has_been_reset_login": "Vaša lozinka je resetovana. Sada možete da se ulogujete sa vašom novom lozinkom.",
|
||||
"bookerlayout_title": "Raspored",
|
||||
"bookerlayout_default_title": "Podrazumevani prikaz",
|
||||
"bookerlayout_description": "Možete da izaberete više njih, a vaši polaznici mogu da menjaju prikaze.",
|
||||
"bookerlayout_user_settings_title": "Raspored zakazivanja",
|
||||
"bookerlayout_user_settings_description": "Možete da izaberete više njih, a polaznici mogu da menjaju izglede. Ovo može da bude izmenjeno na osnovu događaja.",
|
||||
"bookerlayout_month_view": "Mesec",
|
||||
"bookerlayout_week_view": "Nedeljno",
|
||||
"bookerlayout_column_view": "Kolona",
|
||||
"bookerlayout_error_min_one_enabled": "Mora da bude omogućen najmanje jedan izgled.",
|
||||
"bookerlayout_error_default_not_enabled": "Izgled koji ste izabrali da bude podrazumevan nije deo omogućenih izgleda.",
|
||||
"bookerlayout_error_unknown_layout": "Izgled koji ste izabrali nije važeći izgled.",
|
||||
"bookerlayout_override_global_settings": "Možete da upravljate ovim za sve tipove događaja u <2>podešavanje / izgled</2> ili <6>izmeni samo za ovaj događaj</6>.",
|
||||
"unexpected_error_try_again": "Došlo je do neočekivane greške. Pokušajte ponovo.",
|
||||
"sunday_time_error": "Nevažeće vreme u nedelju",
|
||||
"monday_time_error": "Nevažeće vreme u ponedeljak",
|
||||
|
@ -1038,6 +1050,7 @@
|
|||
"new_event_trigger": "kada je zakazan novi događaj",
|
||||
"email_host_action": "pošalji imejl domaćinu",
|
||||
"email_attendee_action": "pošalji imejl polaznicima",
|
||||
"sms_attendee_action": "Pošalji SMS polazniku",
|
||||
"sms_number_action": "pošalji SMS na određeni broj",
|
||||
"workflows": "Radni tokovi",
|
||||
"new_workflow_btn": "Novi radni tok",
|
||||
|
@ -1238,6 +1251,7 @@
|
|||
"conferencing_description": "Dodajte svoje omiljene aplikacije za video konferencije za vaše sastanke",
|
||||
"add_conferencing_app": "Dodaj aplikaciju za konferenciju",
|
||||
"password_description": "Upravljajte podešavanjima za lozinke vašeg naloga",
|
||||
"set_up_two_factor_authentication": "Podesite dvostruku proveru identiteta",
|
||||
"we_just_need_basic_info": "Trebaju nam neke osnovne informacije da bismo podesili vaš profil.",
|
||||
"skip": "Preskoči",
|
||||
"do_this_later": "Uradite ovo kasnije",
|
||||
|
@ -1269,6 +1283,7 @@
|
|||
"download_responses_description": "Preuzmite sve odgovore iz obrasca u CSV formatu.",
|
||||
"download": "Preuzmi",
|
||||
"download_recording": "Preuzmi snimak",
|
||||
"recording_from_your_recent_call": "Snimak vašeg nedavnog poziva na {{appName}} je spreman za preuzimanje",
|
||||
"create_your_first_form": "Kreirajte vaš prvi formular",
|
||||
"create_your_first_form_description": "Sa formularima za rutiranje možete postavljati kvalifikaciona pitanja i usmeravati ih do prave osobe ili tipa događaja.",
|
||||
"create_your_first_webhook": "Kreirajte vaš prvi Webhook",
|
||||
|
@ -1454,6 +1469,8 @@
|
|||
"find_the_best_person": "Pronađite najbolju dostupnu osobu i potom rotirajte ostale članove tima.",
|
||||
"fixed_round_robin": "Fiksno kružno dodeljivanje",
|
||||
"add_one_fixed_attendee": "Dodajte jednog fiksnog učesnika i onda rotirajte ostale učesnike.",
|
||||
"calcom_is_better_with_team": "{{appName}} je bolji uz timove",
|
||||
"the_calcom_team": "Tim kompanije {{companyName}}",
|
||||
"add_your_team_members": "Dodajte članove vašeg tima vašim tipovima događaja. Koristite kolektivno zakazivanje da uključite svakoga ili pronađete osobu koja najviše odgovara pomoću zakazivanja kružnim dodeljivanjem.",
|
||||
"booking_limit_reached": "Dostignuta je granica zakazivanja za ovaj tip događaja",
|
||||
"duration_limit_reached": "Dostignuta je granica trajanja za ovaj tip događaja",
|
||||
|
@ -1472,6 +1489,7 @@
|
|||
"navigate_installed_apps": "Idi na instalirane aplikacije",
|
||||
"disabled_calendar": "Ako imate instaliran drugi kalendar, nova zakazivanja će biti dodata na njega. Ako nemate, onda povežite novi kalendar da ne biste propustili nova zakazivanja.",
|
||||
"enable_apps": "Omogući aplikacije",
|
||||
"enable_apps_description": "Omogući aplikacije koje korisnici mogu da integrišu sa {{appName}}",
|
||||
"purchase_license": "Kupovina licence",
|
||||
"already_have_key": "Već imam ključ:",
|
||||
"already_have_key_suggestion": "Kopirajte ovde postojeću CALCOM_LICENSE_KEY promenljivu okruženja.",
|
||||
|
@ -1626,6 +1644,7 @@
|
|||
"booking_questions_title": "Pitanja o zakazivanju",
|
||||
"booking_questions_description": "Prilagodite pitanja postavljana na stranici za zakazivanje",
|
||||
"add_a_booking_question": "Dodajte pitanje",
|
||||
"identifier": "Identifikator",
|
||||
"duplicate_email": "Imejl je duplikat",
|
||||
"booking_with_payment_cancelled": "Plaćanje za ovaj događaj više nije moguće",
|
||||
"booking_with_payment_cancelled_already_paid": "Povraćaj novca za plaćenu rezervaciju je u toku.",
|
||||
|
@ -1666,6 +1685,7 @@
|
|||
"spot_popular_event_types": "Uočite popularne tipove događaja",
|
||||
"spot_popular_event_types_description": "Pogledajte koji tipovi događaja dobijaju najviše klikova i zakazivanja",
|
||||
"no_responses_yet": "Još uvek nema odgovora",
|
||||
"no_routes_defined": "Nema definisanih ruta",
|
||||
"this_will_be_the_placeholder": "Ovo će biti čuvar mesta",
|
||||
"error_booking_event": "Došlo je do greške prilikom zakazivanja događaja, osvežite stranicu i pokušajte ponovo",
|
||||
"timeslot_missing_title": "Nisu izabrani termini",
|
||||
|
@ -1762,6 +1782,16 @@
|
|||
"complete_your_booking": "Završite svoje zakazivanje",
|
||||
"complete_your_booking_subject": "Završite svoje zakazivanje: {{title}} dana {{date}}",
|
||||
"confirm_your_details": "Potvrdite svoje podatke",
|
||||
"copy_invite_link": "Kopiraj link za poziv",
|
||||
"edit_invite_link": "Izmeni podešavanja linka",
|
||||
"invite_link_copied": "Link za poziv je kopiran",
|
||||
"invite_link_deleted": "Link za poziv je izbrisan",
|
||||
"invite_link_updated": "Podešavanja linka za poziv su sačuvana",
|
||||
"link_expires_after": "Link je namešte da istekne za...",
|
||||
"one_day": "1 dan",
|
||||
"seven_days": "7 dana",
|
||||
"thirty_days": "30 dana",
|
||||
"team_invite_received": "Pozvani ste da se pridružite timu {{teamName}}",
|
||||
"currency_string": "{{amount, currency}}",
|
||||
"charge_card_dialog_body": "Upravo ćete naplatiti polazniku {{amount, currency}}. Jeste li sigurni da želite da nastavite?",
|
||||
"charge_attendee": "Naplatite polazniku {{amount, currency}}",
|
||||
|
@ -1791,5 +1821,7 @@
|
|||
"connect_google_workspace": "Poveži Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Morate da budete Workspace administrator da biste koristili ovu opciju",
|
||||
"first_event_type_webhook_description": "Napravite svoj prvi webhook za ovaj tip događaja",
|
||||
"create_for": "Napravi za"
|
||||
"create_for": "Napravi za",
|
||||
"additional_url_parameters": "Dodatni URL parametri",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Header } from "./components/Header";
|
|||
import { LargeCalendar } from "./components/LargeCalendar";
|
||||
import { BookerSection } from "./components/Section";
|
||||
import { Away, NotFound } from "./components/Unavailable";
|
||||
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
||||
import { extraDaysConfig, fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
||||
import { useBookerStore, useInitializeBookerStore } from "./store";
|
||||
import type { BookerProps } from "./types";
|
||||
import { useEvent } from "./utils/event";
|
||||
|
@ -48,9 +48,9 @@ const BookerComponent = ({
|
|||
(state) => [state.selectedTimeslot, state.setSelectedTimeslot],
|
||||
shallow
|
||||
);
|
||||
const extraDays = layout === BookerLayouts.COLUMN_VIEW ? (isTablet ? 2 : 4) : 0;
|
||||
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
|
||||
|
||||
const extraDays = isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop;
|
||||
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
|
||||
const animationScope = useBookerResizeAnimation(layout, bookerState);
|
||||
|
||||
useBrandColors({
|
||||
|
@ -125,7 +125,7 @@ const BookerComponent = ({
|
|||
<EventMeta />
|
||||
{layout !== BookerLayouts.MONTH_VIEW &&
|
||||
!(layout === "mobile" && bookerState === "booking") && (
|
||||
<div className=" mt-auto p-5">
|
||||
<div className=" mt-auto px-5 py-3">
|
||||
<DatePicker />
|
||||
</div>
|
||||
)}
|
||||
|
@ -135,9 +135,9 @@ const BookerComponent = ({
|
|||
<BookerSection
|
||||
key="book-event-form"
|
||||
area="main"
|
||||
className="border-subtle sticky top-0 ml-[-1px] h-full p-5 md:w-[var(--booker-main-width)] md:border-l"
|
||||
className="border-subtle sticky top-0 ml-[-1px] h-full px-5 py-3 md:w-[var(--booker-main-width)] md:border-l"
|
||||
{...fadeInLeft}
|
||||
visible={bookerState === "booking" && layout !== BookerLayouts.COLUMN_VIEW}>
|
||||
visible={bookerState === "booking" && layout === BookerLayouts.MONTH_VIEW}>
|
||||
<BookEventForm onCancel={() => setSelectedTimeslot(null)} />
|
||||
</BookerSection>
|
||||
|
||||
|
@ -147,20 +147,17 @@ const BookerComponent = ({
|
|||
visible={bookerState !== "booking" && layout === BookerLayouts.MONTH_VIEW}
|
||||
{...fadeInLeft}
|
||||
initial="visible"
|
||||
className="md:border-subtle ml-[-1px] h-full flex-shrink p-5 md:border-l lg:w-[var(--booker-main-width)]">
|
||||
className="md:border-subtle ml-[-1px] h-full flex-shrink px-5 py-3 md:border-l lg:w-[var(--booker-main-width)]">
|
||||
<DatePicker />
|
||||
</BookerSection>
|
||||
|
||||
<BookerSection
|
||||
key="large-calendar"
|
||||
area="main"
|
||||
visible={
|
||||
layout === BookerLayouts.WEEK_VIEW &&
|
||||
(bookerState === "selecting_date" || bookerState === "selecting_time")
|
||||
}
|
||||
className="border-muted sticky top-0 ml-[-1px] h-full md:border-l"
|
||||
visible={layout === BookerLayouts.WEEK_VIEW}
|
||||
className="border-subtle sticky top-0 ml-[-1px] h-full md:border-l"
|
||||
{...fadeInLeft}>
|
||||
<LargeCalendar />
|
||||
<LargeCalendar extraDays={extraDays} />
|
||||
</BookerSection>
|
||||
|
||||
<BookerSection
|
||||
|
@ -171,7 +168,7 @@ const BookerComponent = ({
|
|||
layout === BookerLayouts.COLUMN_VIEW
|
||||
}
|
||||
className={classNames(
|
||||
"border-subtle flex h-full w-full flex-col p-5 pb-0 md:border-l",
|
||||
"border-subtle flex h-full w-full flex-col px-5 py-3 pb-0 md:border-l",
|
||||
layout === BookerLayouts.MONTH_VIEW &&
|
||||
"scroll-bar h-full overflow-auto md:w-[var(--booker-timeslots-width)]",
|
||||
layout !== BookerLayouts.MONTH_VIEW && "sticky top-0"
|
||||
|
@ -198,7 +195,7 @@ const BookerComponent = ({
|
|||
</div>
|
||||
|
||||
<BookFormAsModal
|
||||
visible={layout === BookerLayouts.COLUMN_VIEW && bookerState === "booking"}
|
||||
visible={layout !== BookerLayouts.MONTH_VIEW && bookerState === "booking"}
|
||||
onCancel={() => setSelectedTimeslot(null)}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calc
|
|||
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { Calendar, Globe } from "@calcom/ui/components/icon";
|
||||
|
||||
import { fadeInUp } from "../config";
|
||||
|
@ -38,7 +39,7 @@ export const EventMeta = () => {
|
|||
<EventTitle className="my-2">{event?.title}</EventTitle>
|
||||
{event.description && (
|
||||
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
|
||||
<div dangerouslySetInnerHTML={{ __html: event.description }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: markdownToSafeHTML(event.description) }} />
|
||||
</EventMetaBlock>
|
||||
)}
|
||||
<div className="space-y-4 font-medium">
|
||||
|
|
|
@ -28,8 +28,11 @@ export function Header({
|
|||
const selectedDate = dayjs(selectedDateString);
|
||||
|
||||
const onLayoutToggle = useCallback(
|
||||
(newLayout: string) => setLayout(newLayout as BookerLayout),
|
||||
[setLayout]
|
||||
(newLayout: string) => {
|
||||
if (layout === newLayout || !newLayout) return;
|
||||
setLayout(newLayout as BookerLayout);
|
||||
},
|
||||
[setLayout, layout]
|
||||
);
|
||||
|
||||
if (isMobile || !enabledLayouts) return null;
|
||||
|
@ -51,7 +54,7 @@ export function Header({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="border-subtle relative z-10 flex border-l border-b p-4">
|
||||
<div className="border-subtle relative z-10 flex border-l border-b px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="min-w-[150px] text-base font-semibold leading-4">
|
||||
{selectedDate.format("MMM D")}-{selectedDate.add(extraDays, "days").format("D")},{" "}
|
||||
|
@ -74,24 +77,22 @@ export function Header({
|
|||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{enabledLayouts.length > 1 && (
|
||||
<div className="ml-auto flex gap-3">
|
||||
<TimeFormatToggle />
|
||||
<div className="fixed top-4 right-4">
|
||||
<LayoutToggleWithData />
|
||||
</div>
|
||||
{/*
|
||||
<div className="ml-auto flex gap-2">
|
||||
<TimeFormatToggle />
|
||||
<div className="fixed top-4 right-4">
|
||||
<LayoutToggleWithData />
|
||||
</div>
|
||||
{/*
|
||||
This second layout toggle is hidden, but needed to reserve the correct spot in the DIV
|
||||
for the fixed toggle above to fit into. If we wouldn't make it fixed in this view, the transition
|
||||
would be really weird, because the element is positioned fixed in the month view, and then
|
||||
when switching layouts wouldn't anymmore, causing it to animate from the center to the top right,
|
||||
while it actuall already was on place. That's why we have this element twice.
|
||||
*/}
|
||||
<div className="pointer-events-none opacity-0" aria-hidden>
|
||||
<LayoutToggleWithData />
|
||||
</div>
|
||||
<div className="pointer-events-none opacity-0" aria-hidden>
|
||||
<LayoutToggleWithData />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,30 +1,48 @@
|
|||
import { shallow } from "zustand/shallow";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
|
||||
|
||||
import { useBookerStore } from "../store";
|
||||
import { useScheduleForEvent } from "../utils/event";
|
||||
|
||||
export const LargeCalendar = () => {
|
||||
const [setSelectedDate, setSelectedTimeslot] = useBookerStore(
|
||||
(state) => [state.setSelectedDate, state.setSelectedTimeslot],
|
||||
shallow
|
||||
);
|
||||
export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
const schedule = useScheduleForEvent({
|
||||
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
|
||||
});
|
||||
|
||||
const availableSlots = useMemo(() => {
|
||||
const availableTimeslots: CalendarAvailableTimeslots = {};
|
||||
if (!schedule.data) return availableTimeslots;
|
||||
if (!schedule.data.slots) return availableTimeslots;
|
||||
for (const day in schedule.data.slots) {
|
||||
availableTimeslots[day] = schedule.data.slots[day].map((slot) => ({
|
||||
start: dayjs(slot.time).toDate(),
|
||||
end: dayjs(slot.time).add(30, "minutes").toDate(),
|
||||
}));
|
||||
}
|
||||
|
||||
return availableTimeslots;
|
||||
}, [schedule]);
|
||||
|
||||
return (
|
||||
<div className="bg-default dark:bg-muted flex h-full w-full flex-col items-center justify-center">
|
||||
Something big is coming...
|
||||
<br />
|
||||
<button
|
||||
className="max-w-[300px] underline"
|
||||
type="button"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
setSelectedDate(dayjs().format("YYYY-MM-DD"));
|
||||
setSelectedTimeslot(dayjs().format());
|
||||
}}>
|
||||
Click this button to set date + time in one go just like the big thing that is coming here would do.
|
||||
:)
|
||||
</button>
|
||||
<div className="h-full">
|
||||
<Calendar
|
||||
availableTimeslots={availableSlots}
|
||||
startHour={8}
|
||||
endHour={18}
|
||||
events={[]}
|
||||
startDate={selectedDate ? new Date(selectedDate) : new Date()}
|
||||
endDate={dayjs(selectedDate).add(extraDays, "day").toDate()}
|
||||
onEmptyCellClick={(date) => setSelectedTimeslot(date.toString())}
|
||||
gridCellsPerHour={2}
|
||||
hoverEventDuration={30}
|
||||
hideHeader
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -184,3 +184,27 @@ export const useBookerResizeAnimation = (layout: BookerLayout, state: BookerStat
|
|||
|
||||
return animationScope;
|
||||
};
|
||||
|
||||
/**
|
||||
* These configures the amount of days that are shown on top of the selected date.
|
||||
*/
|
||||
export const extraDaysConfig = {
|
||||
mobile: {
|
||||
// Desktop tablet feels weird on mobile layout,
|
||||
// but this is simply here to make the types a lot easier..
|
||||
desktop: 0,
|
||||
tablet: 0,
|
||||
},
|
||||
[BookerLayouts.MONTH_VIEW]: {
|
||||
desktop: 0,
|
||||
tablet: 0,
|
||||
},
|
||||
[BookerLayouts.WEEK_VIEW]: {
|
||||
desktop: 7,
|
||||
tablet: 4,
|
||||
},
|
||||
[BookerLayouts.COLUMN_VIEW]: {
|
||||
desktop: 4,
|
||||
tablet: 2,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -36,19 +36,25 @@ export const AvailableTimes = ({
|
|||
const hasTimeSlots = !!seatsPerTimeslot;
|
||||
const [layout] = useBookerStore((state) => [state.layout], shallow);
|
||||
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
||||
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
|
||||
const isToday = dayjs().isSame(date, "day");
|
||||
|
||||
return (
|
||||
<div className={classNames("text-default", className)}>
|
||||
<header className="bg-default before:bg-default dark:bg-muted dark:before:bg-muted mb-5 flex w-full flex-row items-center font-medium">
|
||||
<span className={classNames(isColumnView && "w-full text-center")}>
|
||||
<span className="text-emphasis font-semibold">
|
||||
<header className="bg-default before:bg-default dark:bg-muted dark:before:bg-muted mb-3 flex w-full flex-row items-center font-medium">
|
||||
<span
|
||||
className={classNames(
|
||||
isColumnView && "w-full text-center",
|
||||
isColumnView ? "text-subtle text-xs uppercase" : "text-emphasis font-semibold"
|
||||
)}>
|
||||
<span className={classNames(isToday && "!text-default")}>
|
||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
isColumnView && isToday ? "bg-brand-default text-brand ml-2" : "text-default",
|
||||
"inline-flex items-center justify-center rounded-3xl px-1 pt-0.5 text-sm font-medium"
|
||||
isColumnView && isToday && "bg-brand-default text-brand ml-2",
|
||||
"inline-flex items-center justify-center rounded-3xl px-1 pt-0.5 font-medium",
|
||||
isMonthView ? "text-default text-sm" : "text-xs"
|
||||
)}>
|
||||
{date.format("DD")}
|
||||
</span>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useCalendarStore } from "../state/store";
|
||||
import "../styles/styles.css";
|
||||
import type { CalendarComponentProps } from "../types/state";
|
||||
import { getDaysBetweenDates, getHoursToDisplay } from "../utils";
|
||||
import { calculateHourSizeInPx, getDaysBetweenDates, getHoursToDisplay } from "../utils";
|
||||
import { DateValues } from "./DateValues";
|
||||
import { BlockedList } from "./blocking/BlockedList";
|
||||
import { EmptyCell } from "./event/Empty";
|
||||
import { EventList } from "./event/EventList";
|
||||
import { SchedulerColumns } from "./grid";
|
||||
|
@ -17,6 +16,7 @@ export function Calendar(props: CalendarComponentProps) {
|
|||
const container = useRef<HTMLDivElement | null>(null);
|
||||
const containerNav = useRef<HTMLDivElement | null>(null);
|
||||
const containerOffset = useRef<HTMLDivElement | null>(null);
|
||||
const schedulerGrid = useRef<HTMLOListElement | null>(null);
|
||||
const initalState = useCalendarStore((state) => state.initState);
|
||||
|
||||
const startDate = useCalendarStore((state) => state.startDate);
|
||||
|
@ -24,12 +24,30 @@ export function Calendar(props: CalendarComponentProps) {
|
|||
const startHour = useCalendarStore((state) => state.startHour || 0);
|
||||
const endHour = useCalendarStore((state) => state.endHour || 23);
|
||||
const usersCellsStopsPerHour = useCalendarStore((state) => state.gridCellsPerHour || 4);
|
||||
const availableTimeslots = useCalendarStore((state) => state.availableTimeslots);
|
||||
const hideHeader = useCalendarStore((state) => state.hideHeader);
|
||||
|
||||
const days = useMemo(() => getDaysBetweenDates(startDate, endDate), [startDate, endDate]);
|
||||
|
||||
const hours = useMemo(() => getHoursToDisplay(startHour || 0, endHour || 23), [startHour, endHour]);
|
||||
|
||||
const numberOfGridStopsPerDay = hours.length * usersCellsStopsPerHour;
|
||||
const [hourSize, setHourSize] = useState(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
|
||||
|
||||
// Reset one hour size on window resize.
|
||||
useLayoutEffect(() => {
|
||||
const onResize = () => {
|
||||
setHourSize(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
// By running this set function also one time in the uselayouteffect, instead of
|
||||
// only in the default value of useState, we make sure that the ref is rendered
|
||||
// to the screen and we read the correct value.
|
||||
setHourSize(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
|
||||
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [startHour, endHour]);
|
||||
|
||||
// Initalise State on inital mount
|
||||
useEffect(() => {
|
||||
|
@ -41,13 +59,18 @@ export function Calendar(props: CalendarComponentProps) {
|
|||
<div
|
||||
className="scheduler-wrapper flex h-full w-full flex-col overflow-y-scroll"
|
||||
style={
|
||||
{ "--one-minute-height": `calc(1.75rem/(60/${usersCellsStopsPerHour}))` } as React.CSSProperties // This can't live in the css file because it's a dynamic value and css variable gets super
|
||||
{
|
||||
"--one-minute-height": `calc(${hourSize}px/60)`,
|
||||
"--gridDefaultSize": `${hourSize}px`,
|
||||
} as React.CSSProperties // This can't live in the css file because it's a dynamic value and css variable gets super
|
||||
}>
|
||||
<SchedulerHeading />
|
||||
<div ref={container} className="bg-default relative isolate flex flex-auto flex-col">
|
||||
{hideHeader !== true && <SchedulerHeading />}
|
||||
<div
|
||||
ref={container}
|
||||
className="bg-default dark:bg-muted relative isolate flex h-full flex-auto flex-col">
|
||||
<div
|
||||
style={{ width: "165%" }}
|
||||
className="flex max-w-full flex-none flex-col sm:max-w-none md:max-w-full">
|
||||
className="flex h-full max-w-full flex-none flex-col sm:max-w-none md:max-w-full">
|
||||
<DateValues containerNavRef={containerNav} days={days} />
|
||||
{/* TODO: Implement this at a later date. */}
|
||||
{/* <CurrentTime
|
||||
|
@ -56,8 +79,14 @@ export function Calendar(props: CalendarComponentProps) {
|
|||
containerRef={container}
|
||||
/> */}
|
||||
<div className="flex flex-auto">
|
||||
<div className="bg-default ring-muted sticky left-0 z-10 w-14 flex-none ring-1" />
|
||||
<div className="grid flex-auto grid-cols-1 grid-rows-1 ">
|
||||
<div className="bg-default dark:bg-muted ring-muted sticky left-0 z-10 w-14 flex-none ring-1" />
|
||||
<div
|
||||
className="grid flex-auto grid-cols-1 grid-rows-1 [--disabled-gradient-foreground:#E6E7EB] [--disabled-gradient-background:#F8F9FB] dark:[--disabled-gradient-background:#262626] dark:[--disabled-gradient-foreground:#393939]"
|
||||
style={{
|
||||
backgroundColor: "var(--disabled-gradient-background)",
|
||||
background:
|
||||
"repeating-linear-gradient(-45deg, var(--disabled-gradient-background), var(--disabled-gradient-background) 2.5px, var(--disabled-gradient-foreground) 2.5px, var(--disabled-gradient-foreground) 5px)",
|
||||
}}>
|
||||
<HorizontalLines
|
||||
hours={hours}
|
||||
numberOfGridStopsPerCell={usersCellsStopsPerHour}
|
||||
|
@ -68,15 +97,16 @@ export function Calendar(props: CalendarComponentProps) {
|
|||
{/* Empty Cells */}
|
||||
<SchedulerColumns
|
||||
zIndex={50}
|
||||
ref={schedulerGrid}
|
||||
offsetHeight={containerOffset.current?.offsetHeight}
|
||||
gridStopsPerDay={numberOfGridStopsPerDay}>
|
||||
<>
|
||||
{[...Array(days.length)].map((_, i) => (
|
||||
<li
|
||||
className="relative"
|
||||
key={i}
|
||||
style={{
|
||||
gridRow: `2 / span ${numberOfGridStopsPerDay}`,
|
||||
position: "relative",
|
||||
}}>
|
||||
{/* While startDate < endDate: */}
|
||||
{[...Array(numberOfGridStopsPerDay)].map((_, j) => {
|
||||
|
@ -89,6 +119,7 @@ export function Calendar(props: CalendarComponentProps) {
|
|||
totalGridCells={numberOfGridStopsPerDay}
|
||||
selectionLength={endHour - startHour}
|
||||
startHour={startHour}
|
||||
availableSlots={availableTimeslots}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -105,7 +136,7 @@ export function Calendar(props: CalendarComponentProps) {
|
|||
return (
|
||||
<li key={day.toISOString()} className="relative" style={{ gridColumnStart: i + 1 }}>
|
||||
<EventList day={day} />
|
||||
<BlockedList day={day} containerRef={container} />
|
||||
{/* <BlockedList day={day} containerRef={container} /> */}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
@ -127,7 +158,7 @@ const MobileNotSupported = ({ children }: { children: React.ReactNode }) => {
|
|||
<h1 className="text-2xl font-bold">Mobile not supported yet </h1>
|
||||
<p className="text-subtle">Please use a desktop browser to view this page</p>
|
||||
</div>
|
||||
<div className="hidden sm:block">{children}</div>
|
||||
<div className="hidden h-full sm:block">{children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ export function DateValues({ days, containerNavRef }: Props) {
|
|||
return (
|
||||
<div
|
||||
ref={containerNavRef}
|
||||
className="bg-default sticky top-0 z-30 flex-none border-b border-b-gray-300 sm:pr-8">
|
||||
className="bg-default dark:bg-muted border-b-subtle sticky top-0 z-30 flex-none border-b sm:pr-8">
|
||||
<div className="text-subtle flex text-sm leading-6 sm:hidden" data-dayslength={days.length}>
|
||||
{days.map((day) => {
|
||||
const isToday = dayjs().isSame(day, "day");
|
||||
|
@ -40,7 +40,10 @@ export function DateValues({ days, containerNavRef }: Props) {
|
|||
return (
|
||||
<div
|
||||
key={day.toString()}
|
||||
className={classNames("flex flex-1 items-center justify-center py-3", isToday && "font-bold")}>
|
||||
className={classNames(
|
||||
"flex flex-1 items-center justify-center py-3 text-xs font-medium uppercase",
|
||||
isToday && "font-bold"
|
||||
)}>
|
||||
<span>
|
||||
{day.format("ddd")}{" "}
|
||||
<span
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import shallow from "zustand/shallow";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
|
|
|
@ -3,12 +3,14 @@ import { classNames } from "@calcom/lib";
|
|||
export function BlockedTimeCell() {
|
||||
return (
|
||||
<div
|
||||
className={classNames("group absolute inset-0 flex h-full flex-col hover:cursor-not-allowed")}
|
||||
className={classNames(
|
||||
"group absolute inset-0 flex h-full flex-col hover:cursor-not-allowed",
|
||||
"[--disabled-gradient-background:#E5E7EB] [--disabled-gradient-foreground:#D1D5DB] dark:[--disabled-gradient-background:#262626] dark:[--disabled-gradient-foreground:#393939]"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: "#D1D5DB",
|
||||
opacity: 0.2,
|
||||
background:
|
||||
"repeating-linear-gradient( -45deg, #E5E7EB, #E5E7EB 4.5px, #D1D5DB 4.5px, #D1D5DB 22.5px )",
|
||||
"repeating-linear-gradient( -45deg, var(--disabled-gradient-background), var(--disabled-gradient-background) 2.5px, var(--disabled-gradient-foreground) 2.5px, var(--disabled-gradient-foreground) 6.5px )",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import shallow from "zustand/shallow";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
import type { CalendarAvailableTimeslots } from "../../types/state";
|
||||
import type { GridCellToDateProps } from "../../utils";
|
||||
import { gridCellToDateTime } from "../../utils";
|
||||
|
||||
export function EmptyCell(props: GridCellToDateProps) {
|
||||
type EmptyCellProps = GridCellToDateProps & {
|
||||
availableSlots?: CalendarAvailableTimeslots;
|
||||
};
|
||||
|
||||
export function EmptyCell(props: EmptyCellProps) {
|
||||
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
|
||||
(state) => ({
|
||||
onEmptyCellClick: state.onEmptyCellClick,
|
||||
|
@ -21,22 +29,41 @@ export function EmptyCell(props: GridCellToDateProps) {
|
|||
startHour: props.startHour,
|
||||
});
|
||||
|
||||
// Empty cell won't show it self (be disabled) when
|
||||
// availableslots is passed in, and it's curren time is not part of the available slots
|
||||
const isDisabled =
|
||||
props.availableSlots &&
|
||||
!props.availableSlots[dayjs(props.day).format("YYYY-MM-DD")]?.find(
|
||||
(slot) => slot.start.getTime() === cellToDate.toDate().getTime()
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group w-full"
|
||||
style={{ height: "1.75rem", overflow: "visible" }}
|
||||
className={classNames(
|
||||
"group w-full",
|
||||
isDisabled && "pointer-events-none",
|
||||
!isDisabled && "bg-default dark:bg-muted"
|
||||
)}
|
||||
data-disabled={isDisabled}
|
||||
data-day={props.day.toISOString()}
|
||||
style={{ height: `calc(${hoverEventDuration}*var(--one-minute-height))`, overflow: "visible" }}
|
||||
onClick={() => onEmptyCellClick && onEmptyCellClick(cellToDate.toDate())}>
|
||||
{hoverEventDuration !== 0 && (
|
||||
{!isDisabled && hoverEventDuration !== 0 && (
|
||||
<div
|
||||
className="opacity-4 bg-subtle hover:bg-emphasis text-emphasis absolute inset-x-1 hidden rounded-[4px]
|
||||
border-[1px]
|
||||
border-gray-900 py-1 px-[6px] text-xs font-semibold leading-5 group-hover:block group-hover:cursor-pointer"
|
||||
className="opacity-4 bg-subtle hover:bg-emphasis text-emphasis dark:border-emphasis absolute hidden
|
||||
rounded-[4px]
|
||||
border-[1px] border-gray-900 py-1 px-[6px] text-xs font-semibold leading-5 group-hover:block group-hover:cursor-pointer"
|
||||
style={{
|
||||
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
|
||||
zIndex: 49,
|
||||
width: "90%",
|
||||
// @TODO: This used to be 90% as per Sean's work. I think this was needed when
|
||||
// multiple events are stacked next to each other. We might need to add this back later.
|
||||
width: "100%",
|
||||
}}>
|
||||
<div className="overflow-ellipsis leading-4">{cellToDate.format("HH:mm")}</div>
|
||||
<div className=" overflow-ellipsis leading-4">
|
||||
{cellToDate.format("HH:mm")}
|
||||
<span className="ml-2 inline">Click to select</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import shallow from "zustand/shallow";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
|
|
|
@ -7,13 +7,17 @@ type Props = {
|
|||
zIndex?: number;
|
||||
};
|
||||
|
||||
export function SchedulerColumns({ offsetHeight, gridStopsPerDay, children, zIndex }: Props) {
|
||||
export const SchedulerColumns = React.forwardRef<HTMLOListElement, Props>(function SchedulerColumns(
|
||||
{ offsetHeight, gridStopsPerDay, children, zIndex },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<ol
|
||||
className="scheduler-grid-row-template col-start-1 col-end-2 row-start-1 grid auto-cols-auto sm:pr-8"
|
||||
ref={ref}
|
||||
className="scheduler-grid-row-template col-start-1 col-end-2 row-start-1 grid auto-cols-auto text-[0px] sm:pr-8"
|
||||
style={{ marginTop: offsetHeight || "var(--gridDefaultSize)", zIndex }}
|
||||
data-gridstopsperday={gridStopsPerDay}>
|
||||
{children}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -15,9 +15,9 @@ export const HorizontalLines = ({
|
|||
const id = useId();
|
||||
return (
|
||||
<div
|
||||
className=" divide-subtle col-start-1 col-end-2 row-start-1 grid divide-y"
|
||||
className=" divide-default pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid divide-y"
|
||||
style={{
|
||||
gridTemplateRows: `repeat(${hours.length}, minmax(${1.75 * numberOfGridStopsPerCell}rem,1fr)`,
|
||||
gridTemplateRows: `repeat(${hours.length}, minmax(var(--gridDefaultSize),1fr)`,
|
||||
}}>
|
||||
<div className="row-end-1 h-7 " ref={containerOffsetRef} />
|
||||
{hours.map((hour) => (
|
||||
|
|
|
@ -3,8 +3,8 @@ import type dayjs from "@calcom/dayjs";
|
|||
export const VeritcalLines = ({ days }: { days: dayjs.Dayjs[] }) => {
|
||||
return (
|
||||
<div
|
||||
className="divide-subtle col-start-1 col-end-2 row-start-1 grid auto-cols-auto grid-rows-1 divide-x
|
||||
sm:pr-8">
|
||||
className="divide-default pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid
|
||||
auto-cols-auto grid-rows-1 divide-x sm:pr-8">
|
||||
{days.map((_, i) => (
|
||||
<div
|
||||
key={`Key_vertical_${i}`}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { TimeRange } from "@calcom/types/schedule";
|
||||
import type { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { CalendarEvent } from "./events";
|
||||
import type { CalendarEvent } from "./events";
|
||||
|
||||
export type View = "month" | "week" | "day";
|
||||
export type Hours =
|
||||
|
@ -51,6 +51,11 @@ export type CalendarPrivateActions = {
|
|||
handleDateChange: (payload: "INCREMENT" | "DECREMENT") => void;
|
||||
};
|
||||
|
||||
export type CalendarAvailableTimeslots = {
|
||||
// Key is the date in YYYY-MM-DD format
|
||||
[key: string]: TimeRange[];
|
||||
};
|
||||
|
||||
export type CalendarState = {
|
||||
/** @NotImplemented This in future will change the view to be daily/weekly/monthly DAY/WEEK are supported currently however WEEK is the most adv.*/
|
||||
view?: View;
|
||||
|
@ -62,6 +67,11 @@ export type CalendarState = {
|
|||
* @Note Ideally you should pass in a sorted array from the DB however, pass the prop `sortEvents` if this is not possible and we will sort this for you..
|
||||
*/
|
||||
events: CalendarEvent[];
|
||||
/**
|
||||
* Instead of letting users choose any option, this will only show these timeslots.
|
||||
* Users can not pick any time themselves but are restricted to the available options.
|
||||
*/
|
||||
availableTimeslots?: CalendarAvailableTimeslots;
|
||||
/** Any time ranges passed in here will display as blocked on the users calendar. Note: Anything < than the current date automatically gets blocked. */
|
||||
blockingDates?: TimeRange[];
|
||||
/** Loading will only expect events to be loading. */
|
||||
|
@ -104,6 +114,10 @@ export type CalendarState = {
|
|||
* @Note It is recommended to sort the events before passing them into the scheduler - e.g. On DB level.
|
||||
*/
|
||||
sortEvents?: boolean;
|
||||
/**
|
||||
* Optional boolean to hide the main header. Default the header will be visible.
|
||||
*/
|
||||
hideHeader?: boolean;
|
||||
};
|
||||
|
||||
export type CalendarComponentProps = CalendarPublicActions & CalendarState;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import dayjs from "@calcom/dayjs";
|
||||
import { TimeRange } from "@calcom/types/schedule";
|
||||
import type { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
// By default starts on Sunday (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
|
||||
export function weekdayDates(weekStart = 0, startDate: Date, length = 6) {
|
||||
|
@ -34,7 +34,7 @@ export function gridCellToDateTime({
|
|||
|
||||
// Add startHour since we use StartOfDay for day props. This could be improved by changing the getDaysBetweenDates function
|
||||
// To handle the startHour+endHour
|
||||
const cellDateTime = dayjs(day).add(minutesIntoSelection, "minutes").add(startHour, "hours");
|
||||
const cellDateTime = dayjs(day).startOf("day").add(minutesIntoSelection, "minutes").add(startHour, "hours");
|
||||
return cellDateTime;
|
||||
}
|
||||
|
||||
|
@ -90,3 +90,17 @@ export function mergeOverlappingDateRanges(dateRanges: TimeRange[]) {
|
|||
}
|
||||
return mergedDateRanges;
|
||||
}
|
||||
|
||||
export function calculateHourSizeInPx(
|
||||
gridElementRef: HTMLOListElement | null,
|
||||
startHour: number,
|
||||
endHour: number
|
||||
) {
|
||||
// Gap added at bottom to give calendar some breathing room.
|
||||
// I guess we could come up with a better way to do this in the future.
|
||||
const gapOnBottom = 50;
|
||||
// In case the calendar has for example a header above it. We take a look at the
|
||||
// distance the grid is rendered from the top, and subtract that from the height.
|
||||
const offsetFromTop = gridElementRef?.getBoundingClientRect().top ?? 65;
|
||||
return (window.innerHeight - offsetFromTop - gapOnBottom) / (endHour - startHour);
|
||||
}
|
||||
|
|
|
@ -131,7 +131,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
</div>
|
||||
|
||||
<Form form={newMemberFormMethods} handleSubmit={(values) => props.onSubmit(values)}>
|
||||
<div className="my-6 space-y-6">
|
||||
<div className="mt-6 mb-10 space-y-6">
|
||||
{/* Indivdual Invite */}
|
||||
{modalImportMode === "INDIVIDUAL" && (
|
||||
<Controller
|
||||
|
|
|
@ -11,6 +11,7 @@ import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import objectKeys from "@calcom/lib/objectKeys";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
@ -265,7 +266,7 @@ const ProfileView = () => {
|
|||
<Label className="text-emphasis mt-5">{t("about")}</Label>
|
||||
<div
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: md.render(markdownToSafeHTML(team.bio)) }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
showToast,
|
||||
TextField,
|
||||
Editor,
|
||||
DialogFooter,
|
||||
} from "@calcom/ui";
|
||||
|
||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
||||
|
@ -159,7 +160,7 @@ export default function CreateEventTypeDialog({
|
|||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-6">
|
||||
<div className="mt-3 space-y-6 pb-10">
|
||||
{teamId && (
|
||||
<TextField
|
||||
type="hidden"
|
||||
|
@ -293,12 +294,12 @@ export default function CreateEventTypeDialog({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-10 flex justify-end gap-x-2">
|
||||
<DialogFooter showDivider>
|
||||
<DialogClose />
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
@ -542,7 +542,7 @@ export const FormBuilder = function FormBuilder({
|
|||
/>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter className="relative rounded px-8 pb-4" showDivider>
|
||||
<DialogFooter className="relative rounded px-8 pb-8" showDivider>
|
||||
<DialogClose color="secondary">{t("cancel")}</DialogClose>
|
||||
<Button data-testid="field-add-save" type="submit" form="form-builder">
|
||||
{isFieldEditMode ? t("save") : t("add")}
|
||||
|
|
|
@ -3,7 +3,7 @@ import sanitizeHtml from "sanitize-html";
|
|||
import { md } from "@calcom/lib/markdownIt";
|
||||
|
||||
export function markdownToSafeHTML(markdown: string | null) {
|
||||
if (!markdown) return null;
|
||||
if (!markdown) return "";
|
||||
|
||||
const html = md.render(markdown);
|
||||
|
||||
|
|
|
@ -45,6 +45,18 @@ export const webhookProcedure = authedProcedure
|
|||
});
|
||||
|
||||
if (webhook) {
|
||||
if (teamId && teamId !== webhook.teamId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
}
|
||||
|
||||
if (eventTypeId && eventTypeId !== webhook.eventTypeId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
}
|
||||
|
||||
if (webhook.teamId) {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
|
|
|
@ -138,7 +138,11 @@ export function DialogFooter(props: { children: ReactNode; className?: string; s
|
|||
return (
|
||||
<div className={classNames("bg-default", props.className)}>
|
||||
{props.showDivider && <hr className="border-subtle absolute right-0 w-full" />}
|
||||
<div className={classNames("-mb-4 flex justify-end space-x-2 pt-4 rtl:space-x-reverse")}>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex justify-end space-x-2 pt-4 rtl:space-x-reverse",
|
||||
props.showDivider && "-mb-4"
|
||||
)}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user