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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user