mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
116 lines
2.7 KiB
TypeScript
116 lines
2.7 KiB
TypeScript
import * as React from "react";
|
|
import type { MetaFunction } from "react-router";
|
|
import { Link, useLoaderData } from "react-router";
|
|
import { Main } from "~/components/Main";
|
|
import { Markdown } from "~/components/Markdown";
|
|
import invariant from "~/utils/invariant";
|
|
import type { SendouRouteHandle } from "~/utils/remix.server";
|
|
import {
|
|
ARTICLES_MAIN_PAGE,
|
|
articlePreviewUrl,
|
|
navIconUrl,
|
|
} from "~/utils/urls";
|
|
import { metaTags, type SerializeFrom } from "../../../utils/remix";
|
|
|
|
import { loader } from "../loaders/a.$slug.server";
|
|
|
|
export { loader };
|
|
|
|
export const handle: SendouRouteHandle = {
|
|
breadcrumb: ({ match }) => {
|
|
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
|
|
|
if (!data) return [];
|
|
|
|
return [
|
|
{
|
|
imgPath: navIconUrl("articles"),
|
|
href: ARTICLES_MAIN_PAGE,
|
|
type: "IMAGE",
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
export const meta: MetaFunction = (args) => {
|
|
invariant(args.params.slug);
|
|
const data = args.data as SerializeFrom<typeof loader> | null;
|
|
|
|
if (!data) return [];
|
|
|
|
const description = data.content.trim().split("\n")[0];
|
|
|
|
return metaTags({
|
|
title: data.title,
|
|
description,
|
|
image: {
|
|
url: articlePreviewUrl(args.params.slug),
|
|
},
|
|
location: args.location,
|
|
});
|
|
};
|
|
|
|
export default function ArticlePage() {
|
|
const data = useLoaderData<typeof loader>();
|
|
return (
|
|
<Main>
|
|
<article className="article">
|
|
<h1>{data.title}</h1>
|
|
<div className="text-sm text-lighter">
|
|
by <Author /> • <time>{data.dateString}</time>
|
|
</div>
|
|
<Markdown>
|
|
{contentWithoutLeadingTitle(data.content, data.title)}
|
|
</Markdown>
|
|
</article>
|
|
</Main>
|
|
);
|
|
}
|
|
|
|
function normalizeText(text: string) {
|
|
return text
|
|
.replace(/\*+/g, "")
|
|
.replace(/…/g, "...")
|
|
.replace(/\\!/g, "!")
|
|
.trim();
|
|
}
|
|
|
|
function contentWithoutLeadingTitle(content: string, title: string) {
|
|
const trimmed = content.trimStart();
|
|
const firstLineEnd = trimmed.indexOf("\n");
|
|
const firstLine =
|
|
firstLineEnd === -1 ? trimmed : trimmed.slice(0, firstLineEnd);
|
|
|
|
if (
|
|
firstLine.startsWith("# ") &&
|
|
normalizeText(firstLine.slice(2)) === normalizeText(title)
|
|
) {
|
|
return trimmed.slice(firstLine.length).trimStart();
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
function Author() {
|
|
const data = useLoaderData<typeof loader>();
|
|
|
|
return data.authors.map((author, i) => {
|
|
if (!author.link) return author.name;
|
|
|
|
const authorLink = author.link.includes("https://sendou.ink")
|
|
? author.link.replace("https://sendou.ink", "")
|
|
: author.link;
|
|
|
|
return (
|
|
<React.Fragment key={author.name}>
|
|
{author.link.includes("https://sendou.ink") ? (
|
|
<Link to={authorLink}>{author.name}</Link>
|
|
) : (
|
|
<a href={author.link}>{author.name}</a>
|
|
)}
|
|
{i < data.authors.length - 1 ? " & " : ""}
|
|
</React.Fragment>
|
|
);
|
|
});
|
|
}
|