diff --git a/package-lock.json b/package-lock.json index 57b8a3aa54b9e4..671dd832c6ff1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21024,6 +21024,10 @@ "resolved": "routes/navigation", "link": true }, + "node_modules/@wordpress/news-widget": { + "resolved": "widgets/news", + "link": true + }, "node_modules/@wordpress/notices": { "resolved": "packages/notices", "link": true @@ -67091,6 +67095,19 @@ "clsx": "^2.1.1" } }, + "widgets/news": { + "name": "@wordpress/news-widget", + "version": "1.0.0", + "dependencies": { + "@wordpress/dataviews": "file:../../packages/dataviews", + "@wordpress/date": "file:../../packages/date", + "@wordpress/element": "file:../../packages/element", + "@wordpress/html-entities": "file:../../packages/html-entities", + "@wordpress/i18n": "file:../../packages/i18n", + "@wordpress/icons": "file:../../packages/icons", + "@wordpress/ui": "file:../../packages/ui" + } + }, "widgets/quick-draft": { "name": "@wordpress/quick-draft-widget", "version": "1.0.0", diff --git a/widgets/news/components/index.ts b/widgets/news/components/index.ts new file mode 100644 index 00000000000000..ceccd74481f2d7 --- /dev/null +++ b/widgets/news/components/index.ts @@ -0,0 +1 @@ +export { NewsList } from './news-list'; diff --git a/widgets/news/components/news-list/index.ts b/widgets/news/components/news-list/index.ts new file mode 100644 index 00000000000000..ceccd74481f2d7 --- /dev/null +++ b/widgets/news/components/news-list/index.ts @@ -0,0 +1 @@ +export { NewsList } from './news-list'; diff --git a/widgets/news/components/news-list/news-list.module.css b/widgets/news/components/news-list/news-list.module.css new file mode 100644 index 00000000000000..e0f6ea4e1417c7 --- /dev/null +++ b/widgets/news/components/news-list/news-list.module.css @@ -0,0 +1,28 @@ +.root { + flex: 1; + min-width: 0; + min-height: 0; + overflow: auto; +} + +.feedIcon { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + border-radius: var(--wpds-border-radius-md); + background-color: var(--wpds-color-bg-track-neutral-weak); + color: var(--wpds-color-fg-content-neutral-weak); +} + +.titleLink { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.meta { + color: var(--wpds-color-fg-content-neutral-weak); +} diff --git a/widgets/news/components/news-list/news-list.tsx b/widgets/news/components/news-list/news-list.tsx new file mode 100644 index 00000000000000..67944fbfb33f68 --- /dev/null +++ b/widgets/news/components/news-list/news-list.tsx @@ -0,0 +1,209 @@ +/** + * WordPress dependencies + */ +import { DataViews, type Field, type View } from '@wordpress/dataviews'; +import { dateI18n } from '@wordpress/date'; +import { useEffect, useMemo, useState } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; +import { __, _x } from '@wordpress/i18n'; +import { globe, postList, wordpress } from '@wordpress/icons'; +import { EmptyState, Icon, Link, Stack, Text } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import styles from './news-list.module.css'; + +interface NewsPost { + id: number; + title: { rendered: string }; + link: string; + date: string; +} + +interface NewsFeed { + key: string; + label: string; + siteUrl: string; + posts: NewsPost[]; +} + +type NewsItem = { + id: string; + feedKey: string; + feedLabel: string; + title: string; + url: string; + date: string; +}; + +const NEWS_FEEDS = [ + { + key: 'news', + label: __( 'WordPress Blog' ), + siteUrl: _x( 'https://wordpress.org/news/', 'News dashboard widget' ), + apiUrl: 'https://wordpress.org/news/wp-json/wp/v2/posts?per_page=4&_fields=id,title,link,date', + }, + { + key: 'planet', + label: __( 'Other WordPress News' ), + siteUrl: _x( 'https://planet.wordpress.org/', 'News dashboard widget' ), + apiUrl: 'https://planet.wordpress.org/wp-json/wp/v2/posts?per_page=4&_fields=id,title,link,date', + }, +]; + +const DEFAULT_LAYOUTS = { list: {} }; + +const INITIAL_VIEW: View = { + type: 'list', + page: 1, + perPage: 4, + search: '', + filters: [], + fields: [], + titleField: 'title', + descriptionField: 'meta', + mediaField: 'feedIcon', + showMedia: true, + layout: { density: 'compact' }, +}; + +function FeedIcon( { item }: { item: NewsItem } ) { + return ( + + ); +} + +function NewsTitle( { item }: { item: NewsItem } ) { + return ( + + { item.title } + + ); +} + +function NewsMeta( { item }: { item: NewsItem } ) { + return ( + + { item.feedLabel } · { dateI18n( __( 'M j, Y g:i a' ), item.date ) } + + ); +} + +const emptyState = ( + + + + + { __( 'Quiet for now — the next headline is on its way.' ) } + + + +); + +function combineFeedPosts( newsFeeds: NewsFeed[] ): NewsItem[] { + return newsFeeds + .flatMap( ( feed ) => + feed.posts.map( ( post ) => ( { + id: `${ feed.key }-${ post.id }`, + feedKey: feed.key, + feedLabel: feed.label, + title: decodeEntities( post.title.rendered ), + url: post.link, + date: post.date, + } ) ) + ) + .sort( + ( a, b ) => + new Date( b.date ).getTime() - new Date( a.date ).getTime() + ); +} + +/* + * Renders WordPress.org and Planet WordPress posts through the DataViews list + * layout. Mounting this component triggers the feed requests. + */ +export function NewsList() { + const [ view, setView ] = useState< View >( INITIAL_VIEW ); + const [ newsFeeds, setNewsFeeds ] = useState< NewsFeed[] >( [] ); + const [ isLoading, setIsLoading ] = 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( () => setIsLoading( false ) ); + }, [] ); + + const items = useMemo( () => combineFeedPosts( newsFeeds ), [ newsFeeds ] ); + + const fields = useMemo< Field< NewsItem >[] >( + () => [ + { + id: 'title', + label: __( 'Title' ), + enableSorting: false, + enableHiding: false, + render: ( { item } ) => , + }, + { + id: 'meta', + label: __( 'Source' ), + enableSorting: false, + enableHiding: false, + render: ( { item } ) => , + }, + { + id: 'feedIcon', + label: __( 'Source' ), + enableSorting: false, + enableHiding: false, + render: ( { item } ) => , + }, + ], + [] + ); + + return ( +
+ item.id } + isLoading={ isLoading } + paginationInfo={ { + totalItems: items.length, + totalPages: 1, + } } + defaultLayouts={ DEFAULT_LAYOUTS } + empty={ emptyState } + > + + +
+ ); +} diff --git a/widgets/news/package.json b/widgets/news/package.json new file mode 100644 index 00000000000000..0dc9253e58e5dd --- /dev/null +++ b/widgets/news/package.json @@ -0,0 +1,14 @@ +{ + "name": "@wordpress/news-widget", + "version": "1.0.0", + "private": true, + "dependencies": { + "@wordpress/dataviews": "file:../../packages/dataviews", + "@wordpress/date": "file:../../packages/date", + "@wordpress/element": "file:../../packages/element", + "@wordpress/html-entities": "file:../../packages/html-entities", + "@wordpress/i18n": "file:../../packages/i18n", + "@wordpress/icons": "file:../../packages/icons", + "@wordpress/ui": "file:../../packages/ui" + } +} diff --git a/widgets/news/render.module.css b/widgets/news/render.module.css new file mode 100644 index 00000000000000..3d9ebe468f84cd --- /dev/null +++ b/widgets/news/render.module.css @@ -0,0 +1,5 @@ +.footer { + border-block-start: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral-weak); + padding-block: var(--wpds-dimension-padding-lg); + padding-inline: var(--wpds-dimension-padding-2xl); +} diff --git a/widgets/news/render.tsx b/widgets/news/render.tsx new file mode 100644 index 00000000000000..782b2d3436ac05 --- /dev/null +++ b/widgets/news/render.tsx @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; +import { Link } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import { NewsList } from './components'; +import styles from './render.module.css'; + +export default function WordPressNews() { + return ( + <> + +
+ + { __( 'See all' ) } + +
+ + ); +} diff --git a/widgets/news/widget.json b/widgets/news/widget.json new file mode 100644 index 00000000000000..4365712cbe7546 --- /dev/null +++ b/widgets/news/widget.json @@ -0,0 +1,7 @@ +{ + "name": "core/news", + "title": "WordPress news", + "description": "Displays the latest news from WordPress.org and Planet WordPress.", + "category": "dashboard", + "presentation": "content-bleed" +} diff --git a/widgets/news/widget.ts b/widgets/news/widget.ts new file mode 100644 index 00000000000000..0acec4ef377d64 --- /dev/null +++ b/widgets/news/widget.ts @@ -0,0 +1,6 @@ +import { __ } from '@wordpress/i18n'; + +export default { + name: 'core/news', + title: __( 'WordPress news' ), +};