feat: add moments page (#112)

适配[瞬间插件](https://github.com/halo-sigs/plugin-moments)。

/kind feature
Fixes https://github.com/halo-dev/theme-earth/issues/70

<img width="1138" alt="image" src="https://github.com/halo-dev/theme-earth/assets/21301288/6fc9df4b-0fb3-4116-8cb9-2e02242acbcc">
<img width="1036" alt="image" src="https://github.com/halo-dev/theme-earth/assets/21301288/e4d3243b-e35c-41f5-a1d6-86c1ed9d8b85">

```release-note
适配瞬间插件。
```
This commit is contained in:
Ryan Wang
2023-10-12 15:52:30 +08:00
committed by GitHub
parent 98e1f89ba1
commit e3c2d95aa6
10 changed files with 234 additions and 19 deletions

View File

@@ -11,4 +11,7 @@ module.exports = {
env: {
node: true,
},
rules: {
"@typescript-eslint/ban-ts-comment": "off",
},
};

View File

@@ -55,6 +55,7 @@
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"release-it": "^15.11.0",
"sass": "^1.69.3",
"tailwindcss": "^3.3.2",
"tailwindcss-plugin-icons": "^2.1.1",
"typescript": "^4.9.5",

22
pnpm-lock.yaml generated
View File

@@ -67,6 +67,9 @@ devDependencies:
release-it:
specifier: ^15.11.0
version: 15.11.0
sass:
specifier: ^1.69.3
version: 1.69.3
tailwindcss:
specifier: ^3.3.2
version: 3.3.2
@@ -78,7 +81,7 @@ devDependencies:
version: 4.9.5
vite:
specifier: ^4.3.9
version: 4.3.9(@types/node@18.11.9)
version: 4.3.9(@types/node@18.11.9)(sass@1.69.3)
packages:
@@ -2113,6 +2116,10 @@ packages:
engines: {node: '>= 4'}
dev: true
/immutable@4.3.4:
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
dev: true
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -3459,6 +3466,16 @@ packages:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: true
/sass@1.69.3:
resolution: {integrity: sha512-X99+a2iGdXkdWn1akFPs0ZmelUzyAQfvqYc2P/MPTrJRuIRoTffGzT9W9nFqG00S+c8hXzVmgxhUuHFdrwxkhQ==}
engines: {node: '>=14.0.0'}
hasBin: true
dependencies:
chokidar: 3.5.3
immutable: 4.3.4
source-map-js: 1.0.2
dev: true
/semver-diff@4.0.0:
resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==}
engines: {node: '>=12'}
@@ -3908,7 +3925,7 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/vite@4.3.9(@types/node@18.11.9):
/vite@4.3.9(@types/node@18.11.9)(sass@1.69.3):
resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@@ -3937,6 +3954,7 @@ packages:
esbuild: 0.17.12
postcss: 8.4.24
rollup: 3.26.0
sass: 1.69.3
optionalDependencies:
fsevents: 2.3.2
dev: true

View File

@@ -1,14 +1,14 @@
interface PostUpvote {
interface upvoteState {
upvotedNames: string[];
init(): void;
upvoted(id: string): boolean;
handleUpvote(name: string): void;
}
export default (): PostUpvote => ({
export default (key: string, group: string, plural: string): upvoteState => ({
upvotedNames: [],
init() {
this.upvotedNames = JSON.parse(localStorage.getItem("halo.upvoted.post.names") || "[]");
this.upvotedNames = JSON.parse(localStorage.getItem(`halo.upvoted.${key}.names`) || "[]");
},
upvoted(id: string) {
return this.upvotedNames.includes(id);
@@ -25,24 +25,24 @@ export default (): PostUpvote => ({
xhr.onload = () => {
this.upvotedNames = [...this.upvotedNames, name];
localStorage.setItem("halo.upvoted.post.names", JSON.stringify(this.upvotedNames));
localStorage.setItem(`halo.upvoted.${key}.names`, JSON.stringify(this.upvotedNames));
const upvoteNode = document.querySelector('[data-upvote-post-name="' + name + '"]');
const upvoteNode = document.querySelector("[data-upvote-" + key + '-name="' + name + '"]');
if (!upvoteNode) {
return;
}
const upvoteCount = parseInt(upvoteNode.textContent || "0");
upvoteNode.textContent = upvoteCount + 1 + " 点赞";
upvoteNode.textContent = upvoteCount + 1 + "";
};
xhr.onerror = function () {
alert("网络请求失败,请稍后再试");
};
xhr.send(
JSON.stringify({
group: "content.halo.run",
plural: "posts",
group: group,
plural: plural,
name: name,
})
);

View File

@@ -1,16 +1,17 @@
import "./styles/tailwind.css";
import "./styles/main.css";
import "./styles/main.scss";
import Alpine from "alpinejs";
import * as tocbot from "tocbot";
import dropdown from "./alpine-data/dropdown";
import colorSchemeSwitcher from "./alpine-data/color-scheme-switcher";
import postUpvote from "./alpine-data/post-upvote";
import upvote from "./alpine-data/upvote";
window.Alpine = Alpine;
Alpine.data("dropdown", dropdown);
Alpine.data("colorSchemeSwitcher", colorSchemeSwitcher);
Alpine.data("postUpvote", postUpvote);
// @ts-ignore
Alpine.data("upvote", upvote);
Alpine.start();

View File

@@ -41,3 +41,12 @@ body {
.is-active-li {
@apply rounded bg-gray-100 dark:bg-slate-600;
}
.moment-content {
.tag {
&::before {
content: "#";
}
@apply rounded bg-gray-100 px-1 py-0.5 text-sm text-gray-900 hover:bg-gray-200 dark:bg-slate-600 dark:text-slate-50 dark:hover:bg-slate-700 dark:hover:text-slate-100;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

180
templates/moments.html Normal file
View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html
xmlns:th="https://www.thymeleaf.org"
th:replace="~{modules/layout :: html(title = '瞬间 - ' + ${site.title}, hero = null, content = ~{::content}, head = null, footer = null, sidebar = ~{::sidebar}, contentClass = '')}"
>
<th:block th:fragment="content">
<div class="rounded-xl bg-white p-4 dark:bg-slate-800">
<h1 class="mb-9 text-2xl font-medium dark:text-slate-50">瞬间</h1>
<div class="mb-8">
<nav class="flex flex-wrap gap-4" aria-label="Tabs">
<a
th:href="@{/moments}"
class="rounded bg-gray-100 px-1 py-0.5 text-sm text-gray-900 hover:bg-gray-200 dark:bg-slate-600 dark:text-slate-50 dark:hover:bg-slate-700 dark:hover:text-slate-100"
th:classappend="${#lists.isEmpty(param.tag) ? '!bg-gray-200 dark:!bg-slate-700 dark:!text-slate-100 ring-2 ring-gray-300 dark:ring-slate-600' : ''}"
>
<span>全部</span>
</a>
<a
th:each="tag : ${tags}"
th:href="@{|?tag=${tag.name}|}"
class="rounded bg-gray-100 px-1 py-0.5 text-sm text-gray-900 hover:bg-gray-200 dark:bg-slate-600 dark:text-slate-50 dark:hover:bg-slate-700 dark:hover:text-slate-100"
th:classappend="${#lists.contains(param.tag, tag.name) ? '!bg-gray-200 dark:!bg-slate-700 dark:!text-slate-100 ring-2 ring-gray-300 dark:ring-slate-600' : ''}"
>
<span th:text="|#${tag.name}|"></span>
<sup th:text="${tag.momentCount}"></sup>
</a>
</nav>
</div>
<div>
<ul
role="list"
class="divide-y divide-gray-100 dark:divide-slate-700"
x-data="upvote('moment','moment.halo.run','moments')"
>
<li
th:each="moment : ${moments.items}"
th:attr="x-data=|{name:'${moment.metadata.name}',showComment:false}|"
th:with="content=${moment.spec.content}"
class="animated fadeIn flex w-full items-start gap-2 py-5"
>
<img class="h-12 w-12 rounded-full" th:src="${moment.owner.avatar}" th:alt="${moment.owner.displayName}" />
<div class="ml-6" style="width: calc(100% - 4.75rem)">
<div
th:utext="${content.html}"
class="moment-content prose prose-base !max-w-none break-words dark:prose-invert prose-pre:p-0"
></div>
<div
th:unless="${#lists.isEmpty(moment.spec.content.medium)}"
class="moment-media mt-4 grid w-full grid-cols-3 gap-2 sm:w-1/2"
>
<div class="aspect-h-1 aspect-w-1" th:each="media : ${moment.spec.content.medium}">
<img
th:if="${#strings.equals(media.type,'PHOTO')}"
class="transform-gpu rounded-lg object-cover"
th:src="${media.url}"
/>
<div
th:if="${#strings.equals(media.type,'VIDEO')}"
x-data="{openVideoModal:false}"
class="aspect-h-1 aspect-w-1"
>
<video th:src="${media.url}" class="rounded-lg object-cover"></video>
<div
th:if="${#strings.equals(media.type,'VIDEO')}"
class="absolute flex h-full w-full cursor-pointer items-center justify-center rounded-lg bg-gray-50 opacity-50 transition-all hover:opacity-70"
@click="openVideoModal = true"
>
<i class="i-tabler-play !h-7 !w-7"></i>
</div>
<template x-teleport="body">
<div>
<div
class="fixed inset-0 z-50 bg-gray-800/40 opacity-100 backdrop-blur-sm"
aria-hidden="true"
x-show="openVideoModal"
x-transition:enter="ease-in-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in-out duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
></div>
<div
class="fixed inset-0 z-50 overflow-y-auto"
tabindex="-1"
x-show="openVideoModal"
x-transition:enter="ease-out duration-200"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div class="flex min-h-full flex-col items-center justify-center p-4 text-center sm:p-0">
<div
@click.outside="openVideoModal = false"
class="relative my-4 transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:w-full sm:max-w-xl"
>
<video th:src="${media.url}" class="w-full" controls></video>
</div>
<div>
<div
@click="openVideoModal = false"
class="group inline-flex items-center justify-center rounded-full bg-white p-1.5"
>
<i class="i-tabler-x block text-gray-600 group-hover:text-gray-900"></i>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<div class="mt-3 flex items-center gap-4">
<div
class="inline-flex cursor-pointer items-center text-sm text-gray-400 transition-all hover:text-red-700 dark:text-slate-400 dark:hover:text-red-700"
x-bind:class="{'!text-red-700': upvoted(name)}"
@click="handleUpvote(name)"
>
<i x-show="upvoted(name)" class="i-tabler-heart-filled h-3 w-3"></i>
<i x-show="!upvoted(name)" class="i-tabler-heart h-3 w-3"></i>
<span
class="ml-1"
th:attr="data-upvote-moment-name=${moment.metadata.name}"
th:text="${moment.stats.upvote}"
>
</span>
</div>
<div
class="inline-flex cursor-pointer items-center text-sm text-gray-400 transition-all hover:text-black dark:text-slate-400 dark:hover:text-slate-300"
:class="{'!text-black dark:!text-slate-300':showComment}"
x-on:click="showComment = !showComment"
>
<i class="i-tabler-message-circle h-3 w-3"></i>
<span class="ml-1" th:text="${moment.stats.approvedComment}"> </span>
</div>
<div class="inline-flex items-center text-sm text-gray-400 transition-all dark:text-slate-400">
<i class="i-tabler-calendar h-3 w-3"></i>
<span class="ml-1" th:text="${#dates.format(moment.spec.releaseTime,'yyyy-MM-dd')}"> </span>
</div>
</div>
<div class="mt-2" x-show="showComment">
<halo:comment
group="moment.halo.run"
kind="Moment"
th:attr="name=${moment.metadata.name}"
colorScheme="window.main.currentColorScheme"
/>
</div>
</div>
</li>
</ul>
</div>
<div class="mt-6 flex items-center justify-between" th:if="${moments.hasPrevious() || moments.hasNext()}">
<a
th:href="@{${moments.prevUrl}}"
class="whitespace-no-wrap group inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-4 py-1 text-sm font-medium leading-6 text-gray-600 shadow-sm hover:bg-gray-50 focus:shadow-none focus:outline-none dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100 dark:hover:bg-slate-600 dark:hover:text-white"
>
<span class="i-tabler-arrow-left text-lg transition-all group-hover:-translate-x-1"></span>
<span>上一页</span>
</a>
<span
class="text-sm text-gray-900 dark:text-slate-50"
th:text="|${moments.page} / ${moments.totalPages}|"
></span>
<a
th:href="@{${moments.nextUrl}}"
class="whitespace-no-wrap group inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-4 py-1 text-sm font-medium leading-6 text-gray-600 shadow-sm hover:bg-gray-50 focus:shadow-none focus:outline-none dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100 dark:hover:bg-slate-600 dark:hover:text-white"
>
<span>下一页</span>
<span class="i-tabler-arrow-right text-lg transition-all group-hover:translate-x-1"></span>
</a>
</div>
</div>
</th:block>
</html>

View File

@@ -28,7 +28,7 @@
<th:block th:replace="~{modules/sidebar :: sidebar(prepend = ~{::sidebar_prepend})}"></th:block>
</th:block>
<th:block th:fragment="content">
<div x-data="postUpvote" class="rounded-xl bg-white p-4 dark:bg-slate-800">
<div x-data="upvote('post','content.halo.run','posts')" class="rounded-xl bg-white p-4 dark:bg-slate-800">
<div th:attr="x-data=|{name:'${post.metadata.name}'}|" class="flex items-center justify-between">
<div class="inline-flex items-center justify-start gap-2">
<a th:href="@{${post.owner.permalink}}" th:title="${post.owner.displayName}">
@@ -53,8 +53,11 @@
<span class="text-gray-300 dark:text-slate-200">/</span>
<span th:text="|${post.stats?.comment ?:0} 评论|"> </span>
<span class="text-gray-300 dark:text-slate-200">/</span>
<span th:attr="data-upvote-post-name=${post.metadata.name}" th:text="|${post.stats?.upvote ?:0} 点赞|">
</span>
<div>
<span th:attr="data-upvote-post-name=${post.metadata.name}" th:text="|${post.stats?.upvote ?:0}|">
</span>
<span> 点赞 </span>
</div>
</div>
</div>
</div>