整合评论

123

abc

sdf

wec

  1. 如果“评论”作为一个独立的项目。使用自己的依赖。

git clone --depth=1 -b ghost-5.116.2-next git@github.com:aidabo/Ghost.git ghostxx

cd .\ghostxx\apps\comments-ui\

// 似乎不能将这个项目单独使用。它好像依赖外部整个项目。
yarn

yarn build

此时,主要观察umd文件夹里生成了umd格式的js,可以通过script标签使用。js内容里包含react本体等。

  1. 修改它,让其可以通过import来使用。

//修改 vite.config.ts (修改入口,格式,除外的东西等)

    build: {
        reportCompressedSize: false,
         outDir: resolve(__dirname, 'dist'), // 改成通用输出目录
        emptyOutDir: true,
        minify: true,
        sourcemap: true,
        cssCodeSplit: true,
        lib: {
            entry: resolve(__dirname, 'src/App.tsx'),
            formats: ['es', 'cjs'],
            name: pkg.name,
            fileName(format) {
                if (format === 'umd') {
                    return `${outputFileName}.min.js`;
                }

                return `${outputFileName}.js`;
            }
        },
        rollupOptions: {
            external: [
                'react',
                'react-dom',
                'react/jsx-runtime'
            ],
            output: {
            }
        },
        commonjsOptions: {
            include: [/ghost/, /node_modules/],
            dynamicRequireRoot: '../../',
            dynamicRequireTargets: SUPPORTED_LOCALES.map(locale => `../../ghost/i18n/locales/${locale}/comments.json`)
        }
    },

11111

  1. 改package.json

"module": "./dist/comments-ui.js",
"exports": {
".": {
"import": "./dist/comments-ui.js"
}
},

  1. 使得这个项目可以被我们的项目参照。

yarn link

还修改文件。src\utils\options.ts 因为这样就不能像以前一样在script里传参了,先写上。

import React, {useMemo} from 'react';
import {CommentsOptions} from '../AppContext';

export function useOptions(scriptTag: HTMLElement) {
const buildOptions = React.useCallback(() => {
/**
* @type {HTMLElement}
*
* 哦,大概是这样的。
*/
const siteUrl = "http://localhost:3000/";
const apiKey = "cedbaa7390f9be0dc28f3c03ed";
const apiUrl = "http://localhost:3000/ghost/api/content/";
const adminUrl = "https://ghost2.240302.xyz/ghost/";
const postId = '689af6d105e5cd0001d0a515';
const colorScheme = "auto";
const avatarSaturation = undefined;
const accentColor = '#000000';
const commentsEnabled = "all";
const title = null; // Null means use the default title. Missing = no title.
const showCount = true;
const publication = 'jibun-see'; // TODO: replace with dynamic data from script
const locale = 'en';

    const options = {locale, siteUrl, apiKey, apiUrl, postId, adminUrl, colorScheme, avatarSaturation, accentColor, commentsEnabled, title, showCount, publication};
    return options;
}, [scriptTag]);

const initialOptions = useMemo(() => buildOptions(), []);
const [options, setOptions] = React.useState<CommentsOptions>(initialOptions);

React.useEffect(() => {
    const observer = new MutationObserver((mutationList) => {
        if (mutationList.some(mutation => mutation.type === 'attributes')) {
            setOptions(buildOptions());
        }
    });

    // observer.observe(scriptTag, {
    //     attributes: true
    // });

    return () => {
        observer.disconnect();
    };
}, [scriptTag, buildOptions]);

return options;

}

下面开始修改咱们项目本体。

首先link

yarn link "@tryghost/comments-ui"

然后在画面上引入。

先建一个组件。Testtest.tsx

import {App} from "@tryghost/comments-ui";

export default function Testtest() {
return (

); }

在Top里引入。

"use client";
import dynamic from "next/dynamic";
import { Box, useTheme, useMediaQuery } from "@mui/material";
import TopBubble from "./TopBubble";
import { useState, useEffect, useRef, useCallback } from "react";

const Masonry = dynamic(() => import("@mui/lab/Masonry"), { ssr: false });
// const App = dynamic(() => import("@tryghost/comments-ui"), {
// ssr: false,
// });
const Testtest = dynamic(() => import("@/components/Testtest"), { ssr: false });

export default function Top() {
const theme = useTheme();
const hasFetchedInitial = useRef(false);
const isSmDown = useMediaQuery(theme.breakpoints.down("sm"));
const [bubbles, setBubbles] = useState<{
pc: any[];
tel: any[][];
}>({
pc: [],
tel: [[]],
});
const [excludeIds, setExcludeIds] = useState<string[]>([]);
const [totalPosts, setTotalPosts] = useState<number | null>(null);
const loadingRef = useRef(false);
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);

const PCbubblesTemplate = [
{ size: 212, text: "" },
{ size: 110, text: "" },
{ size: 238, text: "" },
{ size: 150, text: "" },
{ size: 212, text: "" },
{ size: 251, text: "" },
{ size: 110, text: "" },
{ size: 260, text: "" },
{ size: 212, text: "" },
{ size: 150, text: "" },
{ size: 260, text: "" },
{ size: 150, text: "" },
{ size: 238, text: "" },
{ size: 150, text: "" },
{ size: 250, text: "" },
{ size: 150, text: "" },
{ size: 150, text: "" },
{ size: 150, text: "" },
];

const TELbubblesTemplate = [
{ size: 200, text: "" },
{ size: 150, text: "", class: "h-[130px] ml-20" },
{ size: 238, text: "", class: "-ml-14" },
{ size: 110, text: "", class: "h-[80px]" },
{ size: 260, text: "" },
{ size: 212, text: "", class: "h-[250px] mt-[150px] -ml-4" },
{ size: 150, text: "", class: "h-[150px]" },
{ size: 110, text: "", class: "h-[190px] ml-[80px]" },
{ size: 251, text: "" },
{ size: 212, text: "", class: "h-[200px] -ml-4" },
{ size: 150, text: "" },
{ size: 150, text: "", class: "ml-16" },
{ size: 260, text: "" },
{ size: 150, text: "", class: "h-[270px] mt-[100px] ml-[50px]" },
{ size: 238, text: "", class: "h-[400px] -mt-10" },
{ size: 250, text: "", class: "h-[220px] -ml-[50px]" },
{ size: 180, text: "" },
{ size: 150, text: "", class: "ml-16" },
];

const fetchPosts = useCallback(
async (isLoadMore = false) => {
if (loadingRef.current) return;
if (isLoadMore && !isSmDown) return;
if (totalPosts !== null && excludeIds.length >= totalPosts) return;

  loadingRef.current = true;

  try {
    const response = await fetch("/api/posts/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        limit: isLoadMore ? 18 : 10000,
        excludeIds: isLoadMore ? excludeIds : [],
      }),
    });
    const result = await response.json();

    if (!response.ok) throw new Error(result.error);

    const shuffledPosts = result.data;

    const newPCBubbles = shuffledPosts.map((post: any, index: number) => {
      const template = PCbubblesTemplate[index % PCbubblesTemplate.length];
      return {
        ...template,
        src: post?.src,
        text: post?.text,
        id: post?.id,
      };
    });

    const newTELBubbles = TELbubblesTemplate.slice(
      0,
      Math.min(shuffledPosts.length, TELbubblesTemplate.length)
    ).map((template: any, index: number) => ({
      ...template,
      src: shuffledPosts[index]?.src,
      text: shuffledPosts[index]?.text,
      id: shuffledPosts[index]?.id,
    }));

    setBubbles((prev) => ({
      pc: isLoadMore ? prev.pc : newPCBubbles,
      tel: isLoadMore ? [...prev.tel, newTELBubbles] : [[...newTELBubbles]],
    }));

    setExcludeIds((prev) =>
      isLoadMore
        ? result.meta.excludeIds
        : shuffledPosts
            .slice(0, TELbubblesTemplate.length)
            .map((post: any) => post.id)
    );
    setTotalPosts(result.meta.total);
  } catch (error) {
    console.error("Top -> fetchPosts:", error);
    if (!isLoadMore) setBubbles({ pc: [], tel: [[]] });
  } finally {
    loadingRef.current = false;
  }
},
[isSmDown, excludeIds, totalPosts, PCbubblesTemplate, TELbubblesTemplate]

);

useEffect(() => {
if (hasFetchedInitial.current) return;
const timer = setTimeout(() => {
fetchPosts(false);
hasFetchedInitial.current = true;
}, 0);
return () => clearTimeout(timer);
}, [fetchPosts, isSmDown]);

useEffect(() => {
if (!isSmDown) return;

const handleScroll = () => {
  const scrollHeight = document.documentElement.scrollHeight;
  const scrollTop = document.documentElement.scrollTop;
  const clientHeight = document.documentElement.clientHeight;
  const distanceToBottom = scrollHeight - scrollTop - clientHeight;

  if (distanceToBottom < 2000) {
    if (debounceTimeoutRef.current) {
      clearTimeout(debounceTimeoutRef.current);
    }
    debounceTimeoutRef.current = setTimeout(() => {
      if (!loadingRef.current) {
        fetchPosts(true);
      }
    }, 500);
  }
};

window.addEventListener("scroll", handleScroll);
return () => {
  window.removeEventListener("scroll", handleScroll);
  if (debounceTimeoutRef.current) {
    clearTimeout(debounceTimeoutRef.current);
  }
};

}, [isSmDown, fetchPosts]);

return (





<div
style={{
position: "absolute",
top: "20lvh",
left: "50%",
transform: "translateX(-50%)",
width: "90%",
maxWidth: "450px",
zIndex: 30,
}}
>
<img
src="/images/jiBUN-See.png"
alt="JiBUN"
style={{ width: "100%", height: "auto" }}
/>

  {!isSmDown ? (
    <Masonry
      columns={{ sm: 3, md: 4, lg: 5, xl: 6 }}
      spacing={4}
      sx={{ pt: 3 }}
    >
      {bubbles.pc.map((bubble: any, index: number) => (
        <Box
          key={index}
          className={`item ${bubble.class || ""}`}
          sx={{ mb: 5 }}
        >
          <TopBubble
            id={bubble.id}
            src={bubble.src}
            size={bubble.size}
            text={bubble.text}
            className="top-bubble"
          />
        </Box>
      ))}
    </Masonry>
  ) : (
    <Box sx={{ width: "100%" }}>
      {bubbles.tel.map((pageBubbles, pageIndex) => (
        <Masonry
          key={pageIndex}
          columns={2}
          spacing={4}
          sx={{ pt: 3, mb: -8, width: "100%" }}
        >
          {pageBubbles.map((bubble: any, index: number) => (
            <Box
              key={`${pageIndex}-${index}`}
              className={`item ${bubble.class || ""}`}
              sx={{ mb: 5 }}
            >
              <TopBubble
                id={bubble.id}
                src={bubble.src}
                size={bubble.size}
                text={bubble.text}
                className="top-bubble"
              />
            </Box>
          ))}
        </Masonry>
      ))}
    </Box>
  )}
</Box>
</div>

);
}

最后在next.config里加一个转发规则。

async rewrites() {
return [
{
source: "/ghost/api/:path*/",
destination: ${process.env.GHOST_ADMIN_API_URL}/ghost/api/:path*/,
},
{
source: "/members/api/:path*/",
destination: ${process.env.GHOST_ADMIN_API_URL}/members/api/:path*/,
}
];
},

到处有很多警告,还没一个一个调查。