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:
Peer Richelsen
2023-03-12 23:31:55 +00:00
committed by GitHub
co-authored by zomars
parent 71a3374930
commit 02d70c63a5
8 changed files with 80 additions and 41 deletions
+3 -3
View File
@@ -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} />
+39 -10
View File
@@ -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;
+21 -19
View File
@@ -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 -1
View File
@@ -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">