Uses interactive to load content

This commit is contained in:
2024-02-10 01:31:38 +08:00
parent 8b6c03f020
commit bb65c2a507
9 changed files with 251 additions and 163 deletions

View File

@@ -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"
>

View File

@@ -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>

View 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>;
}

View File

@@ -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>

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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>