empty screen for unpublished teams (#7206)
* empty screen for unpublished teams * i18n for unpublished team, removed contact button, added avatar to EmptyScreen * Merge branch 'main' into 7202-cal-1114-teamnull-better-404-screen # Conflicts: # apps/web/public/static/locales/en/common.json * Adds backend logic * Type fixes --------- Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import type { TeamPageProps } from "pages/team/[slug]";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
type TeamType = TeamPageProps["team"];
|
||||
type TeamType = NonNullable<TeamWithMembers>;
|
||||
type MembersType = TeamType["members"];
|
||||
type MemberType = MembersType[number];
|
||||
|
||||
@@ -57,7 +57,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
||||
);
|
||||
};
|
||||
|
||||
const Team = ({ team }: Omit<TeamPageProps, "trpcState">) => {
|
||||
const Team = ({ team }: { team: TeamType }) => {
|
||||
return (
|
||||
<div>
|
||||
<Members members={team.members} teamName={team.name} />
|
||||
|
||||
@@ -13,7 +13,8 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { Avatar, Button, HeadSeo, AvatarGroup } from "@calcom/ui";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
||||
import { FiArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
@@ -24,13 +25,15 @@ import Team from "@components/team/screens/Team";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
function TeamPage({ team }: TeamPageProps) {
|
||||
function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
useTheme(team.theme);
|
||||
const showMembers = useToggleQuery("members");
|
||||
const { t } = useLocale();
|
||||
const isEmbed = useIsEmbed();
|
||||
const telemetry = useTelemetry();
|
||||
const router = useRouter();
|
||||
const teamName = team.name || "Nameless Team";
|
||||
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.event(
|
||||
@@ -39,6 +42,18 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
);
|
||||
}, [telemetry, router.asPath]);
|
||||
|
||||
if (isUnpublished) {
|
||||
return (
|
||||
<div className="m-8 flex items-center justify-center">
|
||||
<EmptyScreen
|
||||
avatar={<Avatar alt={teamName} imageSrc={getPlaceholderAvatar(team.logo, team.name)} size="lg" />}
|
||||
headline={t("team_is_unpublished", { team: teamName })}
|
||||
description={t("team_is_unpublished_description")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const EventTypes = () => (
|
||||
<ul className="dark:border-darkgray-300 rounded-md border border-gray-200">
|
||||
{team.eventTypes.map((type, index) => (
|
||||
@@ -78,12 +93,8 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
</ul>
|
||||
);
|
||||
|
||||
const teamName = team.name || "Nameless Team";
|
||||
|
||||
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<HeadSeo
|
||||
title={teamName}
|
||||
description={teamName}
|
||||
@@ -124,7 +135,6 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="mt-8 flex justify-center text-center dark:text-white">
|
||||
<Button
|
||||
color="minimal"
|
||||
@@ -140,7 +150,7 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,6 +158,25 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const ssr = await ssrInit(context);
|
||||
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
||||
|
||||
const unpublishedTeam = await prisma.team.findFirst({
|
||||
where: {
|
||||
metadata: {
|
||||
path: ["requestedSlug"],
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (unpublishedTeam) {
|
||||
return {
|
||||
props: {
|
||||
isUnpublished: true,
|
||||
team: unpublishedTeam,
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
const team = await getTeamWithMembers(undefined, slug);
|
||||
|
||||
if (!team) return { notFound: true } as { notFound: true };
|
||||
@@ -165,7 +194,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
team,
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
};
|
||||
|
||||
export default TeamPage;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"rejection_confirmation": "Reject the booking",
|
||||
"manage_this_event": "Manage this event",
|
||||
"invite_team_member": "Invite team member",
|
||||
"invite_team_notifcation_badge":"Inv.",
|
||||
"invite_team_notifcation_badge": "Inv.",
|
||||
"your_event_has_been_scheduled": "Your event has been scheduled",
|
||||
"your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled",
|
||||
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
|
||||
@@ -1328,7 +1328,7 @@
|
||||
"password_reset_leading": "If you don't receive an email soon, check that the email address you entered is correct, check your spam folder or reach out to support if the issue persists.",
|
||||
"password_updated": "Password updated!",
|
||||
"pending_payment": "Pending payment",
|
||||
"pending_invites":"Pending Invites",
|
||||
"pending_invites": "Pending Invites",
|
||||
"confirmation_page_rainbow": "Token gate your event with tokens or NFTs on Ethereum, Polygon, and more.",
|
||||
"not_on_cal": "Not on {{appName}}",
|
||||
"no_calendar_installed": "No calendar installed",
|
||||
@@ -1478,10 +1478,10 @@
|
||||
"add_an_option": "Add an option",
|
||||
"radio": "Radio",
|
||||
"google_meet_warning": "In order to use Google Meet you must set your destination calendar to a Google Calendar",
|
||||
"individual":"Individual",
|
||||
"all_bookings_filter_label":"All Bookings",
|
||||
"all_users_filter_label":"All Users",
|
||||
"your_bookings_filter_label":"Your Bookings",
|
||||
"individual": "Individual",
|
||||
"all_bookings_filter_label": "All Bookings",
|
||||
"all_users_filter_label": "All Users",
|
||||
"your_bookings_filter_label": "Your Bookings",
|
||||
"meeting_url_variable": "Meeting url",
|
||||
"meeting_url_info": "The event meeting conference url",
|
||||
"date_overrides": "Date overrides",
|
||||
@@ -1576,16 +1576,16 @@
|
||||
"delete_sso_configuration_confirmation": "Yes, delete {{connectionType}} configuration",
|
||||
"delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.",
|
||||
"organizer_timezone": "Organizer timezone",
|
||||
"email_no_user_cta":"Create your account",
|
||||
"email_user_cta":"View Invitation",
|
||||
"email_no_user_invite_heading":"You’ve been invited to join a team on {{appName}}",
|
||||
"email_no_user_invite_subheading":"{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
|
||||
"email_no_user_invite_steps_intro":"We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your team in no time.",
|
||||
"email_no_user_step_one":"Choose your username",
|
||||
"email_no_user_step_two":"Connect your calendar account",
|
||||
"email_no_user_step_three":"Set your Availability",
|
||||
"email_no_user_step_four":"Join {{teamName}}",
|
||||
"email_no_user_signoff":"Happy Scheduling from the {{appName}} team",
|
||||
"email_no_user_cta": "Create your account",
|
||||
"email_user_cta": "View Invitation",
|
||||
"email_no_user_invite_heading": "You’ve been invited to join a team on {{appName}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
|
||||
"email_no_user_invite_steps_intro": "We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your team in no time.",
|
||||
"email_no_user_step_one": "Choose your username",
|
||||
"email_no_user_step_two": "Connect your calendar account",
|
||||
"email_no_user_step_three": "Set your Availability",
|
||||
"email_no_user_step_four": "Join {{teamName}}",
|
||||
"email_no_user_signoff": "Happy Scheduling from the {{appName}} team",
|
||||
"impersonation_user_tip": "You are about to impersonate a user, which means you can make changes on their behalf. Please be careful.",
|
||||
"available_variables": "Available variables",
|
||||
"scheduler": "{Scheduler}",
|
||||
@@ -1598,9 +1598,9 @@
|
||||
"collective_or_roundrobin": "Collective or round-robin",
|
||||
"book_your_team_members": "Book your team members together with collective events or cycle through to get the right person with round-robin.",
|
||||
"create_event_on": "Create event on",
|
||||
"default_app_link_title":"Set a default app link",
|
||||
"default_app_link_description":"Setting a default app link allows all newly created event types to use the app link you set.",
|
||||
"change_default_conferencing_app":"Set as default",
|
||||
"default_app_link_title": "Set a default app link",
|
||||
"default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.",
|
||||
"change_default_conferencing_app": "Set as default",
|
||||
"under_maintenance": "Down for maintenance",
|
||||
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
|
||||
"event_type_seats": "{{numberOfSeats}} seats",
|
||||
@@ -1617,6 +1617,8 @@
|
||||
"form_builder_system_field_cant_delete": "This system field can't be removed.",
|
||||
"form_builder_system_field_cant_toggle": "This system field can't be toggled.",
|
||||
"get_started_zapier_templates": "Get started with Zapier templates",
|
||||
"team_is_unpublished": "{{team}} is unpublished",
|
||||
"team_is_unpublished_description": "This team link is currently not available. Please contact the team owner or ask them publish it.",
|
||||
"team_member": "Team member",
|
||||
"a_routing_form": "A Routing Form",
|
||||
"form_description_placeholder": "Form Description",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { AppsStatus as AppsStatusType, CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import {
|
||||
BaseEmailHtml,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
|
||||
import type { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
|
||||
import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { WorkflowActions } from "@prisma/client";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ import { Button } from "../../components/button";
|
||||
|
||||
export function EmptyScreen({
|
||||
Icon,
|
||||
avatar,
|
||||
headline,
|
||||
description,
|
||||
buttonText,
|
||||
buttonOnClick,
|
||||
buttonRaw,
|
||||
}: {
|
||||
Icon: SVGComponent | IconType;
|
||||
headline: string;
|
||||
Icon?: SVGComponent | IconType;
|
||||
avatar?: React.ReactElement;
|
||||
headline: string | React.ReactElement;
|
||||
description: string | React.ReactElement;
|
||||
buttonText?: string;
|
||||
buttonOnClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
@@ -26,9 +28,14 @@ export function EmptyScreen({
|
||||
<div
|
||||
data-testid="empty-screen"
|
||||
className="min-h-80 flex w-full flex-col items-center justify-center rounded-md border border-dashed p-7 lg:p-20">
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-200 dark:bg-white">
|
||||
<Icon className="inline-block h-10 w-10 stroke-[1.3px] dark:bg-gray-900 dark:text-gray-600" />
|
||||
</div>
|
||||
{!avatar ? null : (
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full">{avatar}</div>
|
||||
)}
|
||||
{!Icon ? null : (
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-200 dark:bg-white">
|
||||
<Icon className="inline-block h-10 w-10 stroke-[1.3px] dark:bg-gray-900 dark:text-gray-600" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex max-w-[420px] flex-col items-center">
|
||||
<h2 className="text-semibold font-cal mt-6 text-center text-xl dark:text-gray-300">{headline}</h2>
|
||||
<div className="mt-3 mb-8 text-center text-sm font-normal leading-6 text-gray-700 dark:text-gray-300">
|
||||
|
||||
Reference in New Issue
Block a user