14
resources/client/news/news-article-byline.tsx
Executable file
14
resources/client/news/news-article-byline.tsx
Executable file
@@ -0,0 +1,14 @@
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import React from 'react';
|
||||
import {NewsArticle} from '@app/titles/models/news-article';
|
||||
|
||||
interface Props {
|
||||
article: NewsArticle;
|
||||
}
|
||||
export function NewsArticleByline({article}: Props) {
|
||||
return article.byline ? (
|
||||
<span className="whitespace-nowrap">
|
||||
<Trans message="By :name" values={{name: article.byline}} />
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
28
resources/client/news/news-article-grid-item.tsx
Executable file
28
resources/client/news/news-article-grid-item.tsx
Executable file
@@ -0,0 +1,28 @@
|
||||
import {NewsArticleImage} from '@app/news/news-article-image';
|
||||
import {NewsArticleLink} from '@app/news/news-article-link';
|
||||
import {BulletSeparatedItems} from '@app/titles/bullet-separated-items';
|
||||
import {FormattedDate} from '@common/i18n/formatted-date';
|
||||
import {NewsArticle} from '@app/titles/models/news-article';
|
||||
|
||||
interface Props {
|
||||
article: NewsArticle;
|
||||
}
|
||||
export function NewsArticleGridItem({article}: Props) {
|
||||
return (
|
||||
<div className="items-start gap-14 lg:flex">
|
||||
<NewsArticleImage
|
||||
article={article}
|
||||
className="aspect-poster max-w-90 max-md:hidden"
|
||||
/>
|
||||
<div className="min-w-0 overflow-hidden overflow-ellipsis text-base md:mt-24 lg:mt-6">
|
||||
<NewsArticleLink article={article} className="font-medium" />
|
||||
<BulletSeparatedItems className="mt-10 min-w-0 overflow-hidden overflow-ellipsis text-xs">
|
||||
<FormattedDate date={article.created_at} />
|
||||
<div className="overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{article.source}
|
||||
</div>
|
||||
</BulletSeparatedItems>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
resources/client/news/news-article-image.tsx
Executable file
50
resources/client/news/news-article-image.tsx
Executable file
@@ -0,0 +1,50 @@
|
||||
import {useTrans} from '@common/i18n/use-trans';
|
||||
import {message} from '@common/i18n/message';
|
||||
import clsx from 'clsx';
|
||||
import {NewsArticle} from '@app/titles/models/news-article';
|
||||
import {NewsArticleLink} from '@app/news/news-article-link';
|
||||
import {NewspaperIcon} from '@common/icons/material/Newspaper';
|
||||
|
||||
interface Props {
|
||||
article: NewsArticle;
|
||||
className?: string;
|
||||
size?: string;
|
||||
lazy?: boolean;
|
||||
}
|
||||
export function NewsArticleImage({
|
||||
article,
|
||||
className,
|
||||
size,
|
||||
lazy = true,
|
||||
}: Props) {
|
||||
const {trans} = useTrans();
|
||||
const src = article.image;
|
||||
|
||||
const imageClassName = clsx(
|
||||
className,
|
||||
size,
|
||||
'object-cover bg-fg-base/4 rounded',
|
||||
!src ? 'flex items-center justify-center' : 'block'
|
||||
);
|
||||
|
||||
const image = src ? (
|
||||
<img
|
||||
className={imageClassName}
|
||||
draggable={false}
|
||||
loading={lazy ? 'lazy' : 'eager'}
|
||||
src={src}
|
||||
alt={trans(message('Image for :name', {values: {name: article.title}}))}
|
||||
/>
|
||||
) : (
|
||||
<span className={imageClassName}>
|
||||
<NewspaperIcon className="max-w-[60%] text-divider" size="text-6xl" />
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<NewsArticleLink article={article} className="group relative flex-shrink-0">
|
||||
{image}
|
||||
<div className="pointer-events-none absolute inset-0 bg-black opacity-0 transition-opacity group-hover:opacity-10" />
|
||||
</NewsArticleLink>
|
||||
);
|
||||
}
|
||||
50
resources/client/news/news-article-link.tsx
Executable file
50
resources/client/news/news-article-link.tsx
Executable file
@@ -0,0 +1,50 @@
|
||||
import {Link, LinkProps} from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
import React, {ReactNode, useMemo} from 'react';
|
||||
import {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';
|
||||
import {NewsArticle} from '@app/titles/models/news-article';
|
||||
|
||||
interface Props extends Omit<LinkProps, 'to'> {
|
||||
article: NewsArticle;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
color?: 'primary' | 'inherit';
|
||||
}
|
||||
export function NewsArticleLink({
|
||||
article,
|
||||
className,
|
||||
children,
|
||||
color = 'inherit',
|
||||
...linkProps
|
||||
}: Props) {
|
||||
const finalUri = useMemo(() => {
|
||||
return getNewsArticleLink(article);
|
||||
}, [article]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
{...linkProps}
|
||||
className={clsx(
|
||||
color === 'primary'
|
||||
? 'text-primary hover:text-primary-dark'
|
||||
: 'text-inherit',
|
||||
'overflow-x-hidden overflow-ellipsis outline-none transition-colors hover:underline focus-visible:underline',
|
||||
className,
|
||||
)}
|
||||
to={finalUri}
|
||||
>
|
||||
{children ?? article.title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function getNewsArticleLink(
|
||||
article: NewsArticle,
|
||||
{absolute}: {absolute?: boolean} = {},
|
||||
): string {
|
||||
let link = `/news/${article.slug}`;
|
||||
if (absolute) {
|
||||
link = `${getBootstrapData().settings.base_url}${link}`;
|
||||
}
|
||||
return link;
|
||||
}
|
||||
93
resources/client/news/news-article-page.tsx
Executable file
93
resources/client/news/news-article-page.tsx
Executable file
@@ -0,0 +1,93 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { PageMetaTags } from "@common/http/page-meta-tags";
|
||||
import { PageStatus } from "@common/http/page-status";
|
||||
import { SitePageLayout } from "@app/site-page-layout";
|
||||
import {
|
||||
GetNewsArticleResponse,
|
||||
useNewsArticle
|
||||
} from "@app/admin/news/requests/use-news-article";
|
||||
import { NewsArticle } from "@app/titles/models/news-article";
|
||||
import { Trans } from "@common/i18n/trans";
|
||||
import { FormattedDate } from "@common/i18n/formatted-date";
|
||||
import { BulletSeparatedItems } from "@app/titles/bullet-separated-items";
|
||||
import { NewsArticleImage } from "@app/news/news-article-image";
|
||||
import { NewsArticleLink } from "@app/news/news-article-link";
|
||||
import { NewsArticleByline } from "@app/news/news-article-byline";
|
||||
import { NewsArticleSourceLink } from "@app/news/news-article-source-link";
|
||||
|
||||
export function NewsArticlePage() {
|
||||
const query = useNewsArticle('newsArticlePage');
|
||||
|
||||
const content = query.data ? (
|
||||
<Fragment>
|
||||
<PageMetaTags query={query} />
|
||||
<PageContent data={query.data} />
|
||||
</Fragment>
|
||||
) : (
|
||||
<PageStatus query={query} loaderClassName="absolute inset-0 m-auto" />
|
||||
);
|
||||
|
||||
return <SitePageLayout>{content}</SitePageLayout>;
|
||||
}
|
||||
|
||||
interface PageContentProps {
|
||||
data: GetNewsArticleResponse;
|
||||
}
|
||||
function PageContent({data: {article, related}}: PageContentProps) {
|
||||
return (
|
||||
<div className="container mx-auto mt-14 items-start gap-40 px-14 md:mt-40 md:px-24 lg:flex">
|
||||
<main className="mb-24 rounded border p-16 flex-auto">
|
||||
<h1 className="mb-24 text-3xl md:text-4xl">{article.title}</h1>
|
||||
<div className="items-start gap-16 md:flex">
|
||||
<NewsArticleImage
|
||||
article={article}
|
||||
size="w-184 h-184"
|
||||
className="max-md:mb-24"
|
||||
/>
|
||||
<div
|
||||
className="prose text dark:prose-invert"
|
||||
dangerouslySetInnerHTML={{__html: article.body}}
|
||||
/>
|
||||
</div>
|
||||
<BulletSeparatedItems className="mt-24 text-sm text-muted">
|
||||
<FormattedDate date={article.created_at} />
|
||||
{article.byline ? <NewsArticleByline article={article} /> : null}
|
||||
{article.source ? (
|
||||
<NewsArticleSourceLink article={article} />
|
||||
) : null}
|
||||
</BulletSeparatedItems>
|
||||
</main>
|
||||
<OtherNews articles={related} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface OtherNewsProps {
|
||||
articles: NewsArticle[];
|
||||
}
|
||||
function OtherNews({articles}: OtherNewsProps) {
|
||||
return (
|
||||
<div className="w-full max-w-full flex-shrink-0 lg:w-400">
|
||||
<h2 className="mb-14 text-2xl">
|
||||
<Trans message="Other news" />
|
||||
</h2>
|
||||
{articles.map(article => (
|
||||
<div
|
||||
key={article.id}
|
||||
className="mb-14 flex items-center gap-14 rounded border pr-14"
|
||||
>
|
||||
<NewsArticleImage article={article} size="w-80 h-80" lazy={false} />
|
||||
<div className="min-w-0">
|
||||
<h3 className="line-clamp-2 text-sm font-semibold">
|
||||
<NewsArticleLink article={article} />
|
||||
</h3>
|
||||
<BulletSeparatedItems className="mt-6 text-sm text-muted">
|
||||
<FormattedDate date={article.created_at} />
|
||||
<NewsArticleByline article={article} />
|
||||
</BulletSeparatedItems>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
resources/client/news/news-article-source-link.tsx
Executable file
28
resources/client/news/news-article-source-link.tsx
Executable file
@@ -0,0 +1,28 @@
|
||||
import {NewsArticle} from '@app/titles/models/news-article';
|
||||
import clsx from 'clsx';
|
||||
import {OpenInNewIcon} from '@common/icons/material/OpenInNew';
|
||||
import {LinkStyle} from '@common/ui/buttons/external-link';
|
||||
import React from 'react';
|
||||
|
||||
interface SourceLinkProps {
|
||||
article: NewsArticle;
|
||||
className?: string;
|
||||
}
|
||||
export function NewsArticleSourceLink({article, className}: SourceLinkProps) {
|
||||
return (
|
||||
<div className={clsx('flex items-center gap-4 text-primary', className)}>
|
||||
<OpenInNewIcon size="xs" className="flex-shrink-0" />
|
||||
<a
|
||||
href={article.source_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={clsx(
|
||||
LinkStyle,
|
||||
'whitespace-nowrap overflow-hidden overflow-ellipsis'
|
||||
)}
|
||||
>
|
||||
{article.source}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user