Feat/i18n crowdin (#752)
* feat: add crowdin and supported languages * fix: main branch name * feat: test crowdin integration * feat: add crowdin config skeleton * feat: update crowdin.yml * fix: remove ro translation * test: en translation * test: en translation * New Crowdin translations by Github Action (#735) Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * test: en translation * fix: separate upload/download workflows * wip * New Crowdin translations by Github Action (#738) Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * wip * wip * wip * wip * wip * typo * wip * wip * update crowdin config * update * chore: support i18n de,es,fr,it,pt,ru,ro,en * chore: extract i18n strings * chore: extract booking components strings for i18n * wip * extract more strings * wip * fallback to getServerSideProps for now * New Crowdin translations by Github Action (#874) Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * fix: minor fixes on the datepicker * fix: add dutch lang * fix: linting issues * fix: string * fix: update GHA * cleanup trpc * fix linting Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
This commit is contained in:
parent
33a683d4b0
commit
2c9b301b77
46 changed files with 2805 additions and 1876 deletions
25
.github/workflows/crowdin.yml
vendored
Normal file
25
.github/workflows/crowdin.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name: Crowdin Action
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
synchronize-with-crowdin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: crowdin action
|
||||||
|
uses: crowdin/github-action@1.4.0
|
||||||
|
with:
|
||||||
|
upload_translations: true
|
||||||
|
download_translations: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
@ -5,11 +5,13 @@ import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { useSlots } from "@lib/hooks/useSlots";
|
import { useSlots } from "@lib/hooks/useSlots";
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
|
|
||||||
type AvailableTimesProps = {
|
type AvailableTimesProps = {
|
||||||
|
localeProp: string;
|
||||||
workingHours: {
|
workingHours: {
|
||||||
days: number[];
|
days: number[];
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
@ -27,6 +29,7 @@ type AvailableTimesProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
|
localeProp,
|
||||||
date,
|
date,
|
||||||
eventLength,
|
eventLength,
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
|
@ -36,6 +39,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
users,
|
users,
|
||||||
schedulingType,
|
schedulingType,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLocale({ localeProp: localeProp });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
|
|
||||||
|
@ -53,8 +57,11 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
<div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:-mb-5">
|
<div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:-mb-5">
|
||||||
<div className="text-gray-600 font-light text-lg mb-4 text-left">
|
<div className="text-gray-600 font-light text-lg mb-4 text-left">
|
||||||
<span className="w-1/2 dark:text-white text-gray-600">
|
<span className="w-1/2 dark:text-white text-gray-600">
|
||||||
<strong>{date.format("dddd")}</strong>
|
<strong>{t(date.format("dddd").toLowerCase())}</strong>
|
||||||
<span className="text-gray-500">{date.format(", DD MMMM")}</span>
|
<span className="text-gray-500">
|
||||||
|
{date.format(", DD ")}
|
||||||
|
{t(date.format("MMMM").toLowerCase())}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:max-h-[364px] overflow-y-auto">
|
<div className="md:max-h-[364px] overflow-y-auto">
|
||||||
|
@ -90,7 +97,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
})}
|
})}
|
||||||
{!loading && !error && !slots.length && (
|
{!loading && !error && !slots.length && (
|
||||||
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
|
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
|
||||||
<h1 className="my-6 text-xl text-black dark:text-white">All booked today.</h1>
|
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -103,7 +110,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<p className="text-sm text-yellow-700">Could not load the available time slots.</p>
|
<p className="text-sm text-yellow-700">{t("slots_load_fail")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import utc from "dayjs/plugin/utc";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import getSlots from "@lib/slots";
|
import getSlots from "@lib/slots";
|
||||||
|
|
||||||
dayjs.extend(dayjsBusinessDays);
|
dayjs.extend(dayjsBusinessDays);
|
||||||
|
@ -13,6 +14,7 @@ dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
const DatePicker = ({
|
const DatePicker = ({
|
||||||
|
localeProp,
|
||||||
weekStart,
|
weekStart,
|
||||||
onDatePicked,
|
onDatePicked,
|
||||||
workingHours,
|
workingHours,
|
||||||
|
@ -26,6 +28,7 @@ const DatePicker = ({
|
||||||
periodCountCalendarDays,
|
periodCountCalendarDays,
|
||||||
minimumBookingNotice,
|
minimumBookingNotice,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useLocale({ localeProp: localeProp });
|
||||||
const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]);
|
const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]);
|
||||||
|
|
||||||
const [selectedMonth, setSelectedMonth] = useState<number | null>(
|
const [selectedMonth, setSelectedMonth] = useState<number | null>(
|
||||||
|
@ -135,8 +138,10 @@ const DatePicker = ({
|
||||||
}>
|
}>
|
||||||
<div className="flex text-gray-600 font-light text-xl mb-4">
|
<div className="flex text-gray-600 font-light text-xl mb-4">
|
||||||
<span className="w-1/2 text-gray-600 dark:text-white">
|
<span className="w-1/2 text-gray-600 dark:text-white">
|
||||||
<strong className="text-gray-900 dark:text-white">{inviteeDate().format("MMMM")}</strong>
|
<strong className="text-gray-900 dark:text-white">
|
||||||
<span className="text-gray-500"> {inviteeDate().format("YYYY")}</span>
|
{t(inviteeDate().format("MMMM").toLowerCase())}
|
||||||
|
</strong>{" "}
|
||||||
|
<span className="text-gray-500">{inviteeDate().format("YYYY")}</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="w-1/2 text-right text-gray-600 dark:text-gray-400">
|
<div className="w-1/2 text-right text-gray-600 dark:text-gray-400">
|
||||||
<button
|
<button
|
||||||
|
@ -153,11 +158,11 @@ const DatePicker = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-7 gap-4 text-center border-b border-t dark:border-gray-800 sm:border-0">
|
<div className="grid grid-cols-7 gap-4 text-center border-b border-t dark:border-gray-800 sm:border-0">
|
||||||
{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
{["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||||
.sort((a, b) => (weekStart.startsWith(a) ? -1 : weekStart.startsWith(b) ? 1 : 0))
|
.sort((a, b) => (weekStart.startsWith(a) ? -1 : weekStart.startsWith(b) ? 1 : 0))
|
||||||
.map((weekDay) => (
|
.map((weekDay) => (
|
||||||
<div key={weekDay} className="uppercase text-gray-500 text-xs tracking-widest my-4">
|
<div key={weekDay} className="uppercase text-gray-500 text-xs tracking-widest my-4">
|
||||||
{weekDay}
|
{t(weekDay.toLowerCase()).substring(0, 3)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { FC, useEffect, useState } from "react";
|
||||||
import TimezoneSelect, { ITimezoneOption } from "react-timezone-select";
|
import TimezoneSelect, { ITimezoneOption } from "react-timezone-select";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import { is24h, timeZone } from "../../lib/clock";
|
import { is24h, timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
localeProp: string;
|
||||||
onSelectTimeZone: (selectedTimeZone: string) => void;
|
onSelectTimeZone: (selectedTimeZone: string) => void;
|
||||||
onToggle24hClock: (is24hClock: boolean) => void;
|
onToggle24hClock: (is24hClock: boolean) => void;
|
||||||
};
|
};
|
||||||
|
@ -15,6 +17,7 @@ type Props = {
|
||||||
const TimeOptions: FC<Props> = (props) => {
|
const TimeOptions: FC<Props> = (props) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const [is24hClock, setIs24hClock] = useState(false);
|
const [is24hClock, setIs24hClock] = useState(false);
|
||||||
|
const { t } = useLocale({ localeProp: props.localeProp });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIs24hClock(is24h());
|
setIs24hClock(is24h());
|
||||||
|
@ -35,11 +38,11 @@ const TimeOptions: FC<Props> = (props) => {
|
||||||
return selectedTimeZone !== "" ? (
|
return selectedTimeZone !== "" ? (
|
||||||
<div className="absolute z-10 w-full max-w-80 rounded-sm border border-gray-200 dark:bg-gray-700 dark:border-0 bg-white px-4 py-2">
|
<div className="absolute z-10 w-full max-w-80 rounded-sm border border-gray-200 dark:bg-gray-700 dark:border-0 bg-white px-4 py-2">
|
||||||
<div className="flex mb-4">
|
<div className="flex mb-4">
|
||||||
<div className="w-1/2 dark:text-white text-gray-600 font-medium">Time Options</div>
|
<div className="w-1/2 dark:text-white text-gray-600 font-medium">{t("time_options")}</div>
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<Switch.Group as="div" className="flex items-center justify-end">
|
<Switch.Group as="div" className="flex items-center justify-end">
|
||||||
<Switch.Label as="span" className="mr-3">
|
<Switch.Label as="span" className="mr-3">
|
||||||
<span className="text-sm dark:text-white text-gray-500">am/pm</span>
|
<span className="text-sm dark:text-white text-gray-500">{t("am_pm")}</span>
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
<Switch
|
<Switch
|
||||||
checked={is24hClock}
|
checked={is24hClock}
|
||||||
|
@ -48,7 +51,7 @@ const TimeOptions: FC<Props> = (props) => {
|
||||||
is24hClock ? "bg-black" : "dark:bg-gray-600 bg-gray-200",
|
is24hClock ? "bg-black" : "dark:bg-gray-600 bg-gray-200",
|
||||||
"relative inline-flex flex-shrink-0 h-5 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
|
"relative inline-flex flex-shrink-0 h-5 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
|
||||||
)}>
|
)}>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">{t("use_setting")}</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -58,7 +61,7 @@ const TimeOptions: FC<Props> = (props) => {
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch.Label as="span" className="ml-3">
|
<Switch.Label as="span" className="ml-3">
|
||||||
<span className="text-sm dark:text-white text-gray-500">24h</span>
|
<span className="text-sm dark:text-white text-gray-500">{t("24_h")}</span>
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
</Switch.Group>
|
</Switch.Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||||
|
@ -29,10 +30,11 @@ dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
|
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
|
||||||
|
|
||||||
const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
const AvailabilityPage = ({ profile, eventType, workingHours, localeProp }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
const { isReady } = useTheme(profile.theme);
|
const { isReady } = useTheme(profile.theme);
|
||||||
|
const { t, locale } = useLocale({ localeProp });
|
||||||
|
|
||||||
const selectedDate = useMemo(() => {
|
const selectedDate = useMemo(() => {
|
||||||
const dateString = asStringOrNull(router.query.date);
|
const dateString = asStringOrNull(router.query.date);
|
||||||
|
@ -88,8 +90,8 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={`${rescheduleUid ? "Reschedule" : ""} ${eventType.title} | ${profile.name}`}
|
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
||||||
description={`${rescheduleUid ? "Reschedule" : ""} ${eventType.title}`}
|
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
||||||
name={profile.name}
|
name={profile.name}
|
||||||
avatar={profile.image}
|
avatar={profile.image}
|
||||||
/>
|
/>
|
||||||
|
@ -122,7 +124,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
<div>
|
<div>
|
||||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{eventType.length} minutes
|
{eventType.length} {t("minutes")}
|
||||||
</div>
|
</div>
|
||||||
{eventType.price > 0 && (
|
{eventType.price > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -166,7 +168,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||||
</h1>
|
</h1>
|
||||||
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{eventType.length} minutes
|
{eventType.length} {t("minutes")}
|
||||||
</p>
|
</p>
|
||||||
{eventType.price > 0 && (
|
{eventType.price > 0 && (
|
||||||
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||||
|
@ -186,6 +188,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||||
<p className="mt-3 mb-8 text-gray-600 dark:text-gray-200">{eventType.description}</p>
|
<p className="mt-3 mb-8 text-gray-600 dark:text-gray-200">{eventType.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
localeProp={locale}
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
periodType={eventType?.periodType}
|
periodType={eventType?.periodType}
|
||||||
periodStartDate={eventType?.periodStartDate}
|
periodStartDate={eventType?.periodStartDate}
|
||||||
|
@ -205,6 +208,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||||
|
|
||||||
{selectedDate && (
|
{selectedDate && (
|
||||||
<AvailableTimes
|
<AvailableTimes
|
||||||
|
localeProp={locale}
|
||||||
workingHours={workingHours}
|
workingHours={workingHours}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
minimumBookingNotice={eventType.minimumBookingNotice}
|
minimumBookingNotice={eventType.minimumBookingNotice}
|
||||||
|
@ -237,7 +241,11 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||||
)}
|
)}
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} onToggle24hClock={handleToggle24hClock} />
|
<TimeOptions
|
||||||
|
localeProp={locale}
|
||||||
|
onSelectTimeZone={handleSelectTimeZone}
|
||||||
|
onToggle24hClock={handleToggle24hClock}
|
||||||
|
/>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { createPaymentLink } from "@ee/lib/stripe/client";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import { LocationType } from "@lib/location";
|
import { LocationType } from "@lib/location";
|
||||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||||
|
@ -36,6 +37,7 @@ import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||||
type BookingPageProps = BookPageProps | TeamBookingPageProps;
|
type BookingPageProps = BookPageProps | TeamBookingPageProps;
|
||||||
|
|
||||||
const BookingPage = (props: BookingPageProps) => {
|
const BookingPage = (props: BookingPageProps) => {
|
||||||
|
const { t } = useLocale({ localeProp: props.localeProp });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
const { isReady } = useTheme(props.profile.theme);
|
const { isReady } = useTheme(props.profile.theme);
|
||||||
|
@ -67,8 +69,8 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
|
|
||||||
// TODO: Move to translations
|
// TODO: Move to translations
|
||||||
const locationLabels = {
|
const locationLabels = {
|
||||||
[LocationType.InPerson]: "Link or In-person meeting",
|
[LocationType.InPerson]: t("in_person_meeting"),
|
||||||
[LocationType.Phone]: "Phone call",
|
[LocationType.Phone]: t("phone_call"),
|
||||||
[LocationType.GoogleMeet]: "Google Meet",
|
[LocationType.GoogleMeet]: "Google Meet",
|
||||||
[LocationType.Zoom]: "Zoom Video",
|
[LocationType.Zoom]: "Zoom Video",
|
||||||
[LocationType.Daily]: "Daily.co Video",
|
[LocationType.Daily]: "Daily.co Video",
|
||||||
|
@ -85,7 +87,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
const data = event.target["custom_" + input.id];
|
const data = event.target["custom_" + input.id];
|
||||||
if (data) {
|
if (data) {
|
||||||
if (input.type === EventTypeCustomInputType.BOOL) {
|
if (input.type === EventTypeCustomInputType.BOOL) {
|
||||||
return input.label + "\n" + (data.checked ? "Yes" : "No");
|
return input.label + "\n" + (data.checked ? t("yes") : t("no"));
|
||||||
} else {
|
} else {
|
||||||
return input.label + "\n" + data.value;
|
return input.label + "\n" + data.value;
|
||||||
}
|
}
|
||||||
|
@ -94,7 +96,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
}
|
}
|
||||||
if (!!notes && !!event.target.notes.value) {
|
if (!!notes && !!event.target.notes.value) {
|
||||||
notes += "\n\nAdditional notes:\n" + event.target.notes.value;
|
notes += `\n\n${t("additional_notes")}:\n` + event.target.notes.value;
|
||||||
} else {
|
} else {
|
||||||
notes += event.target.notes.value;
|
notes += event.target.notes.value;
|
||||||
}
|
}
|
||||||
|
@ -185,8 +187,16 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
{rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with {props.profile.name} |
|
{rescheduleUid
|
||||||
Cal.com
|
? t("booking_reschedule_confirmation", {
|
||||||
|
eventTypeTitle: props.eventType.title,
|
||||||
|
profileName: props.profile.name,
|
||||||
|
})
|
||||||
|
: t("booking_confirmation", {
|
||||||
|
eventTypeTitle: props.eventType.title,
|
||||||
|
profileName: props.profile.name,
|
||||||
|
})}{" "}
|
||||||
|
| Cal.com
|
||||||
</title>
|
</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
@ -215,7 +225,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mb-2 text-gray-500">
|
<p className="mb-2 text-gray-500">
|
||||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||||
{props.eventType.length} minutes
|
{props.eventType.length} {t("minutes")}
|
||||||
</p>
|
</p>
|
||||||
{props.eventType.price > 0 && (
|
{props.eventType.price > 0 && (
|
||||||
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||||
|
@ -244,8 +254,8 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
||||||
<form onSubmit={bookingHandler}>
|
<form onSubmit={bookingHandler}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
<label htmlFor="name" className="block text-sm font-medium dark:text-white text-gray-700">
|
||||||
Your name
|
{t("your_name")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
|
@ -262,8 +272,8 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="block text-sm font-medium text-gray-700 dark:text-white">
|
className="block text-sm font-medium dark:text-white text-gray-700">
|
||||||
Email address
|
{t("email_address")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
|
@ -281,7 +291,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
{locations.length > 1 && (
|
{locations.length > 1 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
Location
|
{t("location")}
|
||||||
</span>
|
</span>
|
||||||
{locations.map((location) => (
|
{locations.map((location) => (
|
||||||
<label key={location.type} className="block">
|
<label key={location.type} className="block">
|
||||||
|
@ -306,12 +316,12 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
<label
|
<label
|
||||||
htmlFor="phone"
|
htmlFor="phone"
|
||||||
className="block text-sm font-medium text-gray-700 dark:text-white">
|
className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
Phone Number
|
{t("phone_number")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
name="phone"
|
name="phone"
|
||||||
placeholder="Enter phone number"
|
placeholder={t("enter_phone_number")}
|
||||||
id="phone"
|
id="phone"
|
||||||
required
|
required
|
||||||
className="block w-full border-gray-300 rounded-md shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black sm:text-sm"
|
className="block w-full border-gray-300 rounded-md shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black sm:text-sm"
|
||||||
|
@ -390,7 +400,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
onClick={toggleGuestEmailInput}
|
onClick={toggleGuestEmailInput}
|
||||||
htmlFor="guests"
|
htmlFor="guests"
|
||||||
className="block mb-1 text-sm font-medium text-blue-500 dark:text-white hover:cursor-pointer">
|
className="block mb-1 text-sm font-medium text-blue-500 dark:text-white hover:cursor-pointer">
|
||||||
+ Additional Guests
|
{t("additional_guests")}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{guestToggle && (
|
{guestToggle && (
|
||||||
|
@ -430,24 +440,24 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
<label
|
<label
|
||||||
htmlFor="notes"
|
htmlFor="notes"
|
||||||
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
|
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
|
||||||
Additional notes
|
{t("additional_notes")}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="notes"
|
name="notes"
|
||||||
id="notes"
|
id="notes"
|
||||||
rows={3}
|
rows={3}
|
||||||
className="block w-full border-gray-300 rounded-md shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black sm:text-sm"
|
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
placeholder="Please share anything that will help prepare for our meeting."
|
placeholder={t("share_additional_notes")}
|
||||||
defaultValue={props.booking ? props.booking.description : ""}
|
defaultValue={props.booking ? props.booking.description : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start space-x-2">
|
<div className="flex items-start space-x-2">
|
||||||
{/* TODO: add styling props to <Button variant="" color="" /> and get rid of btn-primary */}
|
{/* TODO: add styling props to <Button variant="" color="" /> and get rid of btn-primary */}
|
||||||
<Button type="submit" loading={loading}>
|
<Button type="submit" loading={loading}>
|
||||||
{rescheduleUid ? "Reschedule" : "Confirm"}
|
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" type="button" onClick={() => router.back()}>
|
<Button color="secondary" type="button" onClick={() => router.back()}>
|
||||||
Cancel
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -459,7 +469,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<p className="text-sm text-yellow-700">
|
<p className="text-sm text-yellow-700">
|
||||||
Could not {rescheduleUid ? "reschedule" : "book"} the meeting.
|
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,10 +3,13 @@ import { CheckIcon } from "@heroicons/react/solid";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import React, { PropsWithChildren } from "react";
|
import React, { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import { DialogClose, DialogContent } from "@components/Dialog";
|
import { DialogClose, DialogContent } from "@components/Dialog";
|
||||||
import { Button } from "@components/ui/Button";
|
import { Button } from "@components/ui/Button";
|
||||||
|
|
||||||
export type ConfirmationDialogContentProps = {
|
export type ConfirmationDialogContentProps = {
|
||||||
|
localeProp: string;
|
||||||
confirmBtnText?: string;
|
confirmBtnText?: string;
|
||||||
cancelBtnText?: string;
|
cancelBtnText?: string;
|
||||||
onConfirm?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
onConfirm?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||||
|
@ -15,7 +18,15 @@ export type ConfirmationDialogContentProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ConfirmationDialogContent(props: PropsWithChildren<ConfirmationDialogContentProps>) {
|
export default function ConfirmationDialogContent(props: PropsWithChildren<ConfirmationDialogContentProps>) {
|
||||||
const { title, variety, confirmBtnText = "Confirm", cancelBtnText = "Cancel", onConfirm, children } = props;
|
const { t } = useLocale({ localeProp: props.localeProp });
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
variety,
|
||||||
|
confirmBtnText = t("confirm"),
|
||||||
|
cancelBtnText = t("cancel"),
|
||||||
|
onConfirm,
|
||||||
|
children,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React from "react";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
|
const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
|
||||||
select: {
|
select: {
|
||||||
|
@ -20,11 +21,14 @@ const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
|
||||||
type EventType = Prisma.EventTypeGetPayload<typeof eventTypeData>;
|
type EventType = Prisma.EventTypeGetPayload<typeof eventTypeData>;
|
||||||
|
|
||||||
export type EventTypeDescriptionProps = {
|
export type EventTypeDescriptionProps = {
|
||||||
|
localeProp: string;
|
||||||
eventType: EventType;
|
eventType: EventType;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventTypeDescription = ({ eventType, className }: EventTypeDescriptionProps) => {
|
export const EventTypeDescription = ({ localeProp, eventType, className }: EventTypeDescriptionProps) => {
|
||||||
|
const { t } = useLocale({ localeProp });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classNames("text-neutral-500 dark:text-white", className)}>
|
<div className={classNames("text-neutral-500 dark:text-white", className)}>
|
||||||
|
@ -41,13 +45,13 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
||||||
{eventType.schedulingType ? (
|
{eventType.schedulingType ? (
|
||||||
<li className="flex whitespace-nowrap">
|
<li className="flex whitespace-nowrap">
|
||||||
<UsersIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
|
<UsersIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && "Round Robin"}
|
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
|
||||||
{eventType.schedulingType === SchedulingType.COLLECTIVE && "Collective"}
|
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<li className="flex whitespace-nowrap">
|
<li className="flex whitespace-nowrap">
|
||||||
<UserIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
|
<UserIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||||
1-on-1
|
{t("1_on_1")}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{eventType.price > 0 && (
|
{eventType.price > 0 && (
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
import React, { SyntheticEvent, useState } from "react";
|
import React, { SyntheticEvent, useState } from "react";
|
||||||
|
|
||||||
import { ErrorCode } from "@lib/auth";
|
import { ErrorCode } from "@lib/auth";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import Modal from "@components/Modal";
|
import Modal from "@components/Modal";
|
||||||
|
|
||||||
const errorMessages: { [key: string]: string } = {
|
const ChangePasswordSection = ({ localeProp }: { localeProp: string }) => {
|
||||||
[ErrorCode.IncorrectPassword]: "Current password is incorrect",
|
|
||||||
[ErrorCode.NewPasswordMatchesOld]:
|
|
||||||
"New password matches your old password. Please choose a different password.",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChangePasswordSection = () => {
|
|
||||||
const [oldPassword, setOldPassword] = useState("");
|
const [oldPassword, setOldPassword] = useState("");
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const { t } = useLocale({ localeProp });
|
||||||
|
|
||||||
|
const errorMessages: { [key: string]: string } = {
|
||||||
|
[ErrorCode.IncorrectPassword]: t("current_incorrect_password"),
|
||||||
|
[ErrorCode.NewPasswordMatchesOld]: t("new_password_matches_old_password"),
|
||||||
|
};
|
||||||
|
|
||||||
const closeSuccessModal = () => {
|
const closeSuccessModal = () => {
|
||||||
setSuccessModalOpen(false);
|
setSuccessModalOpen(false);
|
||||||
|
@ -48,10 +49,10 @@ const ChangePasswordSection = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
setErrorMessage(errorMessages[body.error] || "Something went wrong. Please try again");
|
setErrorMessage(errorMessages[body.error] || `${t("something_went_wrong")}${t("please_try_again")}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error changing password", err);
|
console.error(t("error_changing_password"), err);
|
||||||
setErrorMessage("Something went wrong. Please try again");
|
setErrorMessage(`${t("something_went_wrong")}${t("please_try_again")}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
@ -60,14 +61,14 @@ const ChangePasswordSection = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">Change Password</h2>
|
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">{t("change_password")}</h2>
|
||||||
</div>
|
</div>
|
||||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
|
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
|
||||||
<div className="py-6 lg:pb-8">
|
<div className="py-6 lg:pb-8">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="w-1/2 mr-2">
|
<div className="w-1/2 mr-2">
|
||||||
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
|
||||||
Current Password
|
{t("current_password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
|
@ -78,13 +79,13 @@ const ChangePasswordSection = () => {
|
||||||
id="current_password"
|
id="current_password"
|
||||||
required
|
required
|
||||||
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
|
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||||
placeholder="Your old password"
|
placeholder={t("your_old_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2 ml-2">
|
<div className="w-1/2 ml-2">
|
||||||
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
|
||||||
New Password
|
{t("new_password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
|
@ -95,7 +96,7 @@ const ChangePasswordSection = () => {
|
||||||
required
|
required
|
||||||
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
||||||
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
|
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||||
placeholder="Your super secure new password"
|
placeholder={t("super_secure_new_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,15 +106,15 @@ const ChangePasswordSection = () => {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ml-2 bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
|
className="ml-2 bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
|
||||||
Save
|
{t("save")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-4" />
|
<hr className="mt-4" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<Modal
|
<Modal
|
||||||
heading="Password updated successfully"
|
heading={t("password_updated_successfully")}
|
||||||
description="Your password has been successfully changed."
|
description={t("password_has_been_changed")}
|
||||||
open={successModalOpen}
|
open={successModalOpen}
|
||||||
handleClose={closeSuccessModal}
|
handleClose={closeSuccessModal}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { SyntheticEvent, useState } from "react";
|
import { SyntheticEvent, useState } from "react";
|
||||||
|
|
||||||
import { ErrorCode } from "@lib/auth";
|
import { ErrorCode } from "@lib/auth";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import { Dialog, DialogContent } from "@components/Dialog";
|
import { Dialog, DialogContent } from "@components/Dialog";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
|
@ -18,12 +19,14 @@ interface DisableTwoFactorAuthModalProps {
|
||||||
* Called when the user disables two-factor auth
|
* Called when the user disables two-factor auth
|
||||||
*/
|
*/
|
||||||
onDisable: () => void;
|
onDisable: () => void;
|
||||||
|
localeProp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuthModalProps) => {
|
const DisableTwoFactorAuthModal = ({ onDisable, onCancel, localeProp }: DisableTwoFactorAuthModalProps) => {
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isDisabling, setIsDisabling] = useState(false);
|
const [isDisabling, setIsDisabling] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const { t } = useLocale({ localeProp });
|
||||||
|
|
||||||
async function handleDisable(e: SyntheticEvent) {
|
async function handleDisable(e: SyntheticEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -43,13 +46,13 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
||||||
|
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
if (body.error === ErrorCode.IncorrectPassword) {
|
if (body.error === ErrorCode.IncorrectPassword) {
|
||||||
setErrorMessage("Password is incorrect.");
|
setErrorMessage(t("incorrect_password"));
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage("Something went wrong.");
|
setErrorMessage(t("something_went_wrong"));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErrorMessage("Something went wrong.");
|
setErrorMessage(t("something_went_wrong"));
|
||||||
console.error("Error disabling two-factor authentication", e);
|
console.error(t("error_disabling_2fa"), e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsDisabling(false);
|
setIsDisabling(false);
|
||||||
}
|
}
|
||||||
|
@ -58,15 +61,12 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
||||||
return (
|
return (
|
||||||
<Dialog open={true}>
|
<Dialog open={true}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TwoFactorModalHeader
|
<TwoFactorModalHeader title={t("disable_2fa")} description={t("disable_2fa_recommendation")} />
|
||||||
title="Disable two-factor authentication"
|
|
||||||
description="If you need to disable 2FA, we recommend re-enabling it as soon as possible."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form onSubmit={handleDisable}>
|
<form onSubmit={handleDisable}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
||||||
Password
|
{t("password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
|
@ -90,10 +90,10 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onClick={handleDisable}
|
onClick={handleDisable}
|
||||||
disabled={password.length === 0 || isDisabling}>
|
disabled={password.length === 0 || isDisabling}>
|
||||||
Disable
|
{t("disable")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
Cancel
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { SyntheticEvent, useState } from "react";
|
import React, { SyntheticEvent, useState } from "react";
|
||||||
|
|
||||||
import { ErrorCode } from "@lib/auth";
|
import { ErrorCode } from "@lib/auth";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import { Dialog, DialogContent } from "@components/Dialog";
|
import { Dialog, DialogContent } from "@components/Dialog";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
|
@ -18,6 +19,7 @@ interface EnableTwoFactorModalProps {
|
||||||
* Called when the user enables two-factor auth
|
* Called when the user enables two-factor auth
|
||||||
*/
|
*/
|
||||||
onEnable: () => void;
|
onEnable: () => void;
|
||||||
|
localeProp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SetupStep {
|
enum SetupStep {
|
||||||
|
@ -45,7 +47,7 @@ const WithStep = ({
|
||||||
return step === current ? children : null;
|
return step === current ? children : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps) => {
|
const EnableTwoFactorModal = ({ onEnable, onCancel, localeProp }: EnableTwoFactorModalProps) => {
|
||||||
const [step, setStep] = useState(SetupStep.ConfirmPassword);
|
const [step, setStep] = useState(SetupStep.ConfirmPassword);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [totpCode, setTotpCode] = useState("");
|
const [totpCode, setTotpCode] = useState("");
|
||||||
|
@ -53,6 +55,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
const [secret, setSecret] = useState("");
|
const [secret, setSecret] = useState("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const { t } = useLocale({ localeProp });
|
||||||
|
|
||||||
async function handleSetup(e: SyntheticEvent) {
|
async function handleSetup(e: SyntheticEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -76,13 +79,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.error === ErrorCode.IncorrectPassword) {
|
if (body.error === ErrorCode.IncorrectPassword) {
|
||||||
setErrorMessage("Password is incorrect.");
|
setErrorMessage(t("incorrect_password"));
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage("Something went wrong.");
|
setErrorMessage(t("something_went_wrong"));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErrorMessage("Something went wrong.");
|
setErrorMessage(t("something_went_wrong"));
|
||||||
console.error("Error setting up two-factor authentication", e);
|
console.error(t("error_enabling_2fa"), e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
@ -108,13 +111,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
|
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
|
||||||
setErrorMessage("Code is incorrect. Please try again.");
|
setErrorMessage(`${t("code_is_incorrect")} ${t("please_try_again")}`);
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage("Something went wrong.");
|
setErrorMessage(t("something_went_wrong"));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErrorMessage("Something went wrong.");
|
setErrorMessage(t("something_went_wrong"));
|
||||||
console.error("Error enabling up two-factor authentication", e);
|
console.error(t("error_enabling_2fa"), e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
@ -123,16 +126,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
return (
|
return (
|
||||||
<Dialog open={true}>
|
<Dialog open={true}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TwoFactorModalHeader
|
<TwoFactorModalHeader title={t("enable_2fa")} description={setupDescriptions[step]} />
|
||||||
title="Enable two-factor authentication"
|
|
||||||
description={setupDescriptions[step]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<form onSubmit={handleSetup}>
|
<form onSubmit={handleSetup}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
||||||
Password
|
{t("password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
|
@ -162,7 +162,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
<form onSubmit={handleEnable}>
|
<form onSubmit={handleEnable}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="code" className="mt-4 block text-sm font-medium text-gray-700">
|
<label htmlFor="code" className="mt-4 block text-sm font-medium text-gray-700">
|
||||||
Code
|
{t("code")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
|
@ -191,12 +191,12 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onClick={handleSetup}
|
onClick={handleSetup}
|
||||||
disabled={password.length === 0 || isSubmitting}>
|
disabled={password.length === 0 || isSubmitting}>
|
||||||
Continue
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||||
<Button type="submit" className="ml-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
<Button type="submit" className="ml-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||||
Continue
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||||
|
@ -205,11 +205,11 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onClick={handleEnable}
|
onClick={handleEnable}
|
||||||
disabled={totpCode.length !== 6 || isSubmitting}>
|
disabled={totpCode.length !== 6 || isSubmitting}>
|
||||||
Enable
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
Cancel
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
@ -1,37 +1,45 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import Badge from "@components/ui/Badge";
|
import Badge from "@components/ui/Badge";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
|
|
||||||
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
||||||
|
|
||||||
const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean }) => {
|
const TwoFactorAuthSection = ({
|
||||||
|
twoFactorEnabled,
|
||||||
|
localeProp,
|
||||||
|
}: {
|
||||||
|
twoFactorEnabled: boolean;
|
||||||
|
localeProp: string;
|
||||||
|
}) => {
|
||||||
const [enabled, setEnabled] = useState(twoFactorEnabled);
|
const [enabled, setEnabled] = useState(twoFactorEnabled);
|
||||||
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
||||||
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||||
|
const { t, locale } = useLocale({ localeProp });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">Two-Factor Authentication</h2>
|
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">{t("2fa")}</h2>
|
||||||
<Badge className="text-xs ml-2" variant={enabled ? "success" : "gray"}>
|
<Badge className="text-xs ml-2" variant={enabled ? "success" : "gray"}>
|
||||||
{enabled ? "Enabled" : "Disabled"}
|
{enabled ? "Enabled" : "Disabled"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
|
||||||
Add an extra layer of security to your account in case your password is stolen.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
|
onClick={() => (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
|
||||||
{enabled ? "Disable" : "Enable"} Two-Factor Authentication
|
{enabled ? "Disable" : "Enable"} {t("2fa")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{enableModalOpen && (
|
{enableModalOpen && (
|
||||||
<EnableTwoFactorModal
|
<EnableTwoFactorModal
|
||||||
|
localeProp={locale}
|
||||||
onEnable={() => {
|
onEnable={() => {
|
||||||
setEnabled(true);
|
setEnabled(true);
|
||||||
setEnableModalOpen(false);
|
setEnableModalOpen(false);
|
||||||
|
@ -42,6 +50,7 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
|
||||||
|
|
||||||
{disableModalOpen && (
|
{disableModalOpen && (
|
||||||
<DisableTwoFactorModal
|
<DisableTwoFactorModal
|
||||||
|
localeProp={locale}
|
||||||
onDisable={() => {
|
onDisable={() => {
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
setDisableModalOpen(false);
|
setDisableModalOpen(false);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
|
import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { Member } from "@lib/member";
|
import { Member } from "@lib/member";
|
||||||
import { Team } from "@lib/team";
|
import { Team } from "@lib/team";
|
||||||
|
|
||||||
|
@ -16,7 +17,11 @@ import ErrorAlert from "@components/ui/alerts/Error";
|
||||||
|
|
||||||
import MemberList from "./MemberList";
|
import MemberList from "./MemberList";
|
||||||
|
|
||||||
export default function EditTeam(props: { team: Team | undefined | null; onCloseEdit: () => void }) {
|
export default function EditTeam(props: {
|
||||||
|
localeProp: string;
|
||||||
|
team: Team | undefined | null;
|
||||||
|
onCloseEdit: () => void;
|
||||||
|
}) {
|
||||||
const [members, setMembers] = useState([]);
|
const [members, setMembers] = useState([]);
|
||||||
|
|
||||||
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
||||||
|
@ -30,6 +35,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
const [inviteModalTeam, setInviteModalTeam] = useState<Team | null | undefined>();
|
const [inviteModalTeam, setInviteModalTeam] = useState<Team | null | undefined>();
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [imageSrc, setImageSrc] = useState<string>("");
|
const [imageSrc, setImageSrc] = useState<string>("");
|
||||||
|
const { t, locale } = useLocale({ localeProp: props.localeProp });
|
||||||
|
|
||||||
const loadMembers = () =>
|
const loadMembers = () =>
|
||||||
fetch("/api/teams/" + props.team?.id + "/membership")
|
fetch("/api/teams/" + props.team?.id + "/membership")
|
||||||
|
@ -132,19 +138,19 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
size="sm"
|
size="sm"
|
||||||
StartIcon={ArrowLeftIcon}
|
StartIcon={ArrowLeftIcon}
|
||||||
onClick={() => props.onCloseEdit()}>
|
onClick={() => props.onCloseEdit()}>
|
||||||
Back
|
{t("back")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="pb-5 pr-4 sm:pb-6">
|
<div className="pb-5 pr-4 sm:pb-6">
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900">{props.team?.name}</h3>
|
<h3 className="text-lg font-bold leading-6 text-gray-900">{props.team?.name}</h3>
|
||||||
<div className="max-w-xl mt-2 text-sm text-gray-500">
|
<div className="max-w-xl mt-2 text-sm text-gray-500">
|
||||||
<p>Manage your team</p>
|
<p>{t("manage_your_team")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-2" />
|
<hr className="mt-2" />
|
||||||
<h3 className="font-cal font-bold leading-6 text-gray-900 mt-7 text-md">Profile</h3>
|
<h3 className="font-cal font-bold leading-6 text-gray-900 mt-7 text-md">{t("profile")}</h3>
|
||||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateTeamHandler}>
|
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateTeamHandler}>
|
||||||
{hasErrors && <ErrorAlert message={errorMessage} />}
|
{hasErrors && <ErrorAlert message={errorMessage} />}
|
||||||
<div className="py-6 lg:pb-8">
|
<div className="py-6 lg:pb-8">
|
||||||
|
@ -152,18 +158,22 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
<div className="flex-grow space-y-6">
|
<div className="flex-grow space-y-6">
|
||||||
<div className="block sm:flex">
|
<div className="block sm:flex">
|
||||||
<div className="w-full mb-6 sm:w-1/2 sm:mr-2">
|
<div className="w-full mb-6 sm:w-1/2 sm:mr-2">
|
||||||
<UsernameInput ref={teamUrlRef} defaultValue={props.team?.slug} label={"My team URL"} />
|
<UsernameInput
|
||||||
|
ref={teamUrlRef}
|
||||||
|
defaultValue={props.team?.slug}
|
||||||
|
label={t("my_team_url")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-1/2 sm:ml-2">
|
<div className="w-full sm:w-1/2 sm:ml-2">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
Team name
|
{t("team_name")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
ref={nameRef}
|
ref={nameRef}
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="Your team name"
|
placeholder={t("your_team_name")}
|
||||||
required
|
required
|
||||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||||
defaultValue={props.team?.name}
|
defaultValue={props.team?.name}
|
||||||
|
@ -172,7 +182,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
|
||||||
About
|
{t("about")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<textarea
|
<textarea
|
||||||
|
@ -182,9 +192,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
rows={3}
|
rows={3}
|
||||||
defaultValue={props.team?.bio}
|
defaultValue={props.team?.bio}
|
||||||
className="block w-full mt-1 border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"></textarea>
|
className="block w-full mt-1 border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"></textarea>
|
||||||
<p className="mt-2 text-sm text-gray-500">
|
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
|
||||||
A few sentences about your team. This will appear on your team's URL page.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -206,7 +214,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
target="logo"
|
target="logo"
|
||||||
id="logo-upload"
|
id="logo-upload"
|
||||||
buttonMsg={imageSrc !== "" ? "Edit logo" : "Upload a logo"}
|
buttonMsg={imageSrc !== "" ? t("edit_logo") : t("upload_a_logo")}
|
||||||
handleAvatarChange={handleLogoChange}
|
handleAvatarChange={handleLogoChange}
|
||||||
imageSrc={imageSrc ?? props.team?.logo}
|
imageSrc={imageSrc ?? props.team?.logo}
|
||||||
/>
|
/>
|
||||||
|
@ -214,20 +222,25 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
<hr className="mt-6" />
|
<hr className="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-7">
|
<div className="flex justify-between mt-7">
|
||||||
<h3 className="font-cal font-bold leading-6 text-gray-900 text-md">Members</h3>
|
<h3 className="font-cal font-bold leading-6 text-gray-900 text-md">{t("members")}</h3>
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={PlusIcon}
|
StartIcon={PlusIcon}
|
||||||
onClick={() => onInviteMember(props.team)}>
|
onClick={() => onInviteMember(props.team)}>
|
||||||
New Member
|
{t("new_member")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{!!members.length && (
|
{!!members.length && (
|
||||||
<MemberList members={members} onRemoveMember={onRemoveMember} onChange={loadMembers} />
|
<MemberList
|
||||||
|
localeProp={locale}
|
||||||
|
members={members}
|
||||||
|
onRemoveMember={onRemoveMember}
|
||||||
|
onChange={loadMembers}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<hr className="mt-6" />
|
<hr className="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -245,14 +258,14 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 text-sm">
|
<div className="ml-3 text-sm">
|
||||||
<label htmlFor="hide-branding" className="font-medium text-gray-700">
|
<label htmlFor="hide-branding" className="font-medium text-gray-700">
|
||||||
Disable Cal.com branding
|
{t("disable_cal_branding")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-gray-500">Hide all Cal.com branding from your public pages.</p>
|
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-6" />
|
<hr className="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold leading-6 text-gray-900 mt-7 text-md">Danger Zone</h3>
|
<h3 className="font-bold leading-6 text-gray-900 mt-7 text-md">{t("danger_zone")}</h3>
|
||||||
<div>
|
<div>
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
<Dialog>
|
<Dialog>
|
||||||
|
@ -262,16 +275,15 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
}}
|
}}
|
||||||
className="btn-sm btn-white">
|
className="btn-sm btn-white">
|
||||||
<TrashIcon className="group-hover:text-red text-gray-700 w-3.5 h-3.5 mr-2 inline-block" />
|
<TrashIcon className="group-hover:text-red text-gray-700 w-3.5 h-3.5 mr-2 inline-block" />
|
||||||
Disband Team
|
{t("disband_team")}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
|
localeProp={locale}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title="Disband Team"
|
title={t("disband_team")}
|
||||||
confirmBtnText="Yes, disband team"
|
confirmBtnText={t("confirm_disband_team")}
|
||||||
cancelBtnText="Cancel"
|
|
||||||
onConfirm={() => deleteTeam()}>
|
onConfirm={() => deleteTeam()}>
|
||||||
Are you sure you want to disband this team? Anyone who you've shared this team
|
{t("disband_team_confirmation_message")}
|
||||||
link with will no longer be able to book using it.
|
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
@ -281,19 +293,23 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
||||||
<hr className="mt-8" />
|
<hr className="mt-8" />
|
||||||
<div className="flex justify-end py-4">
|
<div className="flex justify-end py-4">
|
||||||
<Button type="submit" color="primary">
|
<Button type="submit" color="primary">
|
||||||
Save
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<Modal
|
<Modal
|
||||||
heading="Team updated successfully"
|
heading={t("team_updated_successfully")}
|
||||||
description="Your team has been updated successfully."
|
description={t("your_team_updated_successfully")}
|
||||||
open={successModalOpen}
|
open={successModalOpen}
|
||||||
handleClose={closeSuccessModal}
|
handleClose={closeSuccessModal}
|
||||||
/>
|
/>
|
||||||
{showMemberInvitationModal && (
|
{showMemberInvitationModal && (
|
||||||
<MemberInvitationModal team={inviteModalTeam} onExit={onMemberInvitationModalExit} />
|
<MemberInvitationModal
|
||||||
|
localeProp={locale}
|
||||||
|
team={inviteModalTeam}
|
||||||
|
onExit={onMemberInvitationModalExit}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import { UsersIcon } from "@heroicons/react/outline";
|
import { UsersIcon } from "@heroicons/react/outline";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { Team } from "@lib/team";
|
import { Team } from "@lib/team";
|
||||||
|
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
|
|
||||||
export default function MemberInvitationModal(props: { team: Team | undefined | null; onExit: () => void }) {
|
export default function MemberInvitationModal(props: {
|
||||||
|
localeProp: string;
|
||||||
|
team: Team | undefined | null;
|
||||||
|
onExit: () => void;
|
||||||
|
}) {
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const { t } = useLocale({ localeProp: props.localeProp });
|
||||||
|
|
||||||
const handleError = async (res: Response) => {
|
const handleError = async (res: Response) => {
|
||||||
const responseData = await res.json();
|
const responseData = await res.json();
|
||||||
|
@ -64,10 +70,10 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
Invite a new member
|
{t("invite_new_member")}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Invite someone to your team.</p>
|
<p className="text-sm text-gray-400">{t("invite_new_team_member")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,7 +81,7 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="inviteUser" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="inviteUser" className="block text-sm font-medium text-gray-700">
|
||||||
Email or Username
|
{t("email_or_username")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -88,13 +94,13 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block mb-2 text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
|
<label className="block mb-2 text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
|
||||||
Role
|
{t("role")}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="role"
|
id="role"
|
||||||
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-black sm:text-sm">
|
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-black sm:text-sm">
|
||||||
<option value="MEMBER">Member</option>
|
<option value="MEMBER">{t("member")}</option>
|
||||||
<option value="OWNER">Owner</option>
|
<option value="OWNER">{t("owner")}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
|
@ -109,7 +115,7 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-2 text-sm">
|
<div className="ml-2 text-sm">
|
||||||
<label htmlFor="sendInviteEmail" className="font-medium text-gray-700">
|
<label htmlFor="sendInviteEmail" className="font-medium text-gray-700">
|
||||||
Send an invite email
|
{t("send_invite_email")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -122,10 +128,10 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
||||||
)}
|
)}
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" color="primary" className="ml-2">
|
<Button type="submit" color="primary" className="ml-2">
|
||||||
Invite
|
{t("invite")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" color="secondary" onClick={props.onExit}>
|
<Button type="button" color="secondary" onClick={props.onExit}>
|
||||||
Cancel
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { Member } from "@lib/member";
|
import { Member } from "@lib/member";
|
||||||
|
|
||||||
import MemberListItem from "./MemberListItem";
|
import MemberListItem from "./MemberListItem";
|
||||||
|
|
||||||
export default function MemberList(props: {
|
export default function MemberList(props: {
|
||||||
|
localeProp: string;
|
||||||
members: Member[];
|
members: Member[];
|
||||||
onRemoveMember: (text: Member) => void;
|
onRemoveMember: (text: Member) => void;
|
||||||
onChange: (text: string) => void;
|
onChange: (text: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||||
|
|
||||||
const selectAction = (action: string, member: Member) => {
|
const selectAction = (action: string, member: Member) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "remove":
|
case "remove":
|
||||||
|
@ -20,6 +24,7 @@ export default function MemberList(props: {
|
||||||
<ul className="px-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
<ul className="px-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
||||||
{props.members.map((member) => (
|
{props.members.map((member) => (
|
||||||
<MemberListItem
|
<MemberListItem
|
||||||
|
localeProp={locale}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
key={member.id}
|
key={member.id}
|
||||||
member={member}
|
member={member}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { DotsHorizontalIcon, UserRemoveIcon } from "@heroicons/react/outline";
|
import { DotsHorizontalIcon, UserRemoveIcon } from "@heroicons/react/outline";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { Member } from "@lib/member";
|
import { Member } from "@lib/member";
|
||||||
|
|
||||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
import { Dialog, DialogTrigger } from "@components/Dialog";
|
||||||
|
@ -11,11 +12,13 @@ import Button from "@components/ui/Button";
|
||||||
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/Dropdown";
|
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/Dropdown";
|
||||||
|
|
||||||
export default function MemberListItem(props: {
|
export default function MemberListItem(props: {
|
||||||
|
localeProp: string;
|
||||||
member: Member;
|
member: Member;
|
||||||
onActionSelect: (text: string) => void;
|
onActionSelect: (text: string) => void;
|
||||||
onChange: (text: string) => void;
|
onChange: (text: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [member] = useState(props.member);
|
const [member] = useState(props.member);
|
||||||
|
const { t, locale } = useLocale({ localeProp: props.localeProp });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
member && (
|
member && (
|
||||||
|
@ -41,21 +44,21 @@ export default function MemberListItem(props: {
|
||||||
{props.member.role === "INVITEE" && (
|
{props.member.role === "INVITEE" && (
|
||||||
<>
|
<>
|
||||||
<span className="self-center h-6 px-3 py-1 mr-2 text-xs text-yellow-700 capitalize rounded-md bg-yellow-50">
|
<span className="self-center h-6 px-3 py-1 mr-2 text-xs text-yellow-700 capitalize rounded-md bg-yellow-50">
|
||||||
Pending
|
{t("pending")}
|
||||||
</span>
|
</span>
|
||||||
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
|
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
|
||||||
Member
|
{t("member")}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{props.member.role === "MEMBER" && (
|
{props.member.role === "MEMBER" && (
|
||||||
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
|
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
|
||||||
Member
|
{t("member")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{props.member.role === "OWNER" && (
|
{props.member.role === "OWNER" && (
|
||||||
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-blue-700 capitalize rounded-md bg-blue-50">
|
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-blue-700 capitalize rounded-md bg-blue-50">
|
||||||
Owner
|
{t("owner")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
|
@ -73,16 +76,16 @@ export default function MemberListItem(props: {
|
||||||
color="warn"
|
color="warn"
|
||||||
StartIcon={UserRemoveIcon}
|
StartIcon={UserRemoveIcon}
|
||||||
className="w-full">
|
className="w-full">
|
||||||
Remove User
|
{t("remove_member")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
|
localeProp={locale}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title="Remove member"
|
title={t("remove_member")}
|
||||||
confirmBtnText="Yes, remove member"
|
confirmBtnText={t("confirm_remove_member")}
|
||||||
cancelBtnText="Cancel"
|
|
||||||
onConfirm={() => props.onActionSelect("remove")}>
|
onConfirm={() => props.onActionSelect("remove")}>
|
||||||
Are you sure you want to remove this member from the team?
|
{t("remove_member_confirmation_message")}
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { Team } from "@lib/team";
|
import { Team } from "@lib/team";
|
||||||
|
|
||||||
import TeamListItem from "./TeamListItem";
|
import TeamListItem from "./TeamListItem";
|
||||||
|
|
||||||
export default function TeamList(props: {
|
export default function TeamList(props: {
|
||||||
|
localeProp: string;
|
||||||
teams: Team[];
|
teams: Team[];
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
onEditTeam: (text: Team) => void;
|
onEditTeam: (text: Team) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||||
|
|
||||||
const selectAction = (action: string, team: Team) => {
|
const selectAction = (action: string, team: Team) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "edit":
|
case "edit":
|
||||||
|
@ -30,6 +34,7 @@ export default function TeamList(props: {
|
||||||
<ul className="px-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
<ul className="px-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
||||||
{props.teams.map((team: Team) => (
|
{props.teams.map((team: Team) => (
|
||||||
<TeamListItem
|
<TeamListItem
|
||||||
|
localeProp={locale}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
key={team.id}
|
key={team.id}
|
||||||
team={team}
|
team={team}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import showToast from "@lib/notification";
|
import showToast from "@lib/notification";
|
||||||
|
|
||||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
import { Dialog, DialogTrigger } from "@components/Dialog";
|
||||||
|
@ -30,12 +31,14 @@ interface Team {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamListItem(props: {
|
export default function TeamListItem(props: {
|
||||||
|
localeProp: string;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
key: number;
|
key: number;
|
||||||
team: Team;
|
team: Team;
|
||||||
onActionSelect: (text: string) => void;
|
onActionSelect: (text: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [team, setTeam] = useState<Team | null>(props.team);
|
const [team, setTeam] = useState<Team | null>(props.team);
|
||||||
|
const { t, locale } = useLocale({ localeProp: props.localeProp });
|
||||||
|
|
||||||
const acceptInvite = () => invitationResponse(true);
|
const acceptInvite = () => invitationResponse(true);
|
||||||
const declineInvite = () => invitationResponse(false);
|
const declineInvite = () => invitationResponse(false);
|
||||||
|
@ -79,24 +82,24 @@ export default function TeamListItem(props: {
|
||||||
{props.team.role === "INVITEE" && (
|
{props.team.role === "INVITEE" && (
|
||||||
<div>
|
<div>
|
||||||
<Button type="button" color="secondary" onClick={declineInvite}>
|
<Button type="button" color="secondary" onClick={declineInvite}>
|
||||||
Reject
|
{t("reject")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" color="primary" className="ml-1" onClick={acceptInvite}>
|
<Button type="button" color="primary" className="ml-1" onClick={acceptInvite}>
|
||||||
Accept
|
{t("accept")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{props.team.role === "MEMBER" && (
|
{props.team.role === "MEMBER" && (
|
||||||
<div>
|
<div>
|
||||||
<Button type="button" color="primary" onClick={declineInvite}>
|
<Button type="button" color="primary" onClick={declineInvite}>
|
||||||
Leave
|
{t("leave")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{props.team.role === "OWNER" && (
|
{props.team.role === "OWNER" && (
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<span className="self-center h-6 px-3 py-1 text-xs text-gray-700 capitalize rounded-md bg-gray-50">
|
<span className="self-center h-6 px-3 py-1 text-xs text-gray-700 capitalize rounded-md bg-gray-50">
|
||||||
Owner
|
{t("owner")}
|
||||||
</span>
|
</span>
|
||||||
<Tooltip content="Copy link">
|
<Tooltip content="Copy link">
|
||||||
<Button
|
<Button
|
||||||
|
@ -104,7 +107,7 @@ export default function TeamListItem(props: {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
process.env.NEXT_PUBLIC_APP_URL + "/team/" + props.team.slug
|
process.env.NEXT_PUBLIC_APP_URL + "/team/" + props.team.slug
|
||||||
);
|
);
|
||||||
showToast("Link copied!", "success");
|
showToast(t("link_copied"), "success");
|
||||||
}}
|
}}
|
||||||
size="icon"
|
size="icon"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
|
@ -124,14 +127,16 @@ export default function TeamListItem(props: {
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => props.onActionSelect("edit")}
|
onClick={() => props.onActionSelect("edit")}
|
||||||
StartIcon={PencilAltIcon}>
|
StartIcon={PencilAltIcon}>
|
||||||
Edit team
|
{" "}
|
||||||
|
{t("edit_team")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${props.team.slug}`} passHref={true}>
|
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${props.team.slug}`} passHref={true}>
|
||||||
<a target="_blank">
|
<a target="_blank">
|
||||||
<Button type="button" color="minimal" className="w-full" StartIcon={ExternalLinkIcon}>
|
<Button type="button" color="minimal" className="w-full" StartIcon={ExternalLinkIcon}>
|
||||||
Preview team page
|
{" "}
|
||||||
|
{t("preview_team")}
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -146,17 +151,16 @@ export default function TeamListItem(props: {
|
||||||
color="warn"
|
color="warn"
|
||||||
StartIcon={TrashIcon}
|
StartIcon={TrashIcon}
|
||||||
className="w-full">
|
className="w-full">
|
||||||
Disband Team
|
{t("disband_team")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
|
localeProp={locale}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title="Disband Team"
|
title="Disband Team"
|
||||||
confirmBtnText="Yes, disband team"
|
confirmBtnText={t("confirm_disband_team")}
|
||||||
cancelBtnText="Cancel"
|
|
||||||
onConfirm={() => props.onActionSelect("disband")}>
|
onConfirm={() => props.onActionSelect("disband")}>
|
||||||
Are you sure you want to disband this team? Anyone who you've shared this team
|
{t("disband_team_confirmation_message")}
|
||||||
link with will no longer be able to book using it.
|
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
@ -4,11 +4,15 @@ import classnames from "classnames";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
import Text from "@components/ui/Text";
|
import Text from "@components/ui/Text";
|
||||||
|
|
||||||
const Team = ({ team }) => {
|
const Team = ({ team, localeProp }) => {
|
||||||
|
const { t } = useLocale({ localeProp: localeProp });
|
||||||
|
|
||||||
const Member = ({ member }) => {
|
const Member = ({ member }) => {
|
||||||
const classes = classnames(
|
const classes = classnames(
|
||||||
"group",
|
"group",
|
||||||
|
@ -71,7 +75,7 @@ const Team = ({ team }) => {
|
||||||
{team.eventTypes.length > 0 && (
|
{team.eventTypes.length > 0 && (
|
||||||
<aside className="text-center dark:text-white mt-8">
|
<aside className="text-center dark:text-white mt-8">
|
||||||
<Button color="secondary" href={`/team/${team.slug}`} shallow={true} StartIcon={ArrowLeftIcon}>
|
<Button color="secondary" href={`/team/${team.slug}`} shallow={true} StartIcon={ArrowLeftIcon}>
|
||||||
Go back
|
{t("go_back")}
|
||||||
</Button>
|
</Button>
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
|
127
crowdin.yml
Normal file
127
crowdin.yml
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
#
|
||||||
|
# Your Crowdin credentials
|
||||||
|
#
|
||||||
|
"project_id_env" : "CROWDIN_PROJECT_ID"
|
||||||
|
"api_token_env" : "CROWDIN_PERSONAL_TOKEN"
|
||||||
|
"base_path" : "."
|
||||||
|
"base_url" : "https://cal.crowdin.com"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Choose file structure in Crowdin
|
||||||
|
# e.g. true or false
|
||||||
|
#
|
||||||
|
"preserve_hierarchy": true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Files configuration
|
||||||
|
#
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
#
|
||||||
|
# Source files filter
|
||||||
|
# e.g. "/resources/en/*.json"
|
||||||
|
#
|
||||||
|
"source" : "/public/static/locales/en/*.json",
|
||||||
|
|
||||||
|
#
|
||||||
|
# Where translations will be placed
|
||||||
|
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
||||||
|
#
|
||||||
|
"translation" : "/public/static/locales/%two_letters_code%/%original_file_name%",
|
||||||
|
|
||||||
|
#
|
||||||
|
# Files or directories for ignore
|
||||||
|
# e.g. ["/**/?.txt", "/**/[0-9].txt", "/**/*\?*.txt"]
|
||||||
|
#
|
||||||
|
#"ignore" : [],
|
||||||
|
|
||||||
|
#
|
||||||
|
# The dest allows you to specify a file name in Crowdin
|
||||||
|
# e.g. "/messages.json"
|
||||||
|
#
|
||||||
|
#"dest" : "",
|
||||||
|
|
||||||
|
#
|
||||||
|
# File type
|
||||||
|
# e.g. "json"
|
||||||
|
#
|
||||||
|
#"type" : "",
|
||||||
|
|
||||||
|
#
|
||||||
|
# The parameter "update_option" is optional. If it is not set, after the files update the translations for changed strings will be removed. Use to fix typos and for minor changes in the source strings
|
||||||
|
# e.g. "update_as_unapproved" or "update_without_changes"
|
||||||
|
#
|
||||||
|
#"update_option" : "",
|
||||||
|
|
||||||
|
#
|
||||||
|
# Start block (for XML only)
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Defines whether to translate tags attributes.
|
||||||
|
# e.g. 0 or 1 (Default is 1)
|
||||||
|
#
|
||||||
|
# "translate_attributes" : 1,
|
||||||
|
|
||||||
|
#
|
||||||
|
# Defines whether to translate texts placed inside the tags.
|
||||||
|
# e.g. 0 or 1 (Default is 1)
|
||||||
|
#
|
||||||
|
# "translate_content" : 1,
|
||||||
|
|
||||||
|
#
|
||||||
|
# This is an array of strings, where each item is the XPaths to DOM element that should be imported
|
||||||
|
# e.g. ["/content/text", "/content/text[@value]"]
|
||||||
|
#
|
||||||
|
# "translatable_elements" : [],
|
||||||
|
|
||||||
|
#
|
||||||
|
# Defines whether to split long texts into smaller text segments
|
||||||
|
# e.g. 0 or 1 (Default is 1)
|
||||||
|
#
|
||||||
|
# "content_segmentation" : 1,
|
||||||
|
|
||||||
|
#
|
||||||
|
# End block (for XML only)
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Start .properties block
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Defines whether single quote should be escaped by another single quote or backslash in exported translations
|
||||||
|
# e.g. 0 or 1 or 2 or 3 (Default is 3)
|
||||||
|
# 0 - do not escape single quote;
|
||||||
|
# 1 - escape single quote by another single quote;
|
||||||
|
# 2 - escape single quote by backslash;
|
||||||
|
# 3 - escape single quote by another single quote only in strings containing variables ( {0} ).
|
||||||
|
#
|
||||||
|
# "escape_quotes" : 3,
|
||||||
|
|
||||||
|
#
|
||||||
|
# Defines whether any special characters (=, :, ! and #) should be escaped by backslash in exported translations.
|
||||||
|
# e.g. 0 or 1 (Default is 0)
|
||||||
|
# 0 - do not escape special characters
|
||||||
|
# 1 - escape special characters by a backslash
|
||||||
|
#
|
||||||
|
# "escape_special_characters": 0
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# End .properties block
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Does the first line contain header?
|
||||||
|
# e.g. true or false
|
||||||
|
#
|
||||||
|
#"first_line_contains_header" : true,
|
||||||
|
|
||||||
|
#
|
||||||
|
# for spreadsheets
|
||||||
|
# e.g. "identifier,source_phrase,context,uk,ru,fr"
|
||||||
|
#
|
||||||
|
# "scheme" : "",
|
||||||
|
}
|
||||||
|
]
|
|
@ -6,8 +6,8 @@ import prisma from "@lib/prisma";
|
||||||
|
|
||||||
import { i18n } from "../../../next-i18next.config";
|
import { i18n } from "../../../next-i18next.config";
|
||||||
|
|
||||||
export const extractLocaleInfo = async (req: IncomingMessage) => {
|
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage) => {
|
||||||
const session = await getSession({ req: req });
|
const session = await getSession({ req });
|
||||||
const preferredLocale = parser.pick(i18n.locales, req.headers["accept-language"]);
|
const preferredLocale = parser.pick(i18n.locales, req.headers["accept-language"]);
|
||||||
|
|
||||||
if (session?.user?.id) {
|
if (session?.user?.id) {
|
||||||
|
@ -58,7 +58,14 @@ interface localeType {
|
||||||
|
|
||||||
export const localeLabels: localeType = {
|
export const localeLabels: localeType = {
|
||||||
en: "English",
|
en: "English",
|
||||||
|
fr: "French",
|
||||||
|
it: "Italian",
|
||||||
|
ru: "Russian",
|
||||||
|
es: "Spanish",
|
||||||
|
de: "German",
|
||||||
|
pt: "Portuguese",
|
||||||
ro: "Romanian",
|
ro: "Romanian",
|
||||||
|
nl: "Dutch",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OptionType = {
|
export type OptionType = {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
type LocaleProps = {
|
type LocaleProp = {
|
||||||
localeProp: string;
|
localeProp: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLocale = (props: LocaleProps) => {
|
export const useLocale = (props: LocaleProp) => {
|
||||||
const { i18n, t } = useTranslation("common");
|
const { i18n, t } = useTranslation("common");
|
||||||
|
|
||||||
if (i18n.language !== props.localeProp) {
|
if (i18n.language !== props.localeProp) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ const path = require("path");
|
||||||
module.exports = {
|
module.exports = {
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
locales: ["en", "ro"],
|
locales: ["en", "fr", "it", "ru", "es", "de", "pt", "ro", "nl"],
|
||||||
},
|
},
|
||||||
localePath: path.resolve("./public/static/locales"),
|
localePath: path.resolve("./public/static/locales"),
|
||||||
};
|
};
|
||||||
|
|
119
pages/[user].tsx
119
pages/[user].tsx
|
@ -1,29 +1,23 @@
|
||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { trpc } from "@lib/trpc";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
|
|
||||||
import { ssg } from "@server/ssg";
|
export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
|
const { isReady } = useTheme(props.user.theme);
|
||||||
export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
const { user, localeProp, eventTypes } = props;
|
||||||
const { username } = props;
|
const { t, locale } = useLocale({ localeProp });
|
||||||
// data of query below will be will be prepopulated b/c of `getStaticProps`
|
|
||||||
const query = trpc.useQuery(["booking.userEventTypes", { username }]);
|
|
||||||
const { isReady } = useTheme(query.data?.user.theme);
|
|
||||||
if (!query.data) {
|
|
||||||
// this shold never happen as we do `blocking: true`
|
|
||||||
return <>...</>;
|
|
||||||
}
|
|
||||||
const { user, eventTypes } = query.data;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -56,7 +50,7 @@ export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
||||||
<Link href={`/${user.username}/${type.slug}`}>
|
<Link href={`/${user.username}/${type.slug}`}>
|
||||||
<a className="block px-6 py-4">
|
<a className="block px-6 py-4">
|
||||||
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||||
<EventTypeDescription eventType={type} />
|
<EventTypeDescription localeProp={locale} eventType={type} />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,8 +59,10 @@ export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
||||||
{eventTypes.length === 0 && (
|
{eventTypes.length === 0 && (
|
||||||
<div className="shadow overflow-hidden rounded-sm">
|
<div className="shadow overflow-hidden rounded-sm">
|
||||||
<div className="p-8 text-center text-gray-400 dark:text-white">
|
<div className="p-8 text-center text-gray-400 dark:text-white">
|
||||||
<h2 className="font-cal font-semibold text-3xl text-gray-600 dark:text-white">Uh oh!</h2>
|
<h2 className="font-cal font-semibold text-3xl text-gray-600 dark:text-white">
|
||||||
<p className="max-w-md mx-auto">This user hasn't set up any event types yet.</p>
|
{t("uh_oh")}
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-md mx-auto">{t("no_event_types_have_been_setup")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -77,43 +73,76 @@ export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const allUsers = await prisma.user.findMany({
|
const username = (context.query.user as string).toLowerCase();
|
||||||
select: {
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
username: true,
|
|
||||||
},
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
// will statically render everyone on the PRO plan
|
username: username.toLowerCase(),
|
||||||
// the rest will be statically rendered on first visit
|
},
|
||||||
plan: "PRO",
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
bio: true,
|
||||||
|
avatar: true,
|
||||||
|
theme: true,
|
||||||
|
plan: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const usernames = allUsers.flatMap((u) => (u.username ? [u.username] : []));
|
|
||||||
return {
|
|
||||||
paths: usernames.map((user) => ({
|
|
||||||
params: { user },
|
|
||||||
})),
|
|
||||||
|
|
||||||
// https://nextjs.org/docs/basic-features/data-fetching#fallback-blocking
|
if (!user) {
|
||||||
fallback: "blocking",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getStaticProps(context: GetStaticPropsContext<{ user: string }>) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const username = context.params!.user;
|
|
||||||
const data = await ssg.fetchQuery("booking.userEventTypes", { username });
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventTypesWithHidden = await prisma.eventType.findMany({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
teamId: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
users: {
|
||||||
|
some: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
title: true,
|
||||||
|
length: true,
|
||||||
|
description: true,
|
||||||
|
hidden: true,
|
||||||
|
schedulingType: true,
|
||||||
|
price: true,
|
||||||
|
currency: true,
|
||||||
|
},
|
||||||
|
take: user.plan === "FREE" ? 1 : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: ssg.dehydrate(),
|
localeProp: locale,
|
||||||
username,
|
user,
|
||||||
|
eventTypes,
|
||||||
|
...(await serverSideTranslations(locale, ["common"])),
|
||||||
},
|
},
|
||||||
revalidate: 1,
|
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { GetServerSidePropsContext } from "next";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils";
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
@ -16,7 +16,9 @@ export default function Type(props: AvailabilityPageProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const locale = await extractLocaleInfo(context.req);
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
|
// get query params and typecast them to string
|
||||||
|
// (would be even better to assert them instead of typecasting)
|
||||||
const userParam = asStringOrNull(context.query.user);
|
const userParam = asStringOrNull(context.query.user);
|
||||||
const typeParam = asStringOrNull(context.query.type);
|
const typeParam = asStringOrNull(context.query.type);
|
||||||
const dateParam = asStringOrNull(context.query.date);
|
const dateParam = asStringOrNull(context.query.date);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { GetServerSidePropsContext } from "next";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
|
||||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils";
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export default function Book(props: BookPageProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const locale = await extractLocaleInfo(context.req);
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -37,7 +37,8 @@ import {
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils";
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
|
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
|
||||||
import { LocationType } from "@lib/location";
|
import { LocationType } from "@lib/location";
|
||||||
import deleteEventType from "@lib/mutations/event-types/delete-event-type";
|
import deleteEventType from "@lib/mutations/event-types/delete-event-type";
|
||||||
|
@ -84,6 +85,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
const { eventType, locationOptions, availability, team, teamMembers, hasPaymentIntegration, currency } =
|
const { eventType, locationOptions, availability, team, teamMembers, hasPaymentIntegration, currency } =
|
||||||
props;
|
props;
|
||||||
|
|
||||||
|
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||||
|
|
||||||
|
@ -983,6 +985,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
Delete
|
Delete
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
|
localeProp={locale}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title="Delete Event Type"
|
title="Delete Event Type"
|
||||||
confirmBtnText="Yes, delete event type"
|
confirmBtnText="Yes, delete event type"
|
||||||
|
@ -1097,7 +1100,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { req, query } = context;
|
const { req, query } = context;
|
||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
const locale = await extractLocaleInfo(context.req);
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
|
|
||||||
const typeParam = parseInt(asStringOrThrow(query.type));
|
const typeParam = parseInt(asStringOrThrow(query.type));
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils";
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started";
|
import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||||
|
@ -56,10 +56,7 @@ type Profile = PageProps["profiles"][number];
|
||||||
type MembershipCount = EventType["metadata"]["membershipCount"];
|
type MembershipCount = EventType["metadata"]["membershipCount"];
|
||||||
|
|
||||||
const EventTypesPage = (props: PageProps) => {
|
const EventTypesPage = (props: PageProps) => {
|
||||||
const { locale } = useLocale({
|
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||||
localeProp: props.localeProp,
|
|
||||||
namespaces: "event-types-page",
|
|
||||||
});
|
|
||||||
|
|
||||||
const CreateFirstEventTypeView = () => (
|
const CreateFirstEventTypeView = () => (
|
||||||
<div className="md:py-20">
|
<div className="md:py-20">
|
||||||
|
@ -164,7 +161,7 @@ const EventTypesPage = (props: PageProps) => {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<EventTypeDescription eventType={type} />
|
<EventTypeDescription localeProp={locale} eventType={type} />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
@ -379,13 +376,13 @@ const CreateNewEventDialog = ({
|
||||||
disabled: true,
|
disabled: true,
|
||||||
})}
|
})}
|
||||||
StartIcon={PlusIcon}>
|
StartIcon={PlusIcon}>
|
||||||
{t("new-event-type-btn")}
|
{t("new_event_type_btn")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{profiles.filter((profile) => profile.teamId).length > 0 && (
|
{profiles.filter((profile) => profile.teamId).length > 0 && (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button EndIcon={ChevronDownIcon}>{t("new-event-type-btn")}</Button>
|
<Button EndIcon={ChevronDownIcon}>{t("new_event_type_btn")}</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Create an event type under your name or a team.</DropdownMenuLabel>
|
<DropdownMenuLabel>Create an event type under your name or a team.</DropdownMenuLabel>
|
||||||
|
@ -563,7 +560,7 @@ const CreateNewEventDialog = ({
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
const locale = await extractLocaleInfo(context.req);
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
|
|
|
@ -7,7 +7,12 @@ import TimezoneSelect from "react-timezone-select";
|
||||||
|
|
||||||
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { extractLocaleInfo, localeLabels, localeOptions, OptionType } from "@lib/core/i18n/i18n.utils";
|
import {
|
||||||
|
getOrSetUserLocaleFromHeaders,
|
||||||
|
localeLabels,
|
||||||
|
localeOptions,
|
||||||
|
OptionType,
|
||||||
|
} from "@lib/core/i18n/i18n.utils";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
@ -416,7 +421,7 @@ export default function Settings(props: Props) {
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
const locale = await extractLocaleInfo(context.req);
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
import SettingsShell from "@components/SettingsShell";
|
import SettingsShell from "@components/SettingsShell";
|
||||||
|
@ -8,12 +11,14 @@ import Shell from "@components/Shell";
|
||||||
import ChangePasswordSection from "@components/security/ChangePasswordSection";
|
import ChangePasswordSection from "@components/security/ChangePasswordSection";
|
||||||
import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection";
|
import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection";
|
||||||
|
|
||||||
export default function Security({ user }) {
|
export default function Security({ user, localeProp }) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { locale, t } = useLocale({ localeProp });
|
||||||
return (
|
return (
|
||||||
<Shell heading="Security" subtitle="Manage your account's security.">
|
<Shell heading={t("security")} subtitle={t("manage_account_security")}>
|
||||||
<SettingsShell>
|
<SettingsShell>
|
||||||
<ChangePasswordSection />
|
<ChangePasswordSection localeProp={locale} />
|
||||||
<TwoFactorAuthSection twoFactorEnabled={user.twoFactorEnabled} />
|
<TwoFactorAuthSection localeProp={locale} twoFactorEnabled={user.twoFactorEnabled} />
|
||||||
</SettingsShell>
|
</SettingsShell>
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
|
@ -21,6 +26,8 @@ export default function Security({ user }) {
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
}
|
}
|
||||||
|
@ -38,6 +45,11 @@ export async function getServerSideProps(context) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { session, user },
|
props: {
|
||||||
|
localeProp: locale,
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
...(await serverSideTranslations(locale, ["common"])),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,12 @@ import { PlusIcon } from "@heroicons/react/solid";
|
||||||
import { GetServerSideProps } from "next";
|
import { GetServerSideProps } from "next";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import { useSession } from "next-auth/client";
|
import { useSession } from "next-auth/client";
|
||||||
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { Member } from "@lib/member";
|
import { Member } from "@lib/member";
|
||||||
import { Team } from "@lib/team";
|
import { Team } from "@lib/team";
|
||||||
|
|
||||||
|
@ -17,7 +20,7 @@ import TeamList from "@components/team/TeamList";
|
||||||
import TeamListItem from "@components/team/TeamListItem";
|
import TeamListItem from "@components/team/TeamListItem";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
|
|
||||||
export default function Teams() {
|
export default function Teams(props: { localeProp: string }) {
|
||||||
const noop = () => undefined;
|
const noop = () => undefined;
|
||||||
const [, loading] = useSession();
|
const [, loading] = useSession();
|
||||||
const [teams, setTeams] = useState([]);
|
const [teams, setTeams] = useState([]);
|
||||||
|
@ -26,6 +29,7 @@ export default function Teams() {
|
||||||
const [editTeamEnabled, setEditTeamEnabled] = useState(false);
|
const [editTeamEnabled, setEditTeamEnabled] = useState(false);
|
||||||
const [teamToEdit, setTeamToEdit] = useState<Team | null>();
|
const [teamToEdit, setTeamToEdit] = useState<Team | null>();
|
||||||
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
||||||
|
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||||
|
|
||||||
const handleErrors = async (resp: Response) => {
|
const handleErrors = async (resp: Response) => {
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
|
@ -110,7 +114,11 @@ export default function Teams() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{!!teams.length && (
|
{!!teams.length && (
|
||||||
<TeamList teams={teams} onChange={loadData} onEditTeam={editTeam}></TeamList>
|
<TeamList
|
||||||
|
localeProp={locale}
|
||||||
|
teams={teams}
|
||||||
|
onChange={loadData}
|
||||||
|
onEditTeam={editTeam}></TeamList>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!!invites.length && (
|
{!!invites.length && (
|
||||||
|
@ -119,6 +127,7 @@ export default function Teams() {
|
||||||
<ul className="px-4 mt-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
<ul className="px-4 mt-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
||||||
{invites.map((team: Team) => (
|
{invites.map((team: Team) => (
|
||||||
<TeamListItem
|
<TeamListItem
|
||||||
|
localeProp={locale}
|
||||||
onChange={loadData}
|
onChange={loadData}
|
||||||
key={team.id}
|
key={team.id}
|
||||||
team={team}
|
team={team}
|
||||||
|
@ -131,7 +140,7 @@ export default function Teams() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!editTeamEnabled && <EditTeam team={teamToEdit} onCloseEdit={onCloseEdit} />}
|
{!!editTeamEnabled && <EditTeam localeProp={locale} team={teamToEdit} onCloseEdit={onCloseEdit} />}
|
||||||
{showCreateTeamModal && (
|
{showCreateTeamModal && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 overflow-y-auto"
|
className="fixed inset-0 z-50 overflow-y-auto"
|
||||||
|
@ -200,11 +209,16 @@ export default function Teams() {
|
||||||
// Export the `session` prop to use sessions with Server Side Rendering
|
// Export the `session` prop to use sessions with Server Side Rendering
|
||||||
export const getServerSideProps: GetServerSideProps<{ session: Session | null }> = async (context) => {
|
export const getServerSideProps: GetServerSideProps<{ session: Session | null }> = async (context) => {
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { session },
|
props: {
|
||||||
|
session,
|
||||||
|
localeProp: locale,
|
||||||
|
...(await serverSideTranslations(locale, ["common"])),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
@ -18,9 +21,10 @@ import AvatarGroup from "@components/ui/AvatarGroup";
|
||||||
import Button from "@components/ui/Button";
|
import Button from "@components/ui/Button";
|
||||||
import Text from "@components/ui/Text";
|
import Text from "@components/ui/Text";
|
||||||
|
|
||||||
function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
function TeamPage({ team, localeProp }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { isReady } = useTheme();
|
const { isReady } = useTheme();
|
||||||
const showMembers = useToggleQuery("members");
|
const showMembers = useToggleQuery("members");
|
||||||
|
const { t, locale } = useLocale({ localeProp: localeProp });
|
||||||
|
|
||||||
const eventTypes = (
|
const eventTypes = (
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
|
@ -33,7 +37,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
<a className="px-6 py-4 flex justify-between">
|
<a className="px-6 py-4 flex justify-between">
|
||||||
<div className="flex-shrink">
|
<div className="flex-shrink">
|
||||||
<h2 className="font-cal font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
<h2 className="font-cal font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||||
<EventTypeDescription className="text-sm" eventType={type} />
|
<EventTypeDescription localeProp={locale} className="text-sm" eventType={type} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
|
@ -64,7 +68,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
<Avatar alt={teamName} imageSrc={team.logo} className="mx-auto w-20 h-20 rounded-full mb-4" />
|
<Avatar alt={teamName} imageSrc={team.logo} className="mx-auto w-20 h-20 rounded-full mb-4" />
|
||||||
<Text variant="headline">{teamName}</Text>
|
<Text variant="headline">{teamName}</Text>
|
||||||
</div>
|
</div>
|
||||||
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
|
{(showMembers.isOn || !team.eventTypes.length) && <Team localeProp={locale} team={team} />}
|
||||||
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
||||||
<div className="mx-auto max-w-3xl">
|
<div className="mx-auto max-w-3xl">
|
||||||
{eventTypes}
|
{eventTypes}
|
||||||
|
@ -75,7 +79,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
<span className="px-2 bg-gray-100 text-sm text-gray-500 dark:bg-black dark:text-gray-500">
|
<span className="px-2 bg-gray-100 text-sm text-gray-500 dark:bg-black dark:text-gray-500">
|
||||||
OR
|
{t("or")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,7 +90,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
EndIcon={ArrowRightIcon}
|
EndIcon={ArrowRightIcon}
|
||||||
href={`/team/${team.slug}?members=1`}
|
href={`/team/${team.slug}?members=1`}
|
||||||
shallow={true}>
|
shallow={true}>
|
||||||
Book a team member instead
|
{t("book_a_team_member")}
|
||||||
</Button>
|
</Button>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,6 +102,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
||||||
|
|
||||||
const userSelect = Prisma.validator<Prisma.UserSelect>()({
|
const userSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||||
|
@ -160,7 +165,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
localeProp: locale,
|
||||||
team,
|
team,
|
||||||
|
...(await serverSideTranslations(locale, ["common"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { GetServerSidePropsContext } from "next";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils";
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ export default function TeamType(props: AvailabilityTeamPageProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const locale = await extractLocaleInfo(context.req);
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
const slugParam = asStringOrNull(context.query.slug);
|
const slugParam = asStringOrNull(context.query.slug);
|
||||||
const typeParam = asStringOrNull(context.query.type);
|
const typeParam = asStringOrNull(context.query.type);
|
||||||
const dateParam = asStringOrNull(context.query.date);
|
const dateParam = asStringOrNull(context.query.date);
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import "react-phone-number-input/style.css";
|
import "react-phone-number-input/style.css";
|
||||||
|
|
||||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
|
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
@ -14,6 +16,7 @@ export default function TeamBookingPage(props: TeamBookingPageProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||||
const eventTypeId = parseInt(asStringOrThrow(context.query.type));
|
const eventTypeId = parseInt(asStringOrThrow(context.query.type));
|
||||||
if (typeof eventTypeId !== "number" || eventTypeId % 1 !== 0) {
|
if (typeof eventTypeId !== "number" || eventTypeId % 1 !== 0) {
|
||||||
return {
|
return {
|
||||||
|
@ -86,6 +89,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
localeProp: locale,
|
||||||
profile: {
|
profile: {
|
||||||
...eventTypeObject.team,
|
...eventTypeObject.team,
|
||||||
slug: "team/" + eventTypeObject.slug,
|
slug: "team/" + eventTypeObject.slug,
|
||||||
|
@ -94,6 +98,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
},
|
},
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking,
|
booking,
|
||||||
|
...(await serverSideTranslations(locale, ["common"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
147
public/static/locales/de/common.json
Normal file
147
public/static/locales/de/common.json
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
{
|
||||||
|
"edit_logo": "Logo bearbeiten",
|
||||||
|
"upload_a_logo": "Logo hochladen",
|
||||||
|
"enable": "Aktivieren",
|
||||||
|
"code": "Code",
|
||||||
|
"code_is_incorrect": "Code ist falsch.",
|
||||||
|
"add_an_extra_layer_of_security": "Fügen Sie Ihrem Konto eine zusätzliche Sicherheitsstufe hinzu, falls Ihr Passwort gestohlen wird.",
|
||||||
|
"2fa": "Zwei-Faktor-Authentifizierung",
|
||||||
|
"enable_2fa": "Zwei-Faktor-Authentifizierung aktivieren",
|
||||||
|
"disable_2fa": "Zwei-Faktor-Authentifizierung deaktivieren",
|
||||||
|
"disable_2fa_recommendation": "Falls Sie 2FA deaktivieren müssen, empfehlen wir, es so schnell wie möglich wieder zu aktivieren.",
|
||||||
|
"error_disabling_2fa": "Fehler beim Deaktivieren der Zwei-Faktor-Authentifizierung",
|
||||||
|
"error_enabling_2fa": "Fehler beim Einrichten der Zwei-Faktor-Authentifizierung",
|
||||||
|
"security": "Sicherheit",
|
||||||
|
"manage_account_security": "Verwalten Sie die Sicherheit Ihres Kontos.",
|
||||||
|
"password": "Passwort",
|
||||||
|
"password_updated_successfully": "Passwort erfolgreich aktualisiert",
|
||||||
|
"password_has_been_changed": "Ihr Passwort wurde erfolgreich geändert.",
|
||||||
|
"error_changing_password": "Fehler beim Ändern des Passworts",
|
||||||
|
"something_went_wrong": "Etwas ist schief gelaufen",
|
||||||
|
"please_try_again": "Bitte erneut versuchen",
|
||||||
|
"super_secure_new_password": "Ihr supersicheres neues Passwort",
|
||||||
|
"new_password": "Neues Passwort",
|
||||||
|
"your_old_password": "Ihr altes Passwort",
|
||||||
|
"current_password": "Aktuelles Passwort",
|
||||||
|
"change_password": "Passwort ändern",
|
||||||
|
"new_password_matches_old_password": "Neues Passwort stimmt mit Ihrem alten Passwort überein. Bitte wählen Sie ein anderes Passwort.",
|
||||||
|
"current_incorrect_password": "Aktuelles Passwort ist falsch",
|
||||||
|
"incorrect_password": "Passwort ist falsch",
|
||||||
|
"1_on_1": "1-on-1",
|
||||||
|
"24_h": "24 Std",
|
||||||
|
"use_setting": "Benutze Einstellung",
|
||||||
|
"am_pm": "am/pm",
|
||||||
|
"time_options": "Zeitoptionen",
|
||||||
|
"january": "Januar",
|
||||||
|
"february": "Februar",
|
||||||
|
"march": "März",
|
||||||
|
"april": "April",
|
||||||
|
"may": "Mai",
|
||||||
|
"june": "Juni",
|
||||||
|
"july": "Juli",
|
||||||
|
"august": "August",
|
||||||
|
"september": "September",
|
||||||
|
"october": "Oktober",
|
||||||
|
"november": "November",
|
||||||
|
"december": "Dezember",
|
||||||
|
"monday": "Montag",
|
||||||
|
"tuesday": "Dienstag",
|
||||||
|
"wednesday": "Mittwoch",
|
||||||
|
"thursday": "Donnerstag",
|
||||||
|
"friday": "Freitag",
|
||||||
|
"saturday": "Samstag",
|
||||||
|
"sunday": "Sonntag",
|
||||||
|
"all_booked_today": "Ausgebucht für heute.",
|
||||||
|
"slots_load_fail": "Die verfügbaren Zeitfenster konnten nicht geladen werden.",
|
||||||
|
"additional_guests": "+ Weitere Gäste",
|
||||||
|
"your_name": "Ihr Name",
|
||||||
|
"email_address": "E-Mail Adresse",
|
||||||
|
"location": "Ort",
|
||||||
|
"yes": "Ja",
|
||||||
|
"no": "Nein",
|
||||||
|
"additional_notes": "Zusätzliche Notizen",
|
||||||
|
"booking_fail": "Termin konnte nicht gebucht werden.",
|
||||||
|
"reschedule_fail": "Termin konnte nicht neugeplant werden.",
|
||||||
|
"share_additional_notes": "Bitten teilen Sie Notizen zur Vorbereitung des Termins, falls nötig.",
|
||||||
|
"booking_confirmation": "Bestätigen Sie {{eventTypeTitle}} mit {{profileName}}",
|
||||||
|
"booking_reschedule_confirmation": "Planen Sie Ihr {{eventTypeTitle}} mit {{profileName}} um",
|
||||||
|
"in_person_meeting": "Link oder Vor-Ort-Termin",
|
||||||
|
"phone_call": "Telefonat",
|
||||||
|
"phone_number": "Telefonnummer",
|
||||||
|
"enter_phone_number": "Telefonnummer eingeben",
|
||||||
|
"reschedule": "Neuplanen",
|
||||||
|
"book_a_team_member": "Teammitglied stattdessen buchen",
|
||||||
|
"or": "ODER",
|
||||||
|
"go_back": "Zurück",
|
||||||
|
"email_or_username": "E-Mail oder Benutzername",
|
||||||
|
"send_invite_email": "Einladungs-E-Mail senden",
|
||||||
|
"role": "Rolle",
|
||||||
|
"edit_team": "Team bearbeiten",
|
||||||
|
"reject": "Ablehnen",
|
||||||
|
"accept": "Annehmen",
|
||||||
|
"leave": "Verlassen",
|
||||||
|
"profile": "Profil",
|
||||||
|
"my_team_url": "Meine Team-URL",
|
||||||
|
"team_name": "Teamname",
|
||||||
|
"your_team_name": "Ihr Teamname",
|
||||||
|
"team_updated_successfully": "Team erfolgreich aktualisiert",
|
||||||
|
"your_team_updated_successfully": "Ihr Team wurde erfolgreich aktualisiert.",
|
||||||
|
"about": "Beschreibung",
|
||||||
|
"team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.",
|
||||||
|
"members": "Mitglieder",
|
||||||
|
"member": "Mitglied",
|
||||||
|
"owner": "Inhaber",
|
||||||
|
"new_member": "Neues Mitglied",
|
||||||
|
"invite": "Einladen",
|
||||||
|
"invite_new_member": "Ein neues Mitglied einladen",
|
||||||
|
"invite_new_team_member": "Laden Sie jemanden in Ihr Team ein.",
|
||||||
|
"disable_cal_branding": "Cal.com Werbung deaktivieren",
|
||||||
|
"disable_cal_branding_description": "Verstecken Sie Cal.com Werbung auf Ihren öffentlichen Seiten.",
|
||||||
|
"danger_zone": "Achtung",
|
||||||
|
"back": "Zurück",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"continue": "Weiter",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"disband_team": "Team auflösen",
|
||||||
|
"disband_team_confirmation_message": "Bist du sicher, dass du dieses Team auflösen möchtest? Jeder der diesen Team-Link erhalten hat, kann Sie nicht mehr buchen.",
|
||||||
|
"remove_member_confirmation_message": "Sind Sie sicher, dass Sie dieses Mitglied aus dem Team entfernen möchten?",
|
||||||
|
"confirm_disband_team": "Ja, Team auflösen",
|
||||||
|
"confirm_remove_member": "Ja, Mitglied entfernen",
|
||||||
|
"remove_member": "Mitglied entfernen",
|
||||||
|
"manage_your_team": "Team verwalten",
|
||||||
|
"submit": "Abschicken",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"update": "Aktualisieren",
|
||||||
|
"save": "Speichern",
|
||||||
|
"pending": "Ausstehend",
|
||||||
|
"open_options": "Optionen öffnen",
|
||||||
|
"copy_link": "Link kopieren",
|
||||||
|
"preview": "Vorschau",
|
||||||
|
"link_copied": "Link kopiert!",
|
||||||
|
"title": "Titel",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"quick_video_meeting": "Ein schnelles Video-Meeting.",
|
||||||
|
"scheduling_type": "Termintyp",
|
||||||
|
"preview_team": "Teamvorschau",
|
||||||
|
"collective": "Kollektiv",
|
||||||
|
"collective_description": "Planen Sie Meetings, bei denen alle ausgewählten Teammitglieder verfügbar sind.",
|
||||||
|
"duration": "Dauer",
|
||||||
|
"minutes": "Minuten",
|
||||||
|
"round_robin": "Round Robin",
|
||||||
|
"round_robin_description": "Treffen zwischen mehreren Teammitgliedern durchwechseln.",
|
||||||
|
"url": "URL",
|
||||||
|
"hidden": "Versteckt",
|
||||||
|
"readonly": "Schreibgeschützt",
|
||||||
|
"plan_upgrade": "Sie müssen Ihr Paket upgraden, um mehr als einen aktiven Ereignistyp zu haben.",
|
||||||
|
"plan_upgrade_instructions": "Zum Upgrade, gehen Sie auf <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||||
|
"event_types_page_title": "Ereignistypen",
|
||||||
|
"event_types_page_subtitle": "Erstellen Sie teilbare Ereignisse, die andere Personen buchen können.",
|
||||||
|
"new_event_type_btn": "Neuer Ereignistyp",
|
||||||
|
"new_event_type_heading": "Erstellen Sie Ihren ersten Ereignistyp",
|
||||||
|
"new_event_type_description": "Mit Ereignistypen kann man verfügbare Zeiten im Kalendar freigeben, die andere Personen dann buchen können.",
|
||||||
|
"new_event_title": "Neuen Ereignistyp hinzufügen",
|
||||||
|
"new_event_subtitle": "Erstellen Sie einen Ereignistyp unter Ihrem Namen oder einem Team.",
|
||||||
|
"new_team_event": "Neuen Ereignistyp hinzufügen",
|
||||||
|
"new_event_description": "Erstellen Sie einen neuen Ereignistyp, mit dem Personen Zeiten buchen können.",
|
||||||
|
"event_type_created_successfully": "{{eventTypeTitle}} Ereignistyp erfolgreich erstellt"
|
||||||
|
}
|
|
@ -1,3 +1,149 @@
|
||||||
{
|
{
|
||||||
"new-event-type-btn": "New event type"
|
"uh_oh": "Uh oh!",
|
||||||
|
"no_event_types_have_been_setup": "This user hasn't set up any event types yet.",
|
||||||
|
"edit_logo": "Edit logo",
|
||||||
|
"upload_a_logo": "Upload a logo",
|
||||||
|
"enable": "Enable",
|
||||||
|
"code": "Code",
|
||||||
|
"code_is_incorrect": "Code is incorrect.",
|
||||||
|
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
|
||||||
|
"2fa": "Two-Factor Authentication",
|
||||||
|
"enable_2fa": "Enable two-factor authentication",
|
||||||
|
"disable_2fa": "Disable two-factor authentication",
|
||||||
|
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
|
||||||
|
"error_disabling_2fa": "Error disabling two-factor authentication",
|
||||||
|
"error_enabling_2fa": "Error setting up two-factor authentication",
|
||||||
|
"security": "Security",
|
||||||
|
"manage_account_security": "Manage your account's security.",
|
||||||
|
"password": "Password",
|
||||||
|
"password_updated_successfully": "Password updated successfully",
|
||||||
|
"password_has_been_changed": "Your password has been successfully changed.",
|
||||||
|
"error_changing_password": "Error changing password",
|
||||||
|
"something_went_wrong": "Something went wrong",
|
||||||
|
"please_try_again": "Please try again",
|
||||||
|
"super_secure_new_password": "Your super secure new password",
|
||||||
|
"new_password": "New Password",
|
||||||
|
"your_old_password": "Your old password",
|
||||||
|
"current_password": "Current Password",
|
||||||
|
"change_password": "Change Password",
|
||||||
|
"new_password_matches_old_password": "New password matches your old password. Please choose a different password.",
|
||||||
|
"current_incorrect_password": "Current password is incorrect",
|
||||||
|
"incorrect_password": "Password is incorrect",
|
||||||
|
"1_on_1": "1-on-1",
|
||||||
|
"24_h": "24h",
|
||||||
|
"use_setting": "Use setting",
|
||||||
|
"am_pm": "am/pm",
|
||||||
|
"time_options": "Time options",
|
||||||
|
"january": "January",
|
||||||
|
"february": "February",
|
||||||
|
"march": "March",
|
||||||
|
"april": "April",
|
||||||
|
"may": "May",
|
||||||
|
"june": "June",
|
||||||
|
"july": "July",
|
||||||
|
"august": "August",
|
||||||
|
"september": "September",
|
||||||
|
"october": "October",
|
||||||
|
"november": "November",
|
||||||
|
"december": "December",
|
||||||
|
"monday": "Monday",
|
||||||
|
"tuesday": "Tuesday",
|
||||||
|
"wednesday": "Wednesday",
|
||||||
|
"thursday": "Thursday",
|
||||||
|
"friday": "Friday",
|
||||||
|
"saturday": "Saturday",
|
||||||
|
"sunday": "Sunday",
|
||||||
|
"all_booked_today": "All booked today.",
|
||||||
|
"slots_load_fail": "Could not load the available time slots.",
|
||||||
|
"additional_guests": "+ Additional Guests",
|
||||||
|
"your_name": "Your name",
|
||||||
|
"email_address": "Email address",
|
||||||
|
"location": "Location",
|
||||||
|
"yes": "yes",
|
||||||
|
"no": "no",
|
||||||
|
"additional_notes": "Additional notes",
|
||||||
|
"booking_fail": "Could not book the meeting.",
|
||||||
|
"reschedule_fail": "Could not reschedule the meeting.",
|
||||||
|
"share_additional_notes": "Please share anything that will help prepare for our meeting.",
|
||||||
|
"booking_confirmation": "Confirm your {{eventTypeTitle}} with {{profileName}}",
|
||||||
|
"booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}",
|
||||||
|
"in_person_meeting": "Link or In-person meeting",
|
||||||
|
"phone_call": "Phone call",
|
||||||
|
"phone_number": "Phone Number",
|
||||||
|
"enter_phone_number": "Enter phone number",
|
||||||
|
"reschedule": "Reschedule",
|
||||||
|
"book_a_team_member": "Book a team member instead",
|
||||||
|
"or": "OR",
|
||||||
|
"go_back": "Go back",
|
||||||
|
"email_or_username": "Email or Username",
|
||||||
|
"send_invite_email": "Send an invite email",
|
||||||
|
"role": "Role",
|
||||||
|
"edit_team": "Edit team",
|
||||||
|
"reject": "Reject",
|
||||||
|
"accept": "Accept",
|
||||||
|
"leave": "Leave",
|
||||||
|
"profile": "Profile",
|
||||||
|
"my_team_url": "My team URL",
|
||||||
|
"team_name": "Team name",
|
||||||
|
"your_team_name": "Your team name",
|
||||||
|
"team_updated_successfully": "Team updated successfully",
|
||||||
|
"your_team_updated_successfully": "Your team has been updated successfully.",
|
||||||
|
"about": "About",
|
||||||
|
"team_description": "A few sentences about your team. This will appear on your team's URL page.",
|
||||||
|
"members": "Members",
|
||||||
|
"member": "Member",
|
||||||
|
"owner": "Owner",
|
||||||
|
"new_member": "New Member",
|
||||||
|
"invite": "Invite",
|
||||||
|
"invite_new_member": "Invite a new member",
|
||||||
|
"invite_new_team_member": "Invite someone to your team.",
|
||||||
|
"disable_cal_branding": "Disable Cal.com branding",
|
||||||
|
"disable_cal_branding_description": "Hide all Cal.com branding from your public pages.",
|
||||||
|
"danger_zone": "Danger Zone",
|
||||||
|
"back": "Back",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"continue": "Continue",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"disband_team": "Disband Team",
|
||||||
|
"disband_team_confirmation_message": "Are you sure you want to disband this team? Anyone who you've shared this team link with will no longer be able to book using it.",
|
||||||
|
"remove_member_confirmation_message": "Are you sure you want to remove this member from the team?",
|
||||||
|
"confirm_disband_team": "Yes, disband team",
|
||||||
|
"confirm_remove_member": "Yes, remove member",
|
||||||
|
"remove_member": "Remove member",
|
||||||
|
"manage_your_team": "Manage your team",
|
||||||
|
"submit": "Submit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"update": "Update",
|
||||||
|
"save": "Save",
|
||||||
|
"pending": "Pending",
|
||||||
|
"open_options": "Open options",
|
||||||
|
"copy_link": "Copy link to event",
|
||||||
|
"preview": "Preview",
|
||||||
|
"link_copied": "Link copied!",
|
||||||
|
"title": "Title",
|
||||||
|
"description": "Description",
|
||||||
|
"quick_video_meeting": "A quick video meeting.",
|
||||||
|
"scheduling_type": "Scheduling Type",
|
||||||
|
"preview_team": "Preview team",
|
||||||
|
"collective": "Collective",
|
||||||
|
"collective_description": "Schedule meetings when all selected team members are available.",
|
||||||
|
"duration": "Duration",
|
||||||
|
"minutes": "minutes",
|
||||||
|
"round_robin": "Round Robin",
|
||||||
|
"round_robin_description": "Cycle meetings between multiple team members.",
|
||||||
|
"url": "URL",
|
||||||
|
"hidden": "Hidden",
|
||||||
|
"readonly": "Readonly",
|
||||||
|
"plan_upgrade": "You need to upgrade your plan to have more than one active event type.",
|
||||||
|
"plan_upgrade_instructions": "To upgrade, go to <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||||
|
"event_types_page_title": "Event Types",
|
||||||
|
"event_types_page_subtitle": "Create events to share for people to book on your calendar.",
|
||||||
|
"new_event_type_btn": "New event type",
|
||||||
|
"new_event_type_heading": "Create your first event type",
|
||||||
|
"new_event_type_description": "Event types enable you to share links that show available times on your calendar and allow people to make bookings with you.",
|
||||||
|
"new_event_title": "Add a new event type",
|
||||||
|
"new_event_subtitle": "Create an event type under your name or a team.",
|
||||||
|
"new_team_event": "Add a new team event type",
|
||||||
|
"new_event_description": "Create a new event type for people to book times with.",
|
||||||
|
"event_type_created_successfully": "{{eventTypeTitle}} event type created successfully"
|
||||||
}
|
}
|
||||||
|
|
147
public/static/locales/es/common.json
Normal file
147
public/static/locales/es/common.json
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
{
|
||||||
|
"edit_logo": "Cambiar la marca",
|
||||||
|
"upload_a_logo": "Subir una marca",
|
||||||
|
"enable": "Habilitar",
|
||||||
|
"code": "Código",
|
||||||
|
"code_is_incorrect": "El código es incorrecto.",
|
||||||
|
"add_an_extra_layer_of_security": "Agregue una capa adicional de seguridad a su cuenta en caso de que le roben su contraseña.",
|
||||||
|
"2fa": "Autorización de dos factores",
|
||||||
|
"enable_2fa": "Habilitar la autenticación de dos factores",
|
||||||
|
"disable_2fa": "Deshabilitar la autenticación de dos factores",
|
||||||
|
"disable_2fa_recommendation": "Si necesita deshabilitar 2FA, le recomendamos que lo vuelva a habilitar lo antes posible.",
|
||||||
|
"error_disabling_2fa": "Error al deshabilitar la autenticación de dos factores",
|
||||||
|
"error_enabling_2fa": "Error al configurar la autenticación de dos factores",
|
||||||
|
"security": "Seguridad",
|
||||||
|
"manage_account_security": "Administra la seguridad de tu cuenta.",
|
||||||
|
"password": "Contraseña",
|
||||||
|
"password_updated_successfully": "Contraseña actualizada con éxito",
|
||||||
|
"password_has_been_changed": "Su contraseña se ha cambiado correctamente.",
|
||||||
|
"error_changing_password": "Error al cambiar la contraseña",
|
||||||
|
"something_went_wrong": "Algo ha fallado",
|
||||||
|
"please_try_again": "Por favor, inténtalo de nuevo",
|
||||||
|
"super_secure_new_password": "Su nueva contraseña super segura",
|
||||||
|
"new_password": "Nueva contraseña",
|
||||||
|
"your_old_password": "Su contraseña antigua",
|
||||||
|
"current_password": "Contraseña actual",
|
||||||
|
"change_password": "Cambiar Contraseña",
|
||||||
|
"new_password_matches_old_password": "La nueva contraseña coincide con su contraseña antigua. Por favor, elija una contraseña diferente.",
|
||||||
|
"current_incorrect_password": "La contraseña actual es incorrecta",
|
||||||
|
"incorrect_password": "La contraseña es incorrecta",
|
||||||
|
"1_on_1": "1 a 1",
|
||||||
|
"24_h": "24hs",
|
||||||
|
"use_setting": "Usar configuración",
|
||||||
|
"am_pm": "am/pm",
|
||||||
|
"time_options": "Opciones de tiempo",
|
||||||
|
"january": "Enero",
|
||||||
|
"february": "Febrero",
|
||||||
|
"march": "Marzo",
|
||||||
|
"april": "Abril",
|
||||||
|
"may": "Mayo",
|
||||||
|
"june": "Junio",
|
||||||
|
"july": "Julio",
|
||||||
|
"august": "Agosto",
|
||||||
|
"september": "Septiembre",
|
||||||
|
"october": "Octubre",
|
||||||
|
"november": "Noviembre",
|
||||||
|
"december": "Diciembre",
|
||||||
|
"monday": "Lunes",
|
||||||
|
"tuesday": "Martes",
|
||||||
|
"wednesday": "Miércoles",
|
||||||
|
"thursday": "Jueves",
|
||||||
|
"friday": "Viernes",
|
||||||
|
"saturday": "Sábado",
|
||||||
|
"sunday": "Domingo",
|
||||||
|
"all_booked_today": "Todo reservado hoy.",
|
||||||
|
"slots_load_fail": "No se pudo cargar el intervalo de tiempo disponible.",
|
||||||
|
"additional_guests": "+ Invitados adicionales",
|
||||||
|
"your_name": "Tu nombre",
|
||||||
|
"email_address": "Correo electrónico",
|
||||||
|
"location": "Ubicación",
|
||||||
|
"yes": "sí",
|
||||||
|
"no": "no",
|
||||||
|
"additional_notes": "Notas adicionales",
|
||||||
|
"booking_fail": "No se pudo reservar la reunión.",
|
||||||
|
"reschedule_fail": "No se pudo cambiar la reunión.",
|
||||||
|
"share_additional_notes": "Por favor comparta cualquier cosa que nos ayude preparar para esta reunión.",
|
||||||
|
"booking_confirmation": "Confirma tu {{eventTypeTitle}} con {{profileName}}",
|
||||||
|
"booking_reschedule_confirmation": "Cambia tu {{eventTypeTitle}} con {{profileName}}",
|
||||||
|
"in_person_meeting": "Reunión en línea o en persona",
|
||||||
|
"phone_call": "Llamada telefónica",
|
||||||
|
"phone_number": "Número telefónico",
|
||||||
|
"enter_phone_number": "Entra un número de teléfono",
|
||||||
|
"reschedule": "Cambiar",
|
||||||
|
"book_a_team_member": "Reservar un miembro del equipo en su lugar",
|
||||||
|
"or": "O",
|
||||||
|
"go_back": "Volver",
|
||||||
|
"email_or_username": "Correo electrónico o nombre de usuario",
|
||||||
|
"send_invite_email": "Enviar una invitación electrónica",
|
||||||
|
"role": "Título",
|
||||||
|
"edit_team": "Editar equipo",
|
||||||
|
"reject": "Rechazar",
|
||||||
|
"accept": "Aceptar",
|
||||||
|
"leave": "Salir",
|
||||||
|
"profile": "Perfil",
|
||||||
|
"my_team_url": "URL de mi equipo",
|
||||||
|
"team_name": "Nombre del equipo",
|
||||||
|
"your_team_name": "Nombre de tu equipo",
|
||||||
|
"team_updated_successfully": "Equipo actualizado correctamente",
|
||||||
|
"your_team_updated_successfully": "Tu equipo se ha actualizado correctamente.",
|
||||||
|
"about": "Acerca de",
|
||||||
|
"team_description": "Algunas frases sobre tu equipo. Esto aparecerá en la página de tu equipo.",
|
||||||
|
"members": "Miembros",
|
||||||
|
"member": "Miembro",
|
||||||
|
"owner": "Propietario",
|
||||||
|
"new_member": "Nuevo miembro",
|
||||||
|
"invite": "Invitar",
|
||||||
|
"invite_new_member": "Invita a un nuevo miembro",
|
||||||
|
"invite_new_team_member": "Invita a alguien a tu equipo.",
|
||||||
|
"disable_cal_branding": "Desactivar marca de Cal.com",
|
||||||
|
"disable_cal_branding_description": "Ocultar todas las marcas de Cal.com de sus páginas públicas.",
|
||||||
|
"danger_zone": "Zona peligrosa",
|
||||||
|
"back": "Atrás",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"continue": "Continuar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"disband_team": "Disolver Equipo",
|
||||||
|
"disband_team_confirmation_message": "¿Estás seguro de que quieres disolver este equipo? Cualquiera con quien has compartido este enlace de equipo ya no podrá reservar usando el mismo.",
|
||||||
|
"remove_member_confirmation_message": "¿Estás seguro de que quieres eliminar este miembro del equipo?",
|
||||||
|
"confirm_disband_team": "Sí, disolver equipo",
|
||||||
|
"confirm_remove_member": "Sí, eliminar miembro",
|
||||||
|
"remove_member": "Eliminar miembro",
|
||||||
|
"manage_your_team": "Administra tu equipo",
|
||||||
|
"submit": "Enviar",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"update": "Actualizar",
|
||||||
|
"save": "Guardar",
|
||||||
|
"pending": "Pendiente",
|
||||||
|
"open_options": "Abrir opciones",
|
||||||
|
"copy_link": "Copiar enlace al evento",
|
||||||
|
"preview": "Vista previa",
|
||||||
|
"link_copied": "¡Enlace copiado!",
|
||||||
|
"title": "Título",
|
||||||
|
"description": "Descripción",
|
||||||
|
"quick_video_meeting": "Una reunión de vídeo rápida.",
|
||||||
|
"scheduling_type": "Tipo de programación",
|
||||||
|
"preview_team": "Vista previa del equipo",
|
||||||
|
"collective": "Colectivo",
|
||||||
|
"collective_description": "Programe reuniones cuando todos los miembros del equipo seleccionados estén disponibles.",
|
||||||
|
"duration": "Duración",
|
||||||
|
"minutes": "minutos",
|
||||||
|
"round_robin": "Petición firmada por turnos",
|
||||||
|
"round_robin_description": "Ciclo de reuniones entre varios miembros del equipo.",
|
||||||
|
"url": "URL",
|
||||||
|
"hidden": "Oculto",
|
||||||
|
"readonly": "Sólo lectura",
|
||||||
|
"plan_upgrade": "Necesitas actualizar tu plan para tener más de un tipo de evento activo.",
|
||||||
|
"plan_upgrade_instructions": "Para actualizar, dirígete a <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||||
|
"event_types_page_title": "Tipos de Evento",
|
||||||
|
"event_types_page_subtitle": "Crea eventos para que la gente que invites reserve en tu calendario.",
|
||||||
|
"new_event_type_btn": "Nuevo tipo de evento",
|
||||||
|
"new_event_type_heading": "Crea tu primer tipo de evento",
|
||||||
|
"new_event_type_description": "Los tipos de eventos te permiten compartir enlaces que muestran las horas disponibles en tu calendario y permitir que la gente haga reservas contigo.",
|
||||||
|
"new_event_title": "Agregar un nuevo tipo de evento",
|
||||||
|
"new_event_subtitle": "Crea un tipo de evento bajo tu nombre o equipo.",
|
||||||
|
"new_team_event": "Agregar un nuevo tipo de evento de equipo",
|
||||||
|
"new_event_description": "Crea un nuevo tipo de evento con el que la gente pueda hacer reservaciónes.",
|
||||||
|
"event_type_created_successfully": "{{eventTypeTitle}} tipo de evento creado con éxito"
|
||||||
|
}
|
1
public/static/locales/fr/common.json
Normal file
1
public/static/locales/fr/common.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
1
public/static/locales/it/common.json
Normal file
1
public/static/locales/it/common.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
1
public/static/locales/nl/common.json
Normal file
1
public/static/locales/nl/common.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
147
public/static/locales/pt/common.json
Normal file
147
public/static/locales/pt/common.json
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
{
|
||||||
|
"edit_logo": "Editar Logo",
|
||||||
|
"upload_a_logo": "Carregar Logo",
|
||||||
|
"enable": "Ativar",
|
||||||
|
"code": "Código",
|
||||||
|
"code_is_incorrect": "O código está incorreto.",
|
||||||
|
"add_an_extra_layer_of_security": "Adicione uma camada extra de segurança à sua conta, caso a sua palavra-passe seja roubada.",
|
||||||
|
"2fa": "Autenticação com dois fatores",
|
||||||
|
"enable_2fa": "Ativar autenticação de dois fatores",
|
||||||
|
"disable_2fa": "Desativar autenticação de dois fatores",
|
||||||
|
"disable_2fa_recommendation": "Se precisar desativar o 2FA, recomendamos reativá-lo o mais rápido possível.",
|
||||||
|
"error_disabling_2fa": "Erro ao desativar autenticação de dois fatores",
|
||||||
|
"error_enabling_2fa": "Erro ao configurar a autenticação de dois fatores",
|
||||||
|
"security": "Segurança",
|
||||||
|
"manage_account_security": "Gerir a segurança da sua conta.",
|
||||||
|
"password": "Palavra-Passe",
|
||||||
|
"password_updated_successfully": "Palavra-Passe atualizada com sucesso",
|
||||||
|
"password_has_been_changed": "A sua palavra-passe foi alterada com sucesso.",
|
||||||
|
"error_changing_password": "Erro ao alterar a palavra-passe",
|
||||||
|
"something_went_wrong": "Ocorreu um erro",
|
||||||
|
"please_try_again": "Por favor, tente novamente",
|
||||||
|
"super_secure_new_password": "A sua nova palavra-passe é super segura",
|
||||||
|
"new_password": "Nova Palavra-Passe",
|
||||||
|
"your_old_password": "A sua palavra-passe antiga",
|
||||||
|
"current_password": "Palavra-Passe Atual",
|
||||||
|
"change_password": "Alterar Palavra-Passe",
|
||||||
|
"new_password_matches_old_password": "Nova palavra-passe é igual à palavra-passe antiga. Por favor, escolha uma palavra-passe diferente.",
|
||||||
|
"current_incorrect_password": "Palavra-Passe atual está incorreta",
|
||||||
|
"incorrect_password": "Palavra-Passe incorreta",
|
||||||
|
"1_on_1": "1 para 1",
|
||||||
|
"24_h": "24h",
|
||||||
|
"use_setting": "Usar Configuração",
|
||||||
|
"am_pm": "am/pm",
|
||||||
|
"time_options": "Opções de Hora",
|
||||||
|
"january": "Janeiro",
|
||||||
|
"february": "Fevereiro",
|
||||||
|
"march": "Março",
|
||||||
|
"april": "Abril",
|
||||||
|
"may": "Maio",
|
||||||
|
"june": "Junho",
|
||||||
|
"july": "Julho",
|
||||||
|
"august": "Agosto",
|
||||||
|
"september": "Setembro",
|
||||||
|
"october": "Outubro",
|
||||||
|
"november": "Novembro",
|
||||||
|
"december": "Dezembro",
|
||||||
|
"monday": "Segunda-Feira",
|
||||||
|
"tuesday": "Terça-Feira",
|
||||||
|
"wednesday": "Quarta-Feira",
|
||||||
|
"thursday": "Quinta-Feira",
|
||||||
|
"friday": "Sexta-Feira",
|
||||||
|
"saturday": "Sábado",
|
||||||
|
"sunday": "Domingo",
|
||||||
|
"all_booked_today": "Todo o Dia Reservado.",
|
||||||
|
"slots_load_fail": "Não foi possível carregar os horários disponíveis.",
|
||||||
|
"additional_guests": "+ Convidados Adicionais",
|
||||||
|
"your_name": "Seu Nome",
|
||||||
|
"email_address": "Endereço de E-mail",
|
||||||
|
"location": "Localização",
|
||||||
|
"yes": "Sim",
|
||||||
|
"no": "Não",
|
||||||
|
"additional_notes": "Notas Adicionais",
|
||||||
|
"booking_fail": "Não foi possível agendar a reunião.",
|
||||||
|
"reschedule_fail": "Não foi possível re-agendar a reunião.",
|
||||||
|
"share_additional_notes": "Por favor, partilhe qualquer informação para preparar a nossa reunião.",
|
||||||
|
"booking_confirmation": "Confirme o seu {{eventTypeTitle}} com {{profileName}}",
|
||||||
|
"booking_reschedule_confirmation": "Reagende o seu {{eventTypeTitle}} com {{profileName}}",
|
||||||
|
"in_person_meeting": "Link ou Reunião Presencial",
|
||||||
|
"phone_call": "Chamada Telefónica",
|
||||||
|
"phone_number": "Número de Telefone",
|
||||||
|
"enter_phone_number": "Inserir Número do Telefone",
|
||||||
|
"reschedule": "Reagendar",
|
||||||
|
"book_a_team_member": "Reserve um Membro da sua Equipa no seu lugar",
|
||||||
|
"or": "Ou",
|
||||||
|
"go_back": "Voltar atrás",
|
||||||
|
"email_or_username": "E-mail ou Nome de Utilizador",
|
||||||
|
"send_invite_email": "Enviar um e-mail de convite",
|
||||||
|
"role": "Função",
|
||||||
|
"edit_team": "Editar Equipa",
|
||||||
|
"reject": "Rejeitar",
|
||||||
|
"accept": "Aceitar",
|
||||||
|
"leave": "Ausente",
|
||||||
|
"profile": "Perfil",
|
||||||
|
"my_team_url": "URL da Minha Equipa",
|
||||||
|
"team_name": "Nome da Equipa",
|
||||||
|
"your_team_name": "Nome da sua equipa",
|
||||||
|
"team_updated_successfully": "Equipa atualizada com sucesso",
|
||||||
|
"your_team_updated_successfully": "A sua equipa foi atualizada com sucesso.",
|
||||||
|
"about": "Sobre",
|
||||||
|
"team_description": "Algumas frases sobre a sua equipa. Isso aparecerá na sua equipa'URL .",
|
||||||
|
"members": "Membros",
|
||||||
|
"member": "Membro",
|
||||||
|
"owner": "Proprietário",
|
||||||
|
"new_member": "Novo Membro",
|
||||||
|
"invite": "Convidar",
|
||||||
|
"invite_new_member": "Convidar um Novo Membro",
|
||||||
|
"invite_new_team_member": "Convide alguém para a sua equipa.",
|
||||||
|
"disable_cal_branding": "Desativar a marca Cal.com",
|
||||||
|
"disable_cal_branding_description": "Ocultar todas as marcas de Cal.com das suas páginas públicas.",
|
||||||
|
"danger_zone": "Zona de Perigo",
|
||||||
|
"back": "Anterior",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"continue": "Continuar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"disband_team": "Dissolver Equipa",
|
||||||
|
"disband_team_confirmation_message": "Tem a certeza de que deseja dissolver esta equipa? Qualquer pessoa com quem've partilhou este link de equipa não conseguirá fazer uma reserva usando-o.",
|
||||||
|
"remove_member_confirmation_message": "Tem a certeza de que deseja remover este membro da equipa?",
|
||||||
|
"confirm_disband_team": "Sim, dissolver equipa",
|
||||||
|
"confirm_remove_member": "Sim, remover membro",
|
||||||
|
"remove_member": "Remover Membro",
|
||||||
|
"manage_your_team": "Gerir a sua equipa",
|
||||||
|
"submit": "Enviar",
|
||||||
|
"delete": "Apagar",
|
||||||
|
"update": "Atualizar",
|
||||||
|
"save": "Guardar",
|
||||||
|
"pending": "Pendente",
|
||||||
|
"open_options": "Abrir Opções",
|
||||||
|
"copy_link": "Copiar link do evento",
|
||||||
|
"preview": "Pré-Visualizar",
|
||||||
|
"link_copied": "Link copiado!",
|
||||||
|
"title": "Título",
|
||||||
|
"description": "Descrição",
|
||||||
|
"quick_video_meeting": "Uma breve reunião em vídeo.",
|
||||||
|
"scheduling_type": "Tipo do Agendamento",
|
||||||
|
"preview_team": "Pré-visualizar Equipa",
|
||||||
|
"collective": "Coletivo",
|
||||||
|
"collective_description": "Agende reuniões quando todos os membros selecionados da equipa estiverem disponíveis.",
|
||||||
|
"duration": "Duração",
|
||||||
|
"minutes": "Minutos",
|
||||||
|
"round_robin": "Round Robin",
|
||||||
|
"round_robin_description": "Reuniões de ciclo entre vários membros da equipa.",
|
||||||
|
"url": "URL",
|
||||||
|
"hidden": "Oculto",
|
||||||
|
"readonly": "Somente Leitura",
|
||||||
|
"plan_upgrade": "Precisa atualizar o seu plano para ter mais de um tipo de evento ativo.",
|
||||||
|
"plan_upgrade_instructions": "Para fazer a atualização, aceda <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||||
|
"event_types_page_title": "Tipo de Eventos",
|
||||||
|
"event_types_page_subtitle": "Crie eventos para partilhar, para que as pessoas façam reservas no seu calendário.",
|
||||||
|
"new_event_type_btn": "Novo tipo de evento",
|
||||||
|
"new_event_type_heading": "Crie o seu primeiro tipo de evento",
|
||||||
|
"new_event_type_description": "Os tipos de evento permitem partilhar links que mostram os horários disponíveis na sua agenda e permitem que as pessoas façam reservas consigo.",
|
||||||
|
"new_event_title": "Adicionar um novo tipo de evento",
|
||||||
|
"new_event_subtitle": "Crie um tipo de evento sob o seu nome ou equipa.",
|
||||||
|
"new_team_event": "Adicionar um novo tipo de evento de equipa",
|
||||||
|
"new_event_description": "Crie um novo tipo de evento para as pessoas reservarem uma hora.",
|
||||||
|
"event_type_created_successfully": "{{eventTypeTitle}} tipo de evento criado com sucesso"
|
||||||
|
}
|
|
@ -1,3 +1 @@
|
||||||
{
|
{}
|
||||||
"new-event-type-btn": "Nou tip de eveniment"
|
|
||||||
}
|
|
||||||
|
|
147
public/static/locales/ru/common.json
Normal file
147
public/static/locales/ru/common.json
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
{
|
||||||
|
"edit_logo": "Изменить логотип",
|
||||||
|
"upload_a_logo": "Загрузить логотип",
|
||||||
|
"enable": "Включить",
|
||||||
|
"code": "Код",
|
||||||
|
"code_is_incorrect": "Неверный код.",
|
||||||
|
"add_an_extra_layer_of_security": "Добавьте дополнительный уровень безопасности в свою учетную запись на случай кражи пароля.",
|
||||||
|
"2fa": "Двухфакторная авторизация",
|
||||||
|
"enable_2fa": "Включить двухфакторную авторизацию",
|
||||||
|
"disable_2fa": "Отключить двухфакторную авторизацию",
|
||||||
|
"disable_2fa_recommendation": "Если вам нужно отключить двухфакторную авторизацию, мы рекомендуем включить её как можно скорее.",
|
||||||
|
"error_disabling_2fa": "Ошибка отключения двухфакторной авторизации",
|
||||||
|
"error_enabling_2fa": "Ошибка настройки двухфакторной авторизации",
|
||||||
|
"security": "Безопасность",
|
||||||
|
"manage_account_security": "Управление безопасностью вашего аккаунта.",
|
||||||
|
"password": "Пароль",
|
||||||
|
"password_updated_successfully": "Пароль успешно обновлен",
|
||||||
|
"password_has_been_changed": "Ваш пароль был успешно изменен.",
|
||||||
|
"error_changing_password": "Ошибка при изменении пароля",
|
||||||
|
"something_went_wrong": "Что-то пошло не так",
|
||||||
|
"please_try_again": "Пожалуйста, попробуйте еще раз",
|
||||||
|
"super_secure_new_password": "Ваш супербезопасный новый пароль",
|
||||||
|
"new_password": "Новый пароль",
|
||||||
|
"your_old_password": "Ваш старый пароль",
|
||||||
|
"current_password": "Текущий пароль",
|
||||||
|
"change_password": "Изменить пароль",
|
||||||
|
"new_password_matches_old_password": "Новый пароль совпадает с вашим старым паролем. Пожалуйста, выберите другой пароль.",
|
||||||
|
"current_incorrect_password": "Неверный текущий пароль",
|
||||||
|
"incorrect_password": "Неверный пароль",
|
||||||
|
"1_on_1": "1-на-1",
|
||||||
|
"24_h": "24 часа",
|
||||||
|
"use_setting": "Использовать настройки",
|
||||||
|
"am_pm": "am/pm",
|
||||||
|
"time_options": "Настройки времени",
|
||||||
|
"january": "Январь",
|
||||||
|
"february": "Февраль",
|
||||||
|
"march": "Март",
|
||||||
|
"april": "Апрель",
|
||||||
|
"may": "Май",
|
||||||
|
"june": "Июнь",
|
||||||
|
"july": "Июль",
|
||||||
|
"august": "Август",
|
||||||
|
"september": "Сентябрь",
|
||||||
|
"october": "Октябрь",
|
||||||
|
"november": "Ноябрь",
|
||||||
|
"december": "Декабрь",
|
||||||
|
"monday": "Понедельник",
|
||||||
|
"tuesday": "Вторник",
|
||||||
|
"wednesday": "Среда",
|
||||||
|
"thursday": "Четверг",
|
||||||
|
"friday": "Пятница",
|
||||||
|
"saturday": "Суббота",
|
||||||
|
"sunday": "Воскресенье",
|
||||||
|
"all_booked_today": "Сегодня всё забронировано.",
|
||||||
|
"slots_load_fail": "Не удалось загрузить доступные временные интервалы.",
|
||||||
|
"additional_guests": "+ Дополнительные гости",
|
||||||
|
"your_name": "Ваше имя",
|
||||||
|
"email_address": "Адрес электронной почты",
|
||||||
|
"location": "Местоположение",
|
||||||
|
"yes": "да",
|
||||||
|
"no": "нет",
|
||||||
|
"additional_notes": "Дополнительная информация",
|
||||||
|
"booking_fail": "Не удалось забронировать встречу.",
|
||||||
|
"reschedule_fail": "Не удалось перенести встречу.",
|
||||||
|
"share_additional_notes": "Дополнительная информация, которая может помочь подготовиться к нашей встрече.",
|
||||||
|
"booking_confirmation": "Подтвердите вашу встречу «{{eventTypeTitle}}» с {{profileName}}",
|
||||||
|
"booking_reschedule_confirmation": "Перенесите вашу встречу «{{eventTypeTitle}}» с {{profileName}}",
|
||||||
|
"in_person_meeting": "Ссылка или личная встреча",
|
||||||
|
"phone_call": "Телефонный звонок",
|
||||||
|
"phone_number": "Номер телефона",
|
||||||
|
"enter_phone_number": "Введите номер телефона",
|
||||||
|
"reschedule": "Перенести",
|
||||||
|
"book_a_team_member": "Забронировать встречу с одним из членов команды",
|
||||||
|
"or": "ИЛИ",
|
||||||
|
"go_back": "Вернуться",
|
||||||
|
"email_or_username": "Email или имя пользователя",
|
||||||
|
"send_invite_email": "Отправить приглашение по электронной почте",
|
||||||
|
"role": "Роль",
|
||||||
|
"edit_team": "Редактировать команду",
|
||||||
|
"reject": "Отклонить",
|
||||||
|
"accept": "Принять",
|
||||||
|
"leave": "Покинуть",
|
||||||
|
"profile": "Профиль",
|
||||||
|
"my_team_url": "URL-адрес моей команды",
|
||||||
|
"team_name": "Название команды",
|
||||||
|
"your_team_name": "Название вашей команды",
|
||||||
|
"team_updated_successfully": "Команда успешно обновлена",
|
||||||
|
"your_team_updated_successfully": "Ваша команда успешно обновлена.",
|
||||||
|
"about": "О нас",
|
||||||
|
"team_description": "Несколько предложений о вашей команде. Это появится на странице вашей команды.",
|
||||||
|
"members": "Участники",
|
||||||
|
"member": "Участник",
|
||||||
|
"owner": "Владелец",
|
||||||
|
"new_member": "Новый участник",
|
||||||
|
"invite": "Пригласить",
|
||||||
|
"invite_new_member": "Пригласить нового участника",
|
||||||
|
"invite_new_team_member": "Пригласите кого-нибудь в вашу команду.",
|
||||||
|
"disable_cal_branding": "Отключить брендинг Cal.com",
|
||||||
|
"disable_cal_branding_description": "Скрыть весь брендинг Cal.com с ваших публичных страниц.",
|
||||||
|
"danger_zone": "Опасная зона",
|
||||||
|
"back": "Назад",
|
||||||
|
"cancel": "Отмена",
|
||||||
|
"continue": "Продолжить",
|
||||||
|
"confirm": "Подтвердить",
|
||||||
|
"disband_team": "Распустить команду",
|
||||||
|
"disband_team_confirmation_message": "Вы уверены, что хотите распустить эту команду? Любой, с кем вы поделились ссылкой на эту команду, больше не сможет забронировать её.",
|
||||||
|
"remove_member_confirmation_message": "Вы уверены, что хотите удалить этого участника из команды?",
|
||||||
|
"confirm_disband_team": "Да, распустить команду",
|
||||||
|
"confirm_remove_member": "Да, удалить участника",
|
||||||
|
"remove_member": "Удалить участника",
|
||||||
|
"manage_your_team": "Управление вашей командой",
|
||||||
|
"submit": "Отправить",
|
||||||
|
"delete": "Удалить",
|
||||||
|
"update": "Обновить",
|
||||||
|
"save": "Сохранить",
|
||||||
|
"pending": "В ожидании",
|
||||||
|
"open_options": "Открыть настройки",
|
||||||
|
"copy_link": "Скопировать ссылку на событие",
|
||||||
|
"preview": "Предпросмотр",
|
||||||
|
"link_copied": "Ссылка скопирована!",
|
||||||
|
"title": "Заголовок",
|
||||||
|
"description": "Описание",
|
||||||
|
"quick_video_meeting": "Быстрая видео-встреча.",
|
||||||
|
"scheduling_type": "Тип расписания",
|
||||||
|
"preview_team": "Предпросмотр команды",
|
||||||
|
"collective": "Коллективная встреча",
|
||||||
|
"collective_description": "Расписание встреч, когда доступны все выбранные члены команды.",
|
||||||
|
"duration": "Продолжительность",
|
||||||
|
"minutes": "мин.",
|
||||||
|
"round_robin": "По кругу",
|
||||||
|
"round_robin_description": "Цикл встреч между несколькими членами команды.",
|
||||||
|
"url": "URL",
|
||||||
|
"hidden": "Скрытый",
|
||||||
|
"readonly": "Только для чтения",
|
||||||
|
"plan_upgrade": "Необходимо обновить тарифный план, чтобы иметь более одного активного типа события.",
|
||||||
|
"plan_upgrade_instructions": "Для повышения перейдите на <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||||
|
"event_types_page_title": "Типы мероприятий",
|
||||||
|
"event_types_page_subtitle": "Создайте мероприятие, чтобы поделиться с людьми для бронирования в вашем календаре.",
|
||||||
|
"new_event_type_btn": "Новый тип мероприятия",
|
||||||
|
"new_event_type_heading": "Создайте свой первый тип мероприятия",
|
||||||
|
"new_event_type_description": "Типы мероприятий позволяют делиться ссылками, которые показывают время в вашем календаре и позволяют людям бронировать встречи с вами.",
|
||||||
|
"new_event_title": "Добавить новый тип события",
|
||||||
|
"new_event_subtitle": "Создайте тип события для себя или команды.",
|
||||||
|
"new_team_event": "Добавить новый тип события команды",
|
||||||
|
"new_event_description": "Создайте новый тип мероприятия, с помощью которого люди смогут забронировать время.",
|
||||||
|
"event_type_created_successfully": "{{eventTypeTitle}} тип мероприятия успешно создан"
|
||||||
|
}
|
|
@ -4,7 +4,6 @@
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import { createRouter } from "../createRouter";
|
import { createRouter } from "../createRouter";
|
||||||
import { bookingRouter } from "./booking";
|
|
||||||
import { viewerRouter } from "./viewer";
|
import { viewerRouter } from "./viewer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +23,6 @@ export const appRouter = createRouter()
|
||||||
* @link https://trpc.io/docs/error-formatting
|
* @link https://trpc.io/docs/error-formatting
|
||||||
*/
|
*/
|
||||||
// .formatError(({ shape, error }) => { })
|
// .formatError(({ shape, error }) => { })
|
||||||
.merge("viewer.", viewerRouter)
|
.merge("viewer.", viewerRouter);
|
||||||
.merge("booking.", bookingRouter);
|
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createRouter } from "../createRouter";
|
|
||||||
|
|
||||||
export const bookingRouter = createRouter().query("userEventTypes", {
|
|
||||||
input: z.object({
|
|
||||||
username: z.string().min(1),
|
|
||||||
}),
|
|
||||||
async resolve({ input, ctx }) {
|
|
||||||
const { prisma } = ctx;
|
|
||||||
const { username } = input;
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
username: username.toLowerCase(),
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
bio: true,
|
|
||||||
avatar: true,
|
|
||||||
theme: true,
|
|
||||||
plan: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventTypesWithHidden = await prisma.eventType.findMany({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
teamId: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
users: {
|
|
||||||
some: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
title: true,
|
|
||||||
length: true,
|
|
||||||
description: true,
|
|
||||||
hidden: true,
|
|
||||||
schedulingType: true,
|
|
||||||
price: true,
|
|
||||||
currency: true,
|
|
||||||
},
|
|
||||||
take: user.plan === "FREE" ? 1 : undefined,
|
|
||||||
});
|
|
||||||
const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden);
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
eventTypes,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
Loading…
Reference in a new issue