News Dashboard: split into card + container, parent handles the data from the repo (#20503)

* Made card element it is own reusable component and passing the data as property.

* Created the umb-news-container element to handle all the priority grouping.

* Added hover styles to normal-priority cards.

* Removed unused variable.
This commit is contained in:
Engiber Lozada
2025-10-16 14:55:43 +02:00
committed by GitHub
parent ae73fb3431
commit 271edb5214
3 changed files with 245 additions and 187 deletions

View File

@@ -0,0 +1,132 @@
import { css, customElement, html, nothing, property, unsafeHTML, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { NewsDashboardItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@customElement('umb-news-card')
export class UmbNewsCardElement extends UmbLitElement {
@property({ type: Object })
item!: NewsDashboardItemResponseModel;
@property({ type: Number })
priority: number = 3;
#renderHeading(priority: number, text: string) {
if (priority <= 2) return html`<h2 class="card-title">${text}</h2>`;
return html`<h3 class="card-title">${text}</h3>`;
}
override render() {
if (!this.item) return nothing;
const isLastRow = this.priority === 3;
const showImage = this.priority <= 2 && !!this.item.imageUrl;
const content = html`
${when(
showImage,
() =>
this.item.imageUrl
? html`<img class="card-img" src=${this.item.imageUrl} alt=${this.item.imageAltText ?? ''} />`
: html`<div class="card-img placeholder" aria-hidden="true"></div>`,
() => nothing,
)}
<div class="card-body">
${this.#renderHeading(this.priority, this.item.header)}
${this.item.body ? html`<div class="card-text">${unsafeHTML(this.item.body)}</div>` : nothing}
${!isLastRow && this.item.url
? html`<div class="card-actions">
<uui-button look="outline" href=${this.item.url} target="_blank" rel="noopener">
${this.item.buttonText || 'Open'}
</uui-button>
</div>`
: nothing}
</div>
`;
// Last row: whole card is a link
return isLastRow
? this.item.url
? html`
<a class="card normal-priority" role="listitem" href=${this.item.url} target="_blank" rel="noopener">
${content}
</a>
`
: html` <article class="card normal-priority" role="listitem">${content}</article> `
: html` <article class="card" role="listitem">${content}</article> `;
}
static override styles = css`
:host {
display: block;
height: 100%;
}
.card {
background: var(--uui-color-surface);
border-radius: var(--uui-border-radius, 8px);
box-shadow: var(
--uui-box-box-shadow,
var(--uui-shadow-depth-1, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24))
);
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
.card-img {
width: 100%;
object-fit: cover;
display: block;
}
.card-img.placeholder {
height: 8px;
}
.card-body {
display: flex;
flex-direction: column;
padding: var(--uui-size-space-5);
flex: 1 1 auto;
justify-content: space-between;
gap: var(--uui-size-space-3, 9px);
}
.card-title {
margin: 0;
}
.card-text > p {
margin: 0;
}
.normal-priority {
display: block;
border: 1px solid var(--uui-color-divider);
border-radius: var(--uui-border-radius, 8px);
text-decoration: none;
color: inherit;
overflow: hidden;
.card-body {
gap: 0;
}
}
.normal-priority:hover {
color: var(--uui-color-interactive-emphasis);
}
.card-actions {
align-self: end;
}
`;
}
export default UmbNewsCardElement;
declare global {
interface HTMLElementTagNameMap {
'umb-news-card': UmbNewsCardElement;
}
}

View File

@@ -0,0 +1,110 @@
import { css, customElement, html, nothing, property, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { NewsDashboardItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import './umb-news-card.element.js';
import { sanitizeHTML } from '@umbraco-cms/backoffice/utils';
@customElement('umb-news-container')
export class UmbNewsContainerElement extends UmbLitElement {
@property({ type: Array })
items: Array<NewsDashboardItemResponseModel> = [];
#groupItemsByPriority(items: NewsDashboardItemResponseModel[]) {
const sanitizedItems = items.map((i) => ({
...i,
body: i.body ? sanitizeHTML(i.body) : '',
}));
// Separate items by priority.
const priority1 = sanitizedItems.filter((item) => item.priority === 'High');
const priority2 = sanitizedItems.filter((item) => item.priority === 'Medium');
const priority3 = sanitizedItems.filter((item) => item.priority === 'Normal');
// Group 1: First 4 items from priority 1.
const group1Items = priority1.slice(0, 4);
const overflow1 = priority1.slice(4);
// Group 2: Overflow from priority 1 + priority 2 items (max 4 total).
const group2Items = [...overflow1, ...priority2].slice(0, 4);
const overflow2Count = overflow1.length + priority2.length - 4;
const overflow2 = overflow2Count > 0 ? [...overflow1, ...priority2].slice(4) : [];
// Group 3: Overflow from groups 1 & 2 + priority 3 items.
const group3Items = [...overflow2, ...priority3];
return [
{ priority: 1, items: group1Items },
{ priority: 2, items: group2Items },
{ priority: 3, items: group3Items },
];
}
override render() {
if (!this.items?.length) return nothing;
const groups = this.#groupItemsByPriority(this.items);
return html`
${repeat(
groups,
(g) => g.priority,
(g) => html`
<div class="cards" role="list" aria-label=${`Priority ${g.priority}`}>
${repeat(
g.items,
(i, idx) => i.url || i.header || idx,
(i) => html`<umb-news-card .item=${i} .priority=${g.priority}></umb-news-card>`,
)}
</div>
`,
)}
`;
}
static override styles = css`
.cards {
--cols: 4;
--gap: var(--uui-size-space-4);
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols)), 1fr));
gap: var(--gap);
}
.cards + .cards {
margin-top: var(--uui-size-space-5);
}
/* For when container-type is not been assigned, not so sure about it???*/
@media (max-width: 1200px) {
.cards {
grid-template-columns: repeat(auto-fit, minmax(2, 1fr));
}
}
@media (max-width: 700px) {
.cards {
grid-template-columns: 1fr;
}
}
@container dashboard (max-width: 1200px) {
.cards {
grid-template-columns: repeat(auto-fit, minmax(2, 1fr));
}
}
@container dashboard (max-width: 700px) {
.cards {
grid-template-columns: 1fr;
}
}
`;
}
export default UmbNewsContainerElement;
declare global {
interface HTMLElementTagNameMap {
'umb-news-container': UmbNewsContainerElement;
}
}

View File

@@ -1,32 +1,16 @@
import { UmbNewsDashboardRepository } from './repository/index.js';
import {
css,
customElement,
html,
nothing,
repeat,
state,
unsafeHTML,
when,
} from '@umbraco-cms/backoffice/external/lit';
import { sanitizeHTML } from '@umbraco-cms/backoffice/utils';
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { NewsDashboardItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
interface UmbNewsDashboardGroupedItems {
priority: number;
items: Array<NewsDashboardItemResponseModel>;
}
import './components/umb-news-container.element.js';
@customElement('umb-umbraco-news-dashboard')
export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
@state()
private _items: Array<NewsDashboardItemResponseModel> = [];
@state()
private _groupedItems: Array<UmbNewsDashboardGroupedItems> = [];
@state()
private _loaded: boolean = false;
@@ -35,40 +19,9 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
override async firstUpdated() {
const res = await this.#repo.getNewsDashboard();
this._items = res.data?.items ?? [];
this._groupedItems = this.#groupItemsByPriority();
this._loaded = true;
}
#groupItemsByPriority(): Array<UmbNewsDashboardGroupedItems> {
const sanitizedItems = this._items.map((i) => ({
...i,
body: i.body ? sanitizeHTML(i.body) : '',
}));
// Separate items by priority.
const priority1 = sanitizedItems.filter((item) => item.priority === 'High');
const priority2 = sanitizedItems.filter((item) => item.priority === 'Medium');
const priority3 = sanitizedItems.filter((item) => item.priority === 'Normal');
// Group 1: First 4 items from priority 1.
const group1Items = priority1.slice(0, 4);
const overflow1 = priority1.slice(4);
// Group 2: Overflow from priority 1 + priority 2 items (max 4 total).
const group2Items = [...overflow1, ...priority2].slice(0, 4);
const overflow2Count = overflow1.length + priority2.length - 4;
const overflow2 = overflow2Count > 0 ? [...overflow1, ...priority2].slice(4) : [];
// Group 3: Overflow from groups 1 & 2 + priority 3 items.
const group3Items = [...overflow2, ...priority3];
return [
{ priority: 1, items: group1Items },
{ priority: 2, items: group2Items },
{ priority: 3, items: group3Items },
];
}
override render() {
if (!this._loaded) {
return html`<div class="loader"><uui-loader></uui-loader></div>`;
@@ -78,58 +31,7 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
return this.#renderDefaultContent();
}
return html`
${repeat(
this._groupedItems,
(g) => g.priority,
(g) => html`
<div class="cards">
${repeat(
g.items,
(i, idx) => i.url || i.header || idx,
(i) => {
const isLastRow = g.priority === 3;
const content = html`
${when(
g.priority <= 2,
() =>
html`${i.imageUrl
? html`<img class="card-img" src=${i.imageUrl} alt=${i.imageAltText ?? ''} />`
: html`<div class="card-img placeholder" aria-hidden="true"></div>`}`,
() => nothing,
)}
<div class="card-body">
${g.priority <= 2
? html`<h2 class="card-title">${i.header}</h2>`
: html`<h3 class="card-title">${i.header}</h3>`}
${i.body ? html`<div class="card-text">${unsafeHTML(i.body)}</div>` : null}
${!isLastRow && i.url
? html`<div class="card-actions">
<uui-button look="outline" href=${i.url} target="_blank">
${i.buttonText || 'Open'}
</uui-button>
</div>`
: nothing}
</div>
`;
// LAST ROW: whole card is a link
return isLastRow
? i.url
? html`
<a class="card normal-priority" role="listitem" href=${i.url} target="_blank" rel="noopener">
${content}
</a>
`
: html` <article class="card normal-priority" role="listitem">${content}</article> `
: html` <article class="card" role="listitem">${content}</article> `;
},
)}
</div>
`,
)}
`;
return html` <umb-news-container .items=${this._items}></umb-news-container> `;
}
#renderDefaultContent() {
@@ -234,92 +136,6 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
margin-top: 0;
margin-bottom: 0;
}
/* Grid */
.cards {
--cols: 4;
--gap: var(--uui-size-space-4);
width: 100%;
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols)), 1fr)
);
gap: var(--gap);
}
.cards + .cards {
margin-top: var(--uui-size-space-5);
}
@container (max-width: 1200px) {
.cards {
grid-template-columns: repeat(auto-fit, minmax(2, 1fr));
}
}
@container (max-width: 700px) {
.cards {
grid-template-columns: 1fr;
}
}
/* Card */
.card {
background: var(--uui-color-surface);
border-radius: var(--uui-border-radius, 8px);
box-shadow: var(
--uui-box-box-shadow,
var(--uui-shadow-depth-1, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24))
);
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
.card-img {
width: 100%;
object-fit: cover;
display: block;
}
.card-img.placeholder {
height: 8px;
}
.card-body {
display: flex;
flex-direction: column;
padding: var(--uui-size-space-5);
flex: 1 1 auto;
justify-content: space-between;
gap: var(--uui-size-space-3, 9px);
}
.card-title {
margin: 0;
}
.card-text > p {
margin: 0;
}
.normal-priority {
display: block;
border: 1px solid var(--uui-color-divider);
border-radius: var(--uui-border-radius, 8px);
text-decoration: none;
color: inherit;
overflow: hidden;
.card-body {
gap: 0;
}
}
.card-actions {
align-self: end;
}
`,
];
}