From ee9b3eccafe9dd78e291588e1b6a1a4ff13a5077 Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Tue, 30 May 2023 17:31:49 +0100 Subject: [PATCH] feat: Use team logos for various meta icons (#8955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use team logos for various meta icons * Automatically resize team icons for different uses * Fix api/logo error when unable to find a team * Avoid loading image-optimizer in api/logo when not needed --------- Co-authored-by: Omar López Co-authored-by: alannnc Co-authored-by: Alex van Andel Co-authored-by: Bailey Pumfleet --- apps/web/pages/_document.tsx | 6 +- apps/web/pages/api/logo.ts | 117 ++++++++++++++++++++++++--- apps/web/public/browserconfig.xml | 2 +- apps/web/public/site.webmanifest | 4 +- packages/lib/constants.ts | 6 ++ packages/ui/components/logo/Logo.tsx | 2 +- 6 files changed, 119 insertions(+), 18 deletions(-) diff --git a/apps/web/pages/_document.tsx b/apps/web/pages/_document.tsx index dcc53b486c..0c9179f148 100644 --- a/apps/web/pages/_document.tsx +++ b/apps/web/pages/_document.tsx @@ -40,9 +40,9 @@ class MyDocument extends Document { return ( - - - + + + diff --git a/apps/web/pages/api/logo.ts b/apps/web/pages/api/logo.ts index 00af5dba8d..72dac999d7 100644 --- a/apps/web/pages/api/logo.ts +++ b/apps/web/pages/api/logo.ts @@ -1,7 +1,18 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; -import { IS_SELF_HOSTED, LOGO, LOGO_ICON, WEBAPP_URL } from "@calcom/lib/constants"; +import { + ANDROID_CHROME_ICON_192, + ANDROID_CHROME_ICON_256, + APPLE_TOUCH_ICON, + FAVICON_16, + FAVICON_32, + IS_SELF_HOSTED, + LOGO, + LOGO_ICON, + MSTILE_ICON, + WEBAPP_URL, +} from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; const log = logger.getChildLogger({ prefix: ["[api/logo]"] }); @@ -20,11 +31,79 @@ function extractSubdomainAndDomain(hostname: string) { } const logoApiSchema = z.object({ - icon: z.coerce.boolean().optional(), + type: z.coerce.string().optional(), }); const SYSTEM_SUBDOMAINS = ["console", "app", "www"]; +type LogoType = + | "logo" + | "icon" + | "favicon-16" + | "favicon-32" + | "apple-touch-icon" + | "mstile" + | "android-chrome-192" + | "android-chrome-256"; + +type LogoTypeDefinition = { + fallback: string; + w?: number; + h?: number; + source: "appLogo" | "appIconLogo"; +}; + +const logoDefinitions: Record = { + logo: { + fallback: `${WEBAPP_URL}${LOGO}`, + source: "appLogo", + }, + icon: { + fallback: `${WEBAPP_URL}${LOGO_ICON}`, + source: "appIconLogo", + }, + "favicon-16": { + fallback: `${WEBAPP_URL}${FAVICON_16}`, + w: 16, + h: 16, + source: "appIconLogo", + }, + "favicon-32": { + fallback: `${WEBAPP_URL}${FAVICON_32}`, + w: 32, + h: 32, + source: "appIconLogo", + }, + "apple-touch-icon": { + fallback: `${WEBAPP_URL}${APPLE_TOUCH_ICON}`, + w: 180, + h: 180, + source: "appLogo", + }, + mstile: { + fallback: `${WEBAPP_URL}${MSTILE_ICON}`, + w: 150, + h: 150, + source: "appLogo", + }, + "android-chrome-192": { + fallback: `${WEBAPP_URL}${ANDROID_CHROME_ICON_192}`, + w: 192, + h: 192, + source: "appLogo", + }, + "android-chrome-256": { + fallback: `${WEBAPP_URL}${ANDROID_CHROME_ICON_256}`, + w: 256, + h: 256, + source: "appLogo", + }, +}; + +function isValidLogoType(type: string): type is LogoType { + return type in logoDefinitions; +} + async function getTeamLogos(subdomain: string) { try { if ( @@ -39,7 +118,7 @@ async function getTeamLogos(subdomain: string) { } // load from DB const { default: prisma } = await import("@calcom/prisma"); - const team = await prisma.team.findUniqueOrThrow({ + const team = await prisma.team.findUnique({ where: { slug: subdomain, }, @@ -48,16 +127,16 @@ async function getTeamLogos(subdomain: string) { appIconLogo: true, }, }); - // try to use team logos, otherwise default to LOGO/LOGO_ICON regardless + return { - appLogo: team.appLogo || `${WEBAPP_URL}${LOGO}`, - appIconLogo: team.appIconLogo || `${WEBAPP_URL}${LOGO_ICON}`, + appLogo: team?.appLogo, + appIconLogo: team?.appIconLogo, }; } catch (error) { if (error instanceof Error) log.debug(error.message); return { - appLogo: `${WEBAPP_URL}${LOGO}`, - appIconLogo: `${WEBAPP_URL}${LOGO_ICON}`, + appLogo: undefined, + appIconLogo: undefined, }; } } @@ -75,14 +154,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!domains) throw new Error("No domains"); const [subdomain] = domains; - const { appLogo, appIconLogo } = await getTeamLogos(subdomain); + const teamLogos = await getTeamLogos(subdomain); - const filteredLogo = parsedQuery?.icon ? appIconLogo : appLogo; + // Resolve all icon types to team logos, falling back to Cal.com defaults. + const type: LogoType = parsedQuery?.type && isValidLogoType(parsedQuery.type) ? parsedQuery.type : "logo"; + const logoDefinition = logoDefinitions[type]; + const filteredLogo = teamLogos[logoDefinition.source] ?? logoDefinition.fallback; try { const response = await fetch(filteredLogo); const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); + let buffer = Buffer.from(arrayBuffer); + + // If we need to resize the team logos (via Next.js' built-in image processing) + if (teamLogos[logoDefinition.source] && logoDefinition.w) { + const { detectContentType, optimizeImage } = await import("next/dist/server/image-optimizer"); + buffer = await optimizeImage({ + buffer, + contentType: detectContentType(buffer) ?? "image/jpeg", + quality: 100, + width: logoDefinition.w, + height: logoDefinition.h, // optional + }); + } + res.setHeader("Content-Type", response.headers.get("content-type") as string); res.setHeader("Cache-Control", "s-maxage=86400"); res.send(buffer); diff --git a/apps/web/public/browserconfig.xml b/apps/web/public/browserconfig.xml index 748e8459c5..5cd1b5f5ab 100644 --- a/apps/web/public/browserconfig.xml +++ b/apps/web/public/browserconfig.xml @@ -2,7 +2,7 @@ - + #ff0000 diff --git a/apps/web/public/site.webmanifest b/apps/web/public/site.webmanifest index 15a8e42abb..04adc4cba9 100644 --- a/apps/web/public/site.webmanifest +++ b/apps/web/public/site.webmanifest @@ -3,12 +3,12 @@ "short_name": "Cal.com", "icons": [ { - "src": "/android-chrome-192x192.png", + "src": "/api/logo?type=android-chrome-192", "sizes": "192x192", "type": "image/png" }, { - "src": "/android-chrome-256x256.png", + "src": "/api/logo?type=android-chrome-256", "sizes": "256x256", "type": "image/png" } diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 19b47fb762..7b5997144f 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -40,6 +40,12 @@ export const HOSTED_CAL_FEATURES = process.env.NEXT_PUBLIC_HOSTED_CAL_FEATURES | export const NEXT_PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL || `https://${process.env.VERCEL_URL}`; export const LOGO = "/calcom-logo-white-word.svg"; export const LOGO_ICON = "/cal-com-icon-white.svg"; +export const FAVICON_16 = "/favicon-16x16.png"; +export const FAVICON_32 = "/favicon-32x32.png"; +export const APPLE_TOUCH_ICON = "/apple-touch-icon.png"; +export const MSTILE_ICON = "/mstile-150x150.png"; +export const ANDROID_CHROME_ICON_192 = "/android-chrome-192x192.png"; +export const ANDROID_CHROME_ICON_256 = "/android-chrome-256x256.png"; export const ROADMAP = "https://cal.com/roadmap"; export const DESKTOP_APP_LINK = "https://cal.com/download"; export const JOIN_SLACK = "https://cal.com/slack"; diff --git a/packages/ui/components/logo/Logo.tsx b/packages/ui/components/logo/Logo.tsx index 31e7e224cd..872e9ffb9c 100644 --- a/packages/ui/components/logo/Logo.tsx +++ b/packages/ui/components/logo/Logo.tsx @@ -15,7 +15,7 @@ export default function Logo({

{icon ? ( - Cal + Cal ) : (