Feat/subdomain logochanges (#8264)
* logo map * Archimed logo * usememo * Subdomain logo logic * or our logo * Update packages/features/orgs/SubdomainProvider.tsx * Provider comments * Move DB logic to db * Fix a11y on alt tag * Added example for dynamic endpoint * Move to API approach * Implement Icon logo on subdomain level --------- Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
2ac5ef10ba
commit
d202b536b5
|
@ -1,7 +1,6 @@
|
|||
import classNames from "classnames";
|
||||
|
||||
import { APP_NAME, LOGO } from "@calcom/lib/constants";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { HeadSeo, Logo } from "@calcom/ui";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
|
||||
|
@ -18,10 +17,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||
return (
|
||||
<div className="flex min-h-screen flex-col justify-center bg-[#f3f4f6] py-12 sm:px-6 lg:px-8">
|
||||
<HeadSeo title={props.title} description={props.description} />
|
||||
{props.showLogo && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="mb-auto h-4" src={LOGO} alt={`${APP_NAME} Logo`} />
|
||||
)}
|
||||
{props.showLogo && <Logo small inline={false} className="mx-auto mb-auto" />}
|
||||
|
||||
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
||||
{props.heading && <h2 className="font-cal text-emphasis text-center text-3xl">{props.heading}</h2>}
|
||||
|
|
|
@ -155,6 +155,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||
<SessionProvider session={session || undefined}>
|
||||
<CustomI18nextProvider {...props}>
|
||||
<TooltipProvider>
|
||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||
<CalcomThemeProvider
|
||||
nonce={props.pageProps.nonce}
|
||||
isThemeSupported={props.Component.isThemeSupported}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import fs from "fs";
|
||||
import mime from "mime-types";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { LOGO, LOGO_ICON } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: [`[api/logo]`] });
|
||||
|
||||
function removePort(url: string) {
|
||||
return url.replace(/:\d+$/, "");
|
||||
}
|
||||
|
||||
function extractSubdomainAndDomain(url: string): [string, string] | null {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
const hostParts = parsedUrl.href.split(".");
|
||||
if (hostParts.length > 2) {
|
||||
const subdomain = hostParts.slice(0, hostParts.length - 2).join(".");
|
||||
const domain = hostParts.slice(hostParts.length - 2).join(".");
|
||||
return [subdomain, removePort(domain)];
|
||||
} else if (hostParts.length === 2) {
|
||||
const subdomain = "";
|
||||
const domain = hostParts.join(".");
|
||||
return [subdomain, removePort(domain)];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const logoApiSchema = z.object({
|
||||
icon: z.coerce.boolean().optional(),
|
||||
});
|
||||
|
||||
function handleDefaultLogo(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
parsedQuery?: z.infer<typeof logoApiSchema>
|
||||
) {
|
||||
let fileNameParts = LOGO.split(".");
|
||||
|
||||
if (parsedQuery?.icon) {
|
||||
fileNameParts = LOGO_ICON.split(".");
|
||||
}
|
||||
|
||||
const { [fileNameParts.length - 1]: fileExtension } = fileNameParts;
|
||||
const STATIC_PATH = path.join(process.cwd(), "public" + LOGO);
|
||||
const imageBuffer = fs.readFileSync(STATIC_PATH);
|
||||
const mimeType = mime.lookup(fileExtension);
|
||||
if (mimeType) res.setHeader("Content-Type", mimeType);
|
||||
res.setHeader("Cache-Control", "s-maxage=86400");
|
||||
res.send(imageBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* This API endpoint is used to serve the logo associated with a team if no logo is found we serve our default logo
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { query } = req;
|
||||
const parsedQuery = logoApiSchema.parse(query);
|
||||
|
||||
const hostname = req?.headers["host"];
|
||||
if (!hostname) throw new Error("No hostname");
|
||||
const domains = extractSubdomainAndDomain(hostname);
|
||||
if (!domains) throw new Error("No domains");
|
||||
const [subdomain, domain] = domains;
|
||||
// Only supported on cal.com and cal.dev
|
||||
if (!["cal.com", "cal.dev"].includes(domain)) return handleDefaultLogo(req, res, parsedQuery);
|
||||
// Skip if no subdomain
|
||||
if (!subdomain) throw new Error("No subdomain");
|
||||
// Omit system subdomains
|
||||
if (["console", "app", "www"].includes(subdomain)) return handleDefaultLogo(req, res, parsedQuery);
|
||||
|
||||
const { default: prisma } = await import("@calcom/prisma");
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
slug: subdomain,
|
||||
},
|
||||
select: {
|
||||
appLogo: true,
|
||||
appIconLogo: true,
|
||||
},
|
||||
});
|
||||
|
||||
const filteredLogo = parsedQuery?.icon ? team?.appIconLogo : team?.appLogo;
|
||||
|
||||
if (!filteredLogo) throw new Error("No team appLogo");
|
||||
|
||||
const response = await fetch(filteredLogo);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
res.setHeader("Content-Type", response.headers.get("content-type") as string);
|
||||
res.setHeader("Cache-Control", "s-maxage=86400");
|
||||
res.send(buffer);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) log.debug(e.message);
|
||||
handleDefaultLogo(req, res);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Team" ADD COLUMN "appLogo" TEXT;
|
||||
ALTER TABLE "Team" ADD COLUMN "appIconLogo" TEXT;
|
|
@ -235,6 +235,8 @@ model Team {
|
|||
/// @zod.min(1)
|
||||
slug String? @unique
|
||||
logo String?
|
||||
appLogo String?
|
||||
appIconLogo String?
|
||||
bio String?
|
||||
hideBranding Boolean @default(false)
|
||||
hideBookATeamMember Boolean @default(false)
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
import classNames from "@calcom/lib/classNames";
|
||||
import { LOGO_ICON, LOGO } from "@calcom/lib/constants";
|
||||
|
||||
export default function Logo({ small, icon }: { small?: boolean; icon?: boolean }) {
|
||||
export default function Logo({
|
||||
small,
|
||||
icon,
|
||||
inline = true,
|
||||
className,
|
||||
}: {
|
||||
small?: boolean;
|
||||
icon?: boolean;
|
||||
inline?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<h3 className="logo inline ">
|
||||
<h3 className={classNames("logo", inline && "inline", className)}>
|
||||
<strong>
|
||||
{icon ? (
|
||||
<img className="mx-auto w-9 dark:invert" alt="Cal" title="Cal" src={LOGO_ICON} />
|
||||
<img className="mx-auto w-9 dark:invert" alt="Cal" title="Cal" src="/api/logo?icon=1" />
|
||||
) : (
|
||||
<img
|
||||
className={classNames(small ? "h-4 w-auto" : "h-5 w-auto", "dark:invert")}
|
||||
alt="Cal"
|
||||
title="Cal"
|
||||
src={LOGO}
|
||||
src="/api/logo"
|
||||
/>
|
||||
)}
|
||||
</strong>
|
||||
|
|
Loading…
Reference in New Issue
Block a user