forked from didirus/AstralRinth
308 lines
7.4 KiB
Vue
308 lines
7.4 KiB
Vue
<script setup lang="ts">
|
|
import { Avatar, ButtonStyled } from "@modrinth/ui";
|
|
import { RssIcon, GitGraphIcon } from "@modrinth/assets";
|
|
import dayjs from "dayjs";
|
|
import { articles as rawArticles } from "@modrinth/blog";
|
|
import { computed } from "vue";
|
|
import type { User } from "@modrinth/utils";
|
|
import ShareArticleButtons from "~/components/ui/ShareArticleButtons.vue";
|
|
import NewsletterButton from "~/components/ui/NewsletterButton.vue";
|
|
|
|
const config = useRuntimeConfig();
|
|
const route = useRoute();
|
|
|
|
const rawArticle = rawArticles.find((article) => article.slug === route.params.slug);
|
|
|
|
if (!rawArticle) {
|
|
throw createError({
|
|
fatal: true,
|
|
statusCode: 404,
|
|
message: "The requested article could not be found.",
|
|
});
|
|
}
|
|
|
|
const authorsUrl = `users?ids=${JSON.stringify(rawArticle.authors)}`;
|
|
|
|
const [authors, html] = await Promise.all([
|
|
rawArticle.authors
|
|
? useAsyncData(authorsUrl, () => useBaseFetch(authorsUrl)).then((data) => {
|
|
const users = data.data as Ref<User[]>;
|
|
users.value.sort((a, b) => {
|
|
return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id);
|
|
});
|
|
|
|
return users;
|
|
})
|
|
: Promise.resolve(),
|
|
rawArticle.html(),
|
|
]);
|
|
|
|
const article = computed(() => ({
|
|
...rawArticle,
|
|
path: `/news/${rawArticle.slug}`,
|
|
thumbnail: rawArticle.thumbnail
|
|
? `/news/article/${rawArticle.slug}/thumbnail.webp`
|
|
: `/news/default.webp`,
|
|
title: rawArticle.title,
|
|
summary: rawArticle.summary,
|
|
date: rawArticle.date,
|
|
html,
|
|
}));
|
|
|
|
const authorCount = computed(() => authors?.value?.length ?? 0);
|
|
|
|
const articleTitle = computed(() => article.value.title);
|
|
const articleUrl = computed(() => `https://modrinth.com/news/article/${route.params.slug}`);
|
|
|
|
const thumbnailPath = computed(() =>
|
|
article.value.thumbnail
|
|
? `${config.public.siteUrl}${article.value.thumbnail}`
|
|
: `${config.public.siteUrl}/news/default.jpg`,
|
|
);
|
|
|
|
const dayjsDate = computed(() => dayjs(article.value.date));
|
|
|
|
useSeoMeta({
|
|
title: () => `${articleTitle.value} - Modrinth News`,
|
|
ogTitle: () => articleTitle.value,
|
|
description: () => article.value.summary,
|
|
ogDescription: () => article.value.summary,
|
|
ogType: "article",
|
|
ogImage: () => thumbnailPath.value,
|
|
articlePublishedTime: () => dayjsDate.value.toISOString(),
|
|
twitterCard: "summary_large_image",
|
|
twitterImage: () => thumbnailPath.value,
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page experimental-styles-within py-6">
|
|
<div
|
|
class="flex flex-wrap items-center justify-between gap-4 border-0 border-b-[1px] border-solid border-divider px-6 pb-6"
|
|
>
|
|
<nuxt-link :to="`/news`">
|
|
<h1 class="m-0 text-3xl font-extrabold hover:underline">News</h1>
|
|
</nuxt-link>
|
|
<div class="flex gap-2">
|
|
<NewsletterButton />
|
|
<ButtonStyled circular>
|
|
<a v-tooltip="`RSS feed`" aria-label="RSS feed" href="/news/feed/rss.xml" target="_blank">
|
|
<RssIcon />
|
|
</a>
|
|
</ButtonStyled>
|
|
<ButtonStyled circular icon-only>
|
|
<a v-tooltip="`Changelog`" href="/news/changelog" aria-label="Changelog">
|
|
<GitGraphIcon />
|
|
</a>
|
|
</ButtonStyled>
|
|
</div>
|
|
</div>
|
|
<article class="mt-6 flex flex-col gap-4 px-6">
|
|
<h2 class="m-0 text-2xl font-extrabold leading-tight sm:text-4xl">{{ article.title }}</h2>
|
|
<p class="m-0 text-base leading-tight sm:text-lg">{{ article.summary }}</p>
|
|
<div class="mt-auto flex flex-wrap items-center gap-1 text-sm text-secondary sm:text-base">
|
|
<template v-for="(author, index) in authors" :key="`author-${author.id}`">
|
|
<span v-if="authorCount - 1 === index && authorCount > 1">and</span>
|
|
<span class="flex items-center">
|
|
<nuxt-link
|
|
:to="`/user/${author.id}`"
|
|
class="inline-flex items-center gap-1 font-semibold hover:underline hover:brightness-[--hover-brightness]"
|
|
>
|
|
<Avatar :src="author.avatar_url" circle size="24px" />
|
|
{{ author.username }}
|
|
</nuxt-link>
|
|
<span v-if="(authors?.length ?? 0) > 2 && index !== authorCount - 1">,</span>
|
|
</span>
|
|
</template>
|
|
<template v-if="!authors || authorCount === 0">
|
|
<nuxt-link
|
|
to="/organization/modrinth"
|
|
class="inline-flex items-center gap-1 font-semibold hover:underline hover:brightness-[--hover-brightness]"
|
|
>
|
|
<Avatar src="https://cdn-raw.modrinth.com/modrinth-icon-96.webp" size="24px" />
|
|
Modrinth Team
|
|
</nuxt-link>
|
|
</template>
|
|
<span class="hidden md:block">•</span>
|
|
<span class="hidden md:block"> {{ dayjsDate.format("MMMM D, YYYY") }}</span>
|
|
</div>
|
|
<span class="text-sm text-secondary sm:text-base md:hidden">
|
|
Posted on {{ dayjsDate.format("MMMM D, YYYY") }}</span
|
|
>
|
|
<ShareArticleButtons :title="article.title" :url="articleUrl" />
|
|
<img
|
|
:src="article.thumbnail"
|
|
class="aspect-video w-full rounded-xl border-[1px] border-solid border-button-border object-cover sm:rounded-2xl"
|
|
:alt="article.title"
|
|
/>
|
|
<div class="markdown-body" v-html="article.html" />
|
|
<h3
|
|
class="mb-0 mt-4 border-0 border-t-[1px] border-solid border-divider pt-4 text-base font-extrabold sm:text-lg"
|
|
>
|
|
Share this article
|
|
</h3>
|
|
<ShareArticleButtons :title="article.title" :url="articleUrl" />
|
|
</article>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.page {
|
|
> *:not(.full-width-bg),
|
|
> .full-width-bg > * {
|
|
max-width: 56rem;
|
|
margin-inline: auto;
|
|
}
|
|
}
|
|
|
|
.brand-gradient-bg {
|
|
background: var(--brand-gradient-bg);
|
|
border-color: var(--brand-gradient-border);
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.page {
|
|
padding-top: 1rem;
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
article {
|
|
gap: 1rem;
|
|
}
|
|
}
|
|
|
|
:deep(.markdown-body) {
|
|
h1,
|
|
h2 {
|
|
border-bottom: none;
|
|
padding: 0;
|
|
}
|
|
|
|
ul > li:not(:last-child) {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
ul,
|
|
ol {
|
|
p {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
}
|
|
|
|
ul,
|
|
ol {
|
|
strong {
|
|
color: var(--color-contrast);
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
|
|
h1,
|
|
h2,
|
|
h3 {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5rem;
|
|
@media (min-width: 640px) {
|
|
font-size: 2rem;
|
|
}
|
|
}
|
|
|
|
h2 {
|
|
font-size: 1.25rem;
|
|
@media (min-width: 640px) {
|
|
font-size: 1.5rem;
|
|
}
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1.125rem;
|
|
@media (min-width: 640px) {
|
|
font-size: 1.25rem;
|
|
}
|
|
}
|
|
|
|
p {
|
|
margin-bottom: 1.25rem;
|
|
font-size: 0.875rem;
|
|
@media (min-width: 640px) {
|
|
font-size: 1rem;
|
|
}
|
|
}
|
|
|
|
a {
|
|
color: var(--color-brand);
|
|
font-weight: 600;
|
|
|
|
&:hover {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
|
|
h1,
|
|
h2 {
|
|
a {
|
|
font-weight: 800;
|
|
}
|
|
}
|
|
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4,
|
|
h5,
|
|
h6 {
|
|
a {
|
|
color: var(--color-contrast);
|
|
}
|
|
}
|
|
|
|
img {
|
|
border: 1px solid var(--color-button-border);
|
|
border-radius: var(--radius-md);
|
|
@media (min-width: 640px) {
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
}
|
|
|
|
> img,
|
|
> :has(img:first-child:last-child) {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4,
|
|
h5,
|
|
h6 {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
p {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
ul,
|
|
ol {
|
|
padding-left: 1.25rem;
|
|
}
|
|
|
|
pre {
|
|
overflow-x: auto;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
table {
|
|
display: block;
|
|
overflow-x: auto;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
}
|
|
</style>
|