From f7535651f66ec72f0904c06df44fcc2a2761a0c0 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:27:13 +0000 Subject: [PATCH] refactor(info): improve UI consistency and polish --- .../src/views/project-info/ProjectInfo.scss | 91 ++++++++++++++----- .../src/views/project-info/ProjectInfo.tsx | 89 +++++++++++------- 2 files changed, 127 insertions(+), 53 deletions(-) diff --git a/apps/client/src/views/project-info/ProjectInfo.scss b/apps/client/src/views/project-info/ProjectInfo.scss index 6c3fcc491..772757521 100644 --- a/apps/client/src/views/project-info/ProjectInfo.scss +++ b/apps/client/src/views/project-info/ProjectInfo.scss @@ -1,5 +1,7 @@ @use '@/theme/viewerDefs' as *; +$content-width: min(100%, 1100px); + .project { margin: 0; box-sizing: border-box; /* reset */ @@ -16,56 +18,100 @@ display: flex; flex-direction: column; + /* =================== HEADER ===================*/ + + .project-header { + width: $content-width; + margin-inline: auto; + + display: flex; + align-items: center; + gap: clamp(12px, 2vw, 24px); + + padding-bottom: $view-element-gap; + border-bottom: 1px solid $white-10; + } + .logo { max-width: min(200px, 30vw); } + .title { + font-size: $header-font-size; + font-weight: 600; + line-height: 1.1em; + } + + .description { + font-size: $base-font-size; + color: var(--secondary-color-override, $viewer-secondary-color); + } + + /* =================== CONTENT ===================*/ + .info { flex: 1; - max-height: 100%; + width: $content-width; margin-inline: auto; overflow-y: auto; - width: min(calc(100vw - 4rem), 960px); + display: flex; flex-direction: column; - align-items: start; gap: $view-element-gap; + padding-block: $view-element-gap; padding-bottom: 10vh; } + .info__card { + background-color: var(--card-background-color-override, $viewer-card-bg-color); + border-radius: $element-border-radius; + padding: $view-block-padding $view-inline-padding; + + display: flex; + flex-direction: column; + gap: 0.35em; + } + + .info__media { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: $view-element-gap; + } + + .info__media .info__value { + flex: 1; + min-width: 0; + } + .info__label { + font-size: $timer-label-size; font-weight: 600; + letter-spacing: 0.05em; color: var(--label-color-override, $viewer-label-color); text-transform: uppercase; } .info__value { white-space: break-spaces; - } - - .info__custom { - display: flex; - gap: 1rem; + line-height: 1.35; + overflow-wrap: anywhere; } .info__image-container { - display: flex; - justify-content: center; - align-items: center; - width: 192px; - height: 192px; + flex: 0 0 min(128px, 25%); } .info__image { - max-width: 100%; - max-height: 100%; - object-fit: contain; + display: block; + width: 100%; + height: auto; } .link.info__value { - display: flex; - gap: $view-element-gap; + display: inline-flex; + gap: 0.35em; align-items: center; color: $action-text-color; @@ -78,12 +124,13 @@ /* =================== MOBILE ===================*/ @media screen and (max-width: 768px) { .project { + .project-header { + flex-direction: column; + align-items: start; + gap: 0.5rem; + } .logo img { height: min(50px, 10vh); } - .info__image-container { - width: 96px; - height: 96px; - } } } diff --git a/apps/client/src/views/project-info/ProjectInfo.tsx b/apps/client/src/views/project-info/ProjectInfo.tsx index fed8b32c1..8e6c1f634 100644 --- a/apps/client/src/views/project-info/ProjectInfo.tsx +++ b/apps/client/src/views/project-info/ProjectInfo.tsx @@ -1,4 +1,5 @@ import { OntimeView } from 'ontime-types'; +import { type ReactNode, useState } from 'react'; import { IoOpenOutline } from 'react-icons/io5'; import EmptyPage from '../../common/components/state/EmptyPage'; @@ -41,56 +42,50 @@ function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) { return ( <> - ; + ); } + const hasHeader = Boolean(projectData.logo || projectData.title || projectData.description); + return (
- {projectData.logo && } + {hasHeader && ( +
+ {projectData.logo && } +
+ {projectData.title &&
{projectData.title}
} + {projectData.description &&
{projectData.description}
} +
+
+ )}
- {projectData.title && ( -
-
{getLocalizedString('project.title')}
-
{projectData.title}
-
- )} - {projectData.description && ( -
-
{getLocalizedString('project.description')}
-
{projectData.description}
-
- )} - {projectData.info && ( -
-
{getLocalizedString('project.info')}
-
{projectData.info}
-
- )} + {projectData.info && {projectData.info}} {projectData.url && ( -
+
{getLocalizedString('project.url')}
- {projectData.url} + {projectData.url} +
)} {projectData.custom.map((info, idx) => { - const hasUrl = Boolean(info.url); + const hasImage = Boolean(info.url); return ( // oxlint-disable-next-line react/no-array-index-key - we only have the index to go of here -
- {hasUrl && ( -
- +
+ {info.title &&
{info.title}
} + {hasImage ? ( +
+ + {info.value &&
{info.value}
}
+ ) : ( + info.value &&
{info.value}
)} -
-
{info.title}
-
{info.value}
-
); })} @@ -98,3 +93,35 @@ function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
); } + +interface InfoCardProps { + label: string; + children: ReactNode; +} + +function InfoCard({ label, children }: InfoCardProps) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +/** + * Renders an user provided image, collapsing itself if the image fails to load + * We remove the container along with the image to avoid leaving an empty gap in the card + */ +function InfoImage({ src }: { src: string }) { + const [hasError, setHasError] = useState(false); + + if (hasError) { + return null; + } + + return ( +
+ setHasError(true)} /> +
+ ); +}