Merge branch 'main' into feat/organizations

This commit is contained in:
Leo Giovanetti 2023-06-09 16:46:06 -03:00
commit 893b1f19a0
31 changed files with 332 additions and 131 deletions

View File

@ -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>
);

View File

@ -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)) }}
/>
</>
) : (

View File

@ -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,

View File

@ -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) }}
/>
</>
)}

View File

@ -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",

View File

@ -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",

View File

@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -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)}
/>
</>

View File

@ -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">

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -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,
},
};

View File

@ -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>

View File

@ -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>
</>
);
};

View File

@ -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

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import shallow from "zustand/shallow";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";

View File

@ -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 )",
}}
/>
);

View File

@ -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>

View File

@ -1,4 +1,4 @@
import shallow from "zustand/shallow";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";

View File

@ -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>
);
}
});

View File

@ -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) => (

View File

@ -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}`}

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -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)) }}
/>
</>
)}

View File

@ -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>

View File

@ -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")}

View File

@ -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);

View File

@ -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: {

View File

@ -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>