✨ Uses interactive to load content
This commit is contained in:
@@ -86,7 +86,7 @@ const items: MenuItem[] = [
|
||||
<div class="navbar-end">
|
||||
<label class="swap swap-rotate px-[16px]" data-toggle-theme="dark,light" data-act-class="swap-active">
|
||||
<svg
|
||||
class="swap-on fill-current w-8 h-8"
|
||||
class="swap-on fill-current w-6 h-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
@@ -97,7 +97,7 @@ const items: MenuItem[] = [
|
||||
>
|
||||
|
||||
<svg
|
||||
class="swap-off fill-current w-8 h-8"
|
||||
class="swap-off fill-current w-6 h-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
|
@@ -3,39 +3,47 @@ interface Props {
|
||||
posts: any[];
|
||||
}
|
||||
|
||||
import { POST_TYPES } from "../scripts/consts";
|
||||
|
||||
const { posts } = Astro.props;
|
||||
|
||||
function getThumbnail(item: any): string | null {
|
||||
for (const attachment of item?.attachments ?? []) {
|
||||
if (attachment.mimetype.startsWith("image")) {
|
||||
return attachment.external_url
|
||||
? attachment.external_url
|
||||
: `https://feed.smartsheep.studio/api/attachments/o/${attachment.file_id}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
---
|
||||
|
||||
<div class="grid justify-items-strench shadow-lg">
|
||||
{
|
||||
posts?.map((item) => (
|
||||
<a href={`/p/${item.slug}`}>
|
||||
<a href={`/p/${item.id}`}>
|
||||
<div class="card sm:card-side hover:bg-base-200 transition-colors sm:max-w-none">
|
||||
{item.cover.image.url && (
|
||||
{getThumbnail(item) && (
|
||||
<figure class="mx-auto w-full object-cover p-6 max-sm:pb-0 sm:max-w-[12rem] sm:pe-0">
|
||||
<img
|
||||
loading="lazy"
|
||||
src={item.cover.image.url}
|
||||
src={getThumbnail(item)}
|
||||
class="border-base-content bg-base-300 rounded-btn border border-opacity-5"
|
||||
alt={item.title}
|
||||
alt={item?.title}
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
<div class="card-body">
|
||||
<h2 class="text-xl">{item.title}</h2>
|
||||
<h2 class="text-xl">{item?.title}</h2>
|
||||
<div class="mx-[-2px] mt-[-4px]">
|
||||
<span class="badge badge-accent">{POST_TYPES[item.type]}</span>
|
||||
{item.categories?.map((category: any) => (
|
||||
{item?.categories?.map((category: any) => (
|
||||
<span class="badge badge-primary">{category.name}</span>
|
||||
))}
|
||||
{item.tags?.map((tag: any) => (
|
||||
{item?.tags?.map((tag: any) => (
|
||||
<span class="badge badge-secondary">{tag.name}</span>
|
||||
))}
|
||||
</div>
|
||||
<div class="text-xs opacity-60 line-clamp-3">
|
||||
{item.description}
|
||||
{item?.content?.substring(0, 160).replaceAll("#", "").replaceAll("*", "").trim() + "……"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
15
src/components/posts/Content.tsx
Normal file
15
src/components/posts/Content.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import parse from "html-react-parser";
|
||||
import mediumZoom from "medium-zoom";
|
||||
import DOMPurify from "dompurify";
|
||||
import * as marked from "marked";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Content({ content }: { content: string }) {
|
||||
useEffect(() => {
|
||||
mediumZoom(document.querySelectorAll(".post img"), {
|
||||
background: "var(--fallback-b1,oklch(var(--b1)/1))",
|
||||
});
|
||||
});
|
||||
|
||||
return <article className="prose max-w-none">{parse(DOMPurify.sanitize(marked.parse(content) as string))}</article>;
|
||||
}
|
@@ -5,7 +5,7 @@ import { useState, Fragment, useRef, useEffect } from "react";
|
||||
|
||||
import "aplayer/dist/APlayer.min.css";
|
||||
|
||||
function Video({ url, ...rest }: { url: string, className?: string }) {
|
||||
function Video({ url, ...rest }: { url: string; className?: string }) {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,39 +27,45 @@ function Video({ url, ...rest }: { url: string, className?: string }) {
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={container} {...rest}></div>
|
||||
);
|
||||
return <div ref={container} {...rest}></div>;
|
||||
}
|
||||
|
||||
function Audio({ url, artist, caption, ...rest }: {
|
||||
url: string,
|
||||
artist: string,
|
||||
caption: string,
|
||||
className?: string
|
||||
function Audio({
|
||||
url,
|
||||
artist,
|
||||
caption,
|
||||
...rest
|
||||
}: {
|
||||
url: string;
|
||||
artist: string;
|
||||
caption: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const container = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
new APlayer({
|
||||
container: container.current,
|
||||
audio: [{
|
||||
name: caption,
|
||||
artist: artist,
|
||||
url: url,
|
||||
theme: "#49509e"
|
||||
}]
|
||||
audio: [
|
||||
{
|
||||
name: caption,
|
||||
artist: artist,
|
||||
url: url,
|
||||
theme: "#49509e",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={container} {...rest}></div>
|
||||
);
|
||||
return <div ref={container} {...rest}></div>;
|
||||
}
|
||||
|
||||
export default function Media({ sources, author }: {
|
||||
sources: { caption: string; url: string; type: string }[],
|
||||
author?: { name: string }
|
||||
export default function Media({
|
||||
sources,
|
||||
author,
|
||||
}: {
|
||||
sources: { filename: string; mimetype: string }[];
|
||||
author?: { name: string };
|
||||
}) {
|
||||
const [focus, setFocus] = useState<boolean[]>(sources.map((_, idx) => idx === 0));
|
||||
|
||||
@@ -67,28 +73,32 @@ export default function Media({ sources, author }: {
|
||||
setFocus(focus.map((_, i) => i === idx));
|
||||
}
|
||||
|
||||
function getUrl(item: any) {
|
||||
return item.external_url ? item.external_url : `https://feed.smartsheep.studio/api/attachments/o/${item.file_id}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div role="tablist" className="tabs tabs-lifted">
|
||||
{sources.map((item, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<input
|
||||
type="radio"
|
||||
name={item.caption}
|
||||
name={item.filename}
|
||||
role="tab"
|
||||
className="tab"
|
||||
aria-label={item.caption}
|
||||
aria-label={item.filename}
|
||||
checked={focus[idx]}
|
||||
onChange={() => changeFocus(idx)}
|
||||
/>
|
||||
<div role="tabpanel" className="tab-content bg-base-100 border-base-300 rounded-box w-full">
|
||||
{item.type === "video" && (
|
||||
{item.mimetype === "video" && (
|
||||
<div className="w-full h-[460px]">
|
||||
<Video className="w-full h-full" url={item.url} />
|
||||
<Video className="w-full h-full" url={getUrl(item)} />
|
||||
</div>
|
||||
)}
|
||||
{item.type === "audio" && (
|
||||
{item.mimetype === "audio" && (
|
||||
<div className="w-full">
|
||||
<Audio url={item.url} artist={author?.name ?? "佚名"} caption={item.caption} />
|
||||
<Audio url={getUrl(item)} artist={author?.name ?? "佚名"} caption={item.filename} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import "../assets/fonts/fonts.css";
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
|
||||
import Navbar from "../components/Navbar.astro";
|
||||
import { ViewTransitions } from "astro:transitions";
|
||||
|
@@ -2,59 +2,42 @@
|
||||
import PageLayout from "../../layouts/PageLayout.astro";
|
||||
// @ts-ignore
|
||||
import Media from "../../components/posts/Media";
|
||||
|
||||
import { POST_TYPES } from "../../scripts/consts";
|
||||
import { graphQuery } from "../../scripts/requests";
|
||||
import { DocumentRenderer } from "@keystone-6/document-renderer";
|
||||
import { navigate } from "astro:transitions/client";
|
||||
import Content from "../../components/posts/Content";
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
const { post } = (
|
||||
await graphQuery(
|
||||
`query Query($where: PostWhereUniqueInput!) {
|
||||
post(where: $where) {
|
||||
slug
|
||||
type
|
||||
title
|
||||
description
|
||||
author {
|
||||
name
|
||||
}
|
||||
assets {
|
||||
caption
|
||||
url
|
||||
type
|
||||
}
|
||||
cover {
|
||||
image {
|
||||
url
|
||||
}
|
||||
}
|
||||
content {
|
||||
document
|
||||
}
|
||||
categories {
|
||||
slug
|
||||
name
|
||||
}
|
||||
tags {
|
||||
slug
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}`,
|
||||
{
|
||||
where: { slug },
|
||||
},
|
||||
)
|
||||
).data;
|
||||
const response = await fetch(`https://feed.smartsheep.studio/api/posts/${slug}`);
|
||||
const post = (await response.json())["data"];
|
||||
|
||||
if (!post) {
|
||||
return Astro.redirect("/404");
|
||||
} else if (post.realm_id != parseInt(process.env.PUBLIC_REALM_ID ?? "0")) {
|
||||
return Astro.redirect("https://feed.smartsheep.studio/posts/" + post.id);
|
||||
}
|
||||
|
||||
function getThumbnail(item: any): string | null {
|
||||
for (const attachment of item?.attachments ?? []) {
|
||||
if (attachment.mimetype.startsWith("image")) {
|
||||
return attachment.external_url
|
||||
? attachment.external_url
|
||||
: `https://feed.smartsheep.studio/api/attachments/o/${attachment.file_id}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAttachments(item: any): any[] {
|
||||
if (item?.attachments[0] && item?.attachments[0].mimetype.startsWith("image")) {
|
||||
return item?.attachments?.slice(1, item?.attachments?.length - 1) ?? [];
|
||||
} else {
|
||||
return item?.attachments;
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthorLink(user: any): string {
|
||||
return `https://feed.smartsheep.studio/accounts/${user.name}`;
|
||||
}
|
||||
---
|
||||
|
||||
@@ -62,28 +45,37 @@ if (!post) {
|
||||
<div class="wrapper">
|
||||
<div class="card w-full shadow-xl post">
|
||||
{
|
||||
post?.cover && (
|
||||
getThumbnail(post) && (
|
||||
<figure>
|
||||
<img src={post?.cover?.image?.url} alt={post?.title} />
|
||||
<img src={getThumbnail(post)} alt={post?.title} />
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{post?.title}</h2>
|
||||
<p class="description">{post?.description ?? "No description"}</p>
|
||||
<div class="divider"></div>
|
||||
<div class="mx-1 mb-5">
|
||||
<h2 class="card-title">{post?.title}</h2>
|
||||
<div class="text-sm flex max-lg:flex-col gap-x-4">
|
||||
<span>
|
||||
<i class="fa-solid fa-user me-1"></i>
|
||||
作者
|
||||
<a class="link" target="_blank" href={getAuthorLink(post?.author)}>{post?.author?.nick ?? "佚名"}</a>
|
||||
</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-calendar me-1"></i>
|
||||
发布于 {new Date(post?.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
post?.assets?.length > 0 && (
|
||||
<div class="mb-5 w-full">
|
||||
<Media client:only sources={post?.assets} author={post?.author} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="prose max-w-none">
|
||||
<DocumentRenderer document={post?.content?.document ?? []} />
|
||||
{
|
||||
getAttachments(post)?.length > 0 && (
|
||||
<div class="mb-5 w-full">
|
||||
<Media client:only sources={getAttachments(post)} author={post?.author} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<Content content={post?.content} client:only />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,12 +87,6 @@ if (!post) {
|
||||
<div>作者</div>
|
||||
<div>{post?.author?.name ?? "佚名"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>类型</div>
|
||||
<div class="text-accent">
|
||||
{POST_TYPES[post?.type as unknown as string]}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>分类</div>
|
||||
<div class="flex gap-1">
|
||||
@@ -136,12 +122,7 @@ if (!post) {
|
||||
</div>
|
||||
</PageLayout>
|
||||
|
||||
<script>
|
||||
import mediumZoom from "medium-zoom";
|
||||
mediumZoom(document.querySelectorAll(".post img"), {
|
||||
background: "var(--fallback-b1,oklch(var(--b1)/1))",
|
||||
});
|
||||
</script>
|
||||
<script></script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
|
@@ -2,54 +2,19 @@
|
||||
import PageLayout from "../../layouts/PageLayout.astro";
|
||||
import PostList from "../../components/PostList.astro";
|
||||
|
||||
import {graphQuery} from "../../scripts/requests";
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const {posts} = (
|
||||
await graphQuery(
|
||||
`query Query($where: PostWhereInput!, $orderBy: [PostOrderByInput!]!) {
|
||||
posts(where: $where, orderBy: $orderBy) {
|
||||
slug
|
||||
type
|
||||
title
|
||||
description
|
||||
cover {
|
||||
image {
|
||||
url
|
||||
}
|
||||
}
|
||||
content {
|
||||
document
|
||||
}
|
||||
categories {
|
||||
name
|
||||
}
|
||||
tags {
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}`,
|
||||
{
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: "desc",
|
||||
},
|
||||
],
|
||||
where: {}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
const response = await fetch(`https://feed.smartsheep.studio/api/posts?offset=0&take=10&realmId=${process.env.PUBLIC_REALM_ID}`);
|
||||
const posts = (await response.json())["data"];
|
||||
---
|
||||
|
||||
<PageLayout title="记录">
|
||||
<div class="max-w-[720px] mx-auto">
|
||||
<div class="pt-16 pb-6 px-6">
|
||||
<h1 class="text-4xl font-bold">记录</h1>
|
||||
<p class="pt-2">记录生活,记录理想,记录记录……</p>
|
||||
</div>
|
||||
|
||||
<PostList posts={posts as any[]}/>
|
||||
<div class="max-w-[720px] mx-auto">
|
||||
<div class="pt-16 pb-6 px-6">
|
||||
<h1 class="text-4xl font-bold">记录</h1>
|
||||
<p class="pt-2">记录生活,记录理想,记录记录……</p>
|
||||
</div>
|
||||
|
||||
<PostList posts={posts} />
|
||||
</div>
|
||||
</PageLayout>
|
||||
|
Reference in New Issue
Block a user