Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions widgets/news/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* WordPress dependencies
*/
import type { ReactNode } from 'react';
import { Link, Stack, Text } from '@wordpress/ui';

/**
* Internal dependencies
*/
import styles from './style.module.css';

export type ListItem = {
id: string;
title: string;
url: string;
meta?: string[];
icon?: ReactNode;
};

export default function List( {
Copy link
Copy Markdown
Member Author

@simison simison May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preferably would use DataViews, but it wasn't great UI for this type of list, hence a rather custom implementation. Should be shared with Events widget.

items,
empty,
}: {
items: ListItem[];
empty?: ReactNode;
} ) {
if ( items.length === 0 ) {
return empty ?? null;
}

return (
<Stack gap="md" direction="column">
{ items.map( ( item ) => (
<Stack
key={ item.id }
gap="md"
direction="row"
align="start"
className={ styles.listItem }
>
{ item.icon && (
<div className={ styles.listItemIcon }>
{ item.icon }
</div>
) }
<Stack
direction="column"
gap="xs"
className={ styles.listItemContent }
>
<Text variant="body-md">
{ item.url ? (
<Link href={ item.url } openInNewTab>
{ item.title }
</Link>
) : (
item.title
) }
</Text>
{ item.meta && item.meta.length > 0 && (
<Stack
direction="row"
gap="xs"
className={ styles.statusText }
>
{ item.meta.map( ( metaItem, index ) => (
<Text
key={ `${ item.id }-meta-${ index }` }
variant="body-sm"
className={ styles.statusText }
>
{ index > 0 && '· ' }
{ metaItem }
</Text>
) ) }
</Stack>
) }
</Stack>
</Stack>
) ) }
</Stack>
);
}
153 changes: 153 additions & 0 deletions widgets/news/render.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* WordPress dependencies
*/
import { useState, useEffect } from '@wordpress/element';
import { dateI18n } from '@wordpress/date';
import { __, _x } from '@wordpress/i18n';
import { globe, postList, wordpress } from '@wordpress/icons';
import { Spinner } from '@wordpress/components';
/* eslint-disable @wordpress/use-recommended-components */
import { Card, EmptyState, Icon, Link, Stack } from '@wordpress/ui';
/* eslint-enable @wordpress/use-recommended-components */

/**
* Internal dependencies
*/
import List, { type ListItem } from './list';

interface NewsPost {
id: number;
title: { rendered: string };
link: string;
date: string;
}

interface NewsFeed {
key: string;
label: string;
siteUrl: string;
posts: NewsPost[];
}

const NEWS_FEEDS = [
{
key: 'news',
label: __( 'WordPress Blog' ),
siteUrl: _x(
'https://wordpress.org/news/',
'Events and News dashboard widget'
),
apiUrl: 'https://wordpress.org/news/wp-json/wp/v2/posts?per_page=3&_fields=id,title,link,date',
},
{
key: 'planet',
label: __( 'Other WordPress News' ),
siteUrl: _x(
'https://planet.wordpress.org/',
'Events and News dashboard widget'
),
apiUrl: 'https://planet.wordpress.org/wp-json/wp/v2/posts?per_page=3&_fields=id,title,link,date',
},
];

function decodeEntities( html: string ): string {
const txt = document.createElement( 'textarea' );
txt.innerHTML = html;
return txt.value;
}

export default function WordPressNews() {
const [ newsFeeds, setNewsFeeds ] = useState< NewsFeed[] >( [] );
const [ newsLoading, setNewsLoading ] = useState( true );

useEffect( () => {
Promise.all(
NEWS_FEEDS.map( async ( feed ) => {
try {
const posts: NewsPost[] = await fetch( feed.apiUrl ).then(
( r ) => r.json()
);
return {
key: feed.key,
label: feed.label,
siteUrl: feed.siteUrl,
posts,
};
} catch {
return {
key: feed.key,
label: feed.label,
siteUrl: feed.siteUrl,
posts: [],
};
}
} )
)
.then( setNewsFeeds )
.finally( () => setNewsLoading( false ) );
}, [] );

const combinedItems: ListItem[] = newsFeeds
.flatMap( ( feed ) =>
feed.posts.map( ( post ) => ( {
feedKey: feed.key,
feedLabel: feed.label,
id: post.id,
title: decodeEntities( post.title.rendered ),
url: post.link,
date: post.date,
} ) )
)
.sort(
( a, b ) =>
new Date( b.date ).getTime() - new Date( a.date ).getTime()
)
.map( ( post ) => ( {
id: `${ post.feedKey }-${ post.id }`,
title: post.title,
url: post.url,
icon:
post.feedKey === 'news' ? (
<Icon icon={ wordpress } />
) : (
<Icon icon={ globe } />
),
meta: [
post.feedLabel,
dateI18n( __( 'M j, Y g:i a' ), post.date ),
],
} ) );

const emptyState = (
<Stack align="center" justify="center" style={ { margin: '24px 0' } }>
<EmptyState.Root>
<EmptyState.Icon icon={ postList } />
<EmptyState.Title>
{ __( 'Quiet for now — the next headline is on its way.' ) }
</EmptyState.Title>
</EmptyState.Root>
</Stack>
);

return (
<Card.Content>
<Stack direction="column" justify="start" gap="md">
{ newsLoading && (
<Stack justify="center" align="center">
<Spinner />
</Stack>
) }
<List items={ combinedItems } empty={ emptyState } />
<Link
href={ _x(
'https://wordpress.org/news/all-posts/',
'Events and News dashboard widget'
) }
openInNewTab
>
{ __( 'See all' ) }
</Link>
</Stack>
</Card.Content>
);
}
30 changes: 30 additions & 0 deletions widgets/news/style.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

.footer {
padding: 12px 0 0;
}

.listItem {
inline-size: 100%;
}

.listItemIcon {
inline-size: 36px;
block-size: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: var(--wpds-color-bg-track-neutral-weak);
color: var(--wpds-color-fg-content-neutral-weak);
flex-shrink: 0;
}

.listItemContent {
flex: 1;
min-inline-size: 0;
}

.statusText {
color: var(--wpds-color-fg-content-neutral-weak);
margin: 0;
}
6 changes: 6 additions & 0 deletions widgets/news/widget.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "core/news",
"title": "WordPress news",
"description": "Displays the latest news from WordPress.org and Planet WordPress.",
"category": "dashboard"
}
6 changes: 6 additions & 0 deletions widgets/news/widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { __ } from '@wordpress/i18n';

export default {
name: 'core/news',
title: __( 'WordPress news' ),
};
Loading