From 6232c635f6431e4fab3cca9b24c78a181a1b848d Mon Sep 17 00:00:00 2001 From: Jeffrey Sica Date: Thu, 12 Mar 2026 18:32:46 -0500 Subject: [PATCH 1/2] Add kiosk view for conference QR code landing pages Adds a full-page kiosk view activated via ?item=&view=kiosk query parameters, designed for conference booth QR code scanning. The view displays project logo, name, maturity badges, GitHub stats, and categorized action cards (Get Started, Contribute, Jobs, Community, Social, Code) with touch-friendly layout and automatic dark mode support via prefers-color-scheme. Signed-off-by: Jeffrey Sica --- ui/webapp/src/data.ts | 1 + ui/webapp/src/layout/index.tsx | 82 +++-- .../src/layout/kiosk/KioskView.module.css | 234 ++++++++++++ ui/webapp/src/layout/kiosk/index.tsx | 347 ++++++++++++++++++ 4 files changed, 634 insertions(+), 30 deletions(-) create mode 100644 ui/webapp/src/layout/kiosk/KioskView.module.css create mode 100644 ui/webapp/src/layout/kiosk/index.tsx diff --git a/ui/webapp/src/data.ts b/ui/webapp/src/data.ts index cc7b5e22..88a114e8 100644 --- a/ui/webapp/src/data.ts +++ b/ui/webapp/src/data.ts @@ -36,6 +36,7 @@ export const VIEW_MODE_PARAM = 'view-mode'; export const GROUP_PARAM = 'group'; export const MODAL_PARAM = 'modal'; export const ITEM_PARAM = 'item'; +export const VIEW_PARAM = 'view'; export const CATEGORY_PARAM = 'category'; export const SUBCATEGORY_PARAM = 'subcategory'; export const PAGE_PARAM = 'page'; diff --git a/ui/webapp/src/layout/index.tsx b/ui/webapp/src/layout/index.tsx index 81d01896..d568da60 100644 --- a/ui/webapp/src/layout/index.tsx +++ b/ui/webapp/src/layout/index.tsx @@ -1,14 +1,17 @@ +import { useSearchParams } from '@solidjs/router'; import isEmpty from 'lodash/isEmpty'; -import { createSignal, JSXElement, onMount } from 'solid-js'; +import { createSignal, JSXElement, onMount, Show } from 'solid-js'; +import { ITEM_PARAM, VIEW_PARAM } from '../data'; import ItemModal from './common/itemModal'; import ZoomModal from './common/zoomModal'; +import KioskView from './kiosk'; import styles from './Layout.module.css'; import Header from './navigation/Header'; import MobileHeader from './navigation/MobileHeader'; import { ActiveItemProvider } from './stores/activeItem'; import { FinancesDataProvider } from './stores/financesData'; -import { FullDataProvider } from './stores/fullData'; +import { FullDataProvider, useFullDataReady } from './stores/fullData'; import { GridWidthProvider } from './stores/gridWidth'; import { GroupActiveProvider } from './stores/groupActive'; import { GuideFileProvider } from './stores/guideFile'; @@ -23,9 +26,23 @@ interface Props { children?: JSXElement; } +// Inner wrapper so useFullDataReady() is called inside FullDataProvider +const KioskWrapper = (props: { itemId: string }) => { + const fullDataReady = useFullDataReady(); + return ; +}; + const Layout = (props: Props) => { + const [searchParams] = useSearchParams(); const [statsVisible, setStatsVisible] = createSignal(true); + const isKioskView = () => searchParams[VIEW_PARAM] === 'kiosk' && !!searchParams[ITEM_PARAM]; + const kioskItemId = (): string => { + const val = searchParams[ITEM_PARAM]; + if (Array.isArray(val)) return val[0] || ''; + return val || ''; + }; + onMount(() => { // Check if statsDS is empty, if so, hide the stats link if (isEmpty(window.statsDS)) { @@ -35,34 +52,39 @@ const Layout = (props: Props) => { return ( - - - - - - - - - - -
- -
-
{props.children}
-
- - - -
-
-
-
-
-
-
-
-
-
+ } + > + + + + + + + + + + +
+ +
+
{props.children}
+
+ + + +
+
+
+
+
+
+
+
+
+
+
); }; diff --git a/ui/webapp/src/layout/kiosk/KioskView.module.css b/ui/webapp/src/layout/kiosk/KioskView.module.css new file mode 100644 index 00000000..63155d61 --- /dev/null +++ b/ui/webapp/src/layout/kiosk/KioskView.module.css @@ -0,0 +1,234 @@ +.kioskContainer { + min-height: 100vh; + background-color: #f5f7fa; + padding: 2rem; + overflow-y: auto; +} + +.header { + text-align: center; + margin-bottom: 2rem; +} + +.logoWrapper { + width: 120px; + height: 120px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: center; +} + +.logo { + max-width: 100%; + max-height: 100%; + height: auto; +} + +.projectName { + font-size: 2rem; + font-weight: bold; +} + +.description { + font-size: 1.1rem; + color: #6c757d; + max-width: 700px; + margin: 0 auto; + line-height: 1.6; +} + +.statsBar { + display: flex; + flex-direction: row; + justify-content: center; + gap: 2rem; + background-color: #fff; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + padding: 1rem 2rem; + margin-bottom: 2rem; +} + +.statItem { + text-align: center; + min-width: 80px; +} + +.statValue { + font-size: 1.5rem; + font-weight: bold; + color: #212529; +} + +.statLabel { + font-size: 0.8rem; + text-transform: uppercase; + color: #6c757d; +} + +.cardsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + max-width: 1200px; + margin: 0 auto; +} + +.actionCard { + background-color: #fff; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + transition: box-shadow 0.2s ease; +} + +.actionCard:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.cardTitle { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.cardLink { + display: flex; + flex-direction: row; + align-items: center; + padding: 0.6rem; + border-radius: 8px; + transition: background-color 0.15s ease; + text-decoration: none; + color: inherit; +} + +.cardLink:hover { + background-color: #f0f4f8; +} + +.cardLinkIcon { + width: 20px; + height: 20px; + margin-right: 0.75rem; + flex-shrink: 0; +} + +.badges { + display: flex; + justify-content: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.notFound { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + text-align: center; +} + +.notFoundTitle { + font-size: 1.8rem; + font-weight: bold; + color: #212529; + margin-bottom: 0.75rem; +} + +.notFoundMessage { + font-size: 1.1rem; + color: #6c757d; + margin-bottom: 2rem; +} + +.poweredBy { + text-align: center; + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid #dee2e6; + color: #6c757d; + font-size: 0.9rem; +} + +@media (prefers-color-scheme: dark) { + .kioskContainer { + background-color: #1a1b1e; + color: #e1e3e6; + } + + .projectName { + color: #e1e3e6; + } + + .description { + color: #9ca3af; + } + + .statsBar { + background-color: #25262b; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } + + .statValue { + color: #e1e3e6; + } + + .statLabel { + color: #9ca3af; + } + + .actionCard { + background-color: #25262b; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } + + .actionCard:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); + } + + .cardLink { + color: #e1e3e6; + } + + .cardLink:hover { + background-color: #2c2d32; + } + + .cardTitle { + color: #e1e3e6; + } + + .notFoundTitle { + color: #e1e3e6; + } + + .notFoundMessage { + color: #9ca3af; + } + + .poweredBy { + border-top-color: #373a40; + color: #9ca3af; + } + + .poweredBy a { + color: #7cb3f0; + } +} + +@media (max-width: 768px) { + .kioskContainer { + padding: 1rem; + } + + .cardsGrid { + grid-template-columns: 1fr; + } + + .cardLink { + padding: 0.8rem; + } +} diff --git a/ui/webapp/src/layout/kiosk/index.tsx b/ui/webapp/src/layout/kiosk/index.tsx new file mode 100644 index 00000000..557dd708 --- /dev/null +++ b/ui/webapp/src/layout/kiosk/index.tsx @@ -0,0 +1,347 @@ +import { + ExternalLink, + FoundationBadge, + getItemDescription, + Image, + Loading, + MaturityBadge, + prettifyNumber, + SVGIcon, + SVGIconKind, +} from 'common'; +import isUndefined from 'lodash/isUndefined'; +import { createEffect, createSignal, For, Match, Show, Switch } from 'solid-js'; + +import { FOUNDATION } from '../../data'; +import { GithubRepository, Item, Repository } from '../../types'; +import itemsDataGetter from '../../utils/itemsDataGetter'; +import styles from './KioskView.module.css'; + +interface Props { + itemId: string; + fullDataReady: boolean; +} + +interface LinkEntry { + label: string; + url: string; + icon: SVGIconKind; +} + +const KioskView = (props: Props) => { + // undefined = still loading, null = not found, Item = found + const [itemInfo, setItemInfo] = createSignal(undefined); + const [primaryRepo, setPrimaryRepo] = createSignal(undefined); + + createEffect(() => { + if (props.itemId && props.fullDataReady) { + const item = itemsDataGetter.getItemById(props.itemId); + setItemInfo(item || null); + + if (!isUndefined(item) && !isUndefined(item.repositories)) { + const primary = item.repositories.find((r: Repository) => r.primary); + setPrimaryRepo(primary); + } + } + }); + + const githubData = (): GithubRepository | undefined => { + const repo = primaryRepo(); + if (!isUndefined(repo)) { + return repo.github_data; + } + return undefined; + }; + + const goodFirstIssuesUrl = (): string | undefined => { + const repo = primaryRepo(); + if (!isUndefined(repo)) { + return `${repo.url}/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22`; + } + return undefined; + }; + + const gitjobsUrl = (): string | undefined => { + const item = itemInfo(); + if (!isUndefined(item) && item !== null) { + return `https://gitjobs.dev/jobs?query=${encodeURIComponent(item.name)}`; + } + return undefined; + }; + + const description = (): string => { + const item = itemInfo(); + if (!isUndefined(item) && item !== null) { + return getItemDescription(item); + } + return ''; + }; + + const getLinkedInUrl = (): string | undefined => { + const item = itemInfo(); + if (!isUndefined(item) && item !== null) { + if (!isUndefined(item.linkedin_url)) { + return item.linkedin_url; + } + if (!isUndefined(item.crunchbase_data) && !isUndefined(item.crunchbase_data.linkedin_url)) { + return item.crunchbase_data.linkedin_url; + } + } + return undefined; + }; + + const communityLinks = (): LinkEntry[] => { + const item = itemInfo(); + if (isUndefined(item) || item === null) return []; + + const links: LinkEntry[] = []; + if (!isUndefined(item.slack_url)) { + links.push({ label: 'Slack', url: item.slack_url, icon: SVGIconKind.Slack }); + } + if (!isUndefined(item.discord_url)) { + links.push({ label: 'Discord', url: item.discord_url, icon: SVGIconKind.Discord }); + } + if (!isUndefined(item.mailing_list_url)) { + links.push({ label: 'Mailing List', url: item.mailing_list_url, icon: SVGIconKind.MailingList }); + } + if (!isUndefined(item.github_discussions_url)) { + links.push({ label: 'GitHub Discussions', url: item.github_discussions_url, icon: SVGIconKind.Discussions }); + } + if (!isUndefined(item.stack_overflow_url)) { + links.push({ label: 'Stack Overflow', url: item.stack_overflow_url, icon: SVGIconKind.StackOverflow }); + } + return links; + }; + + const socialLinks = (): LinkEntry[] => { + const item = itemInfo(); + if (isUndefined(item) || item === null) return []; + + const links: LinkEntry[] = []; + if (!isUndefined(item.twitter_url)) { + links.push({ label: 'Twitter / X', url: item.twitter_url, icon: SVGIconKind.Twitter }); + } + if (!isUndefined(item.youtube_url)) { + links.push({ label: 'YouTube', url: item.youtube_url, icon: SVGIconKind.Youtube }); + } + const linkedIn = getLinkedInUrl(); + if (!isUndefined(linkedIn)) { + links.push({ label: 'LinkedIn', url: linkedIn, icon: SVGIconKind.LinkedIn }); + } + if (!isUndefined(item.bluesky_url)) { + links.push({ label: 'Bluesky', url: item.bluesky_url, icon: SVGIconKind.Bluesky }); + } + if (!isUndefined(item.blog_url)) { + links.push({ label: 'Blog', url: item.blog_url, icon: SVGIconKind.Blog }); + } + return links; + }; + + const codeLinks = (): LinkEntry[] => { + const item = itemInfo(); + if (isUndefined(item) || item === null) return []; + + const links: LinkEntry[] = []; + const repo = primaryRepo(); + if (!isUndefined(repo)) { + links.push({ label: 'Repository', url: repo.url, icon: SVGIconKind.GitHub }); + } + if (!isUndefined(item.docker_url)) { + links.push({ label: 'Docker', url: item.docker_url, icon: SVGIconKind.Docker }); + } + if (!isUndefined(item.package_manager_url)) { + links.push({ label: 'Package Manager', url: item.package_manager_url, icon: SVGIconKind.PackageManager }); + } + return links; + }; + + return ( + + {/* Loading state: data not yet fetched */} + +
+ +
+
+ + {/* Not found state: data loaded but item doesn't exist */} + +
+
+
Project not found
+
+ The requested project could not be found in the landscape. +
+
+ Browse the CNCF Landscape +
+
+
+
+ + {/* Found state: render kiosk content */} + + {(item) => { + const i = item(); + return ( +
+ {/* Header */} +
+
+ +
+
{i.name}
+
+ + + + + + +
+ +
{description()}
+
+
+ + {/* Stats Bar */} + + {(gh) => ( +
+ +
+
{prettifyNumber(gh().stars)}
+
Stars
+
+
+ +
+
{prettifyNumber(gh().contributors.count)}
+
Contributors
+
+
+ +
+
{gh().license}
+
License
+
+
+ +
+
+ {new Date(gh().latest_release!.ts).toLocaleDateString()} +
+
Latest Release
+
+
+
+ )} +
+ + {/* Action Cards Grid */} +
+ {/* Get Started */} + +
+
Get Started
+ + + + Website + + + + + + Documentation + + +
+
+ + {/* Contribute */} + + {(url) => ( +
+
Contribute
+ + + Good First Issues + +
+ )} +
+ + {/* Jobs */} + + {(url) => ( +
+
Jobs
+ + + Find jobs on GitJobs + +
+ )} +
+ + {/* Community */} + 0}> +
+
Community
+ + {(link) => ( + + + {link.label} + + )} + +
+
+ + {/* Social */} + 0}> +
+
Social
+ + {(link) => ( + + + {link.label} + + )} + +
+
+ + {/* Code */} + 0}> +
+
Code
+ + {(link) => ( + + + {link.label} + + )} + +
+
+
+ + {/* Footer */} +
+ Powered by{' '} + CNCF Landscape +
+
+ ); + }} +
+
+ ); +}; + +export default KioskView; From 73417df66227ae654c5ed6aa855f2fd752f7bd8d Mon Sep 17 00:00:00 2001 From: Jeffrey Sica Date: Fri, 13 Mar 2026 09:03:33 -0500 Subject: [PATCH 2/2] fix: apply prettier formatting to kiosk view files Signed-off-by: Jeffrey Sica --- ui/webapp/src/layout/index.tsx | 5 +---- ui/webapp/src/layout/kiosk/index.tsx | 11 +++-------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/ui/webapp/src/layout/index.tsx b/ui/webapp/src/layout/index.tsx index d568da60..cfd4757c 100644 --- a/ui/webapp/src/layout/index.tsx +++ b/ui/webapp/src/layout/index.tsx @@ -52,10 +52,7 @@ const Layout = (props: Props) => { return ( - } - > + }> diff --git a/ui/webapp/src/layout/kiosk/index.tsx b/ui/webapp/src/layout/kiosk/index.tsx index 557dd708..325b208f 100644 --- a/ui/webapp/src/layout/kiosk/index.tsx +++ b/ui/webapp/src/layout/kiosk/index.tsx @@ -169,9 +169,7 @@ const KioskView = (props: Props) => {
Project not found
-
- The requested project could not be found in the landscape. -
+
The requested project could not be found in the landscape.
Browse the CNCF Landscape
@@ -228,9 +226,7 @@ const KioskView = (props: Props) => {
-
- {new Date(gh().latest_release!.ts).toLocaleDateString()} -
+
{new Date(gh().latest_release!.ts).toLocaleDateString()}
Latest Release
@@ -333,8 +329,7 @@ const KioskView = (props: Props) => { {/* Footer */}
- Powered by{' '} - CNCF Landscape + Powered by CNCF Landscape
);