diff --git a/src/app/[locale]/dashboard/[subdomain]/page-component.tsx b/src/app/[locale]/dashboard/[subdomain]/page-component.tsx index b9198119..5a7e7ba2 100644 --- a/src/app/[locale]/dashboard/[subdomain]/page-component.tsx +++ b/src/app/[locale]/dashboard/[subdomain]/page-component.tsx @@ -4,6 +4,7 @@ import { useTranslations } from "next-intl" import { useParams } from "next/navigation" import { CharacterFloatCard } from "~/components/common/CharacterFloatCard" +import Heatmap from "~/components/common/Heatmap" import { DashboardMain } from "~/components/dashboard/DashboardMain" import { Image } from "~/components/ui/Image" import { UniLink } from "~/components/ui/UniLink" @@ -132,6 +133,7 @@ export default function SubdomainIndex() { ))} +
{t.rich("hello.welcome", { p: (chunks) =>

{chunks}

, @@ -205,8 +207,9 @@ export default function SubdomainIndex() {
{pages.data?.pages[0]?.list.map((item) => ( diff --git a/src/components/common/Heatmap.tsx b/src/components/common/Heatmap.tsx new file mode 100644 index 00000000..8bb987e5 --- /dev/null +++ b/src/components/common/Heatmap.tsx @@ -0,0 +1,62 @@ +"use client" + +import dayjs from "dayjs" +import advancedFormat from "dayjs/plugin/advancedFormat" +import { useTranslations } from "next-intl" + +import { Tooltip } from "~/components/ui/Tooltip" +import { useCalendar } from "~/queries/page" + +dayjs.extend(advancedFormat) + +export default function CalHeatmap({ characterId }: { characterId?: number }) { + const calendar = useCalendar(characterId) + const t = useTranslations() + + return ( +
+
+ {calendar.data?.calendar.map((week: any, index: number) => ( +
+ {week.map((day: any) => ( + +

+ {day.count} {t("on-chain posting on")}{" "} + {dayjs(day.day).format("MMM DD, YYYY")} +

+ {(() => + day.titles.map((title: string) => ( +

+ {title} +

+ )))()} + + } + placement="top" + className="z-10" + > +
0 && day.count < 2 + ? "bg-green-300" + : day.count >= 2 && day.count < 5 + ? "bg-green-400" + : day.count >= 5 && day.count < 10 + ? "bg-green-500" + : "bg-green-600") + } + >
+
+ ))} +
+ ))} +
+
+ ) +} diff --git a/src/messages/zh.json b/src/messages/zh.json index 6be0acec..bfad9a1c 100644 --- a/src/messages/zh.json +++ b/src/messages/zh.json @@ -432,5 +432,6 @@ "zh": "中文", "zh-TW": "繁体中文", "Footer": "页脚", - "Support Markdown": "支持 Markdown" + "Support Markdown": "支持 Markdown", + "on-chain posting on": "条链上发布于" } diff --git a/src/models/page.model.ts b/src/models/page.model.ts index 431a60b3..be2cb34c 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -6,6 +6,9 @@ import type { NoteEntity, NoteMetadata, } from "crossbell" +import dayjs from "dayjs" +import advancedFormat from "dayjs/plugin/advancedFormat" +import weekOfYear from "dayjs/plugin/weekOfYear" import { type Address } from "viem" import { GeneralAccount } from "@crossbell/connect-kit" @@ -29,6 +32,9 @@ import { } from "~/lib/types" import { client } from "~/queries/graphql" +dayjs.extend(weekOfYear) +dayjs.extend(advancedFormat) + export const PINNED_PAGE_KEY = "xlog_pinned_page" export async function checkPageSlug(input: { @@ -296,8 +302,9 @@ export async function getPagesBySite(input: { cursor: data?.notes?.length < limit ? null - : `${input.characterId}_${data?.notes?.[data?.notes?.length - 1] - ?.noteId}`, + : `${input.characterId}_${ + data?.notes?.[data?.notes?.length - 1]?.noteId + }`, } const expandedNotes = { @@ -384,6 +391,114 @@ export async function getPagesBySite(input: { return expandedNotes } +export async function getCalendar(characterId?: number) { + if (!characterId) { + return { + calendar: [], + } + } + + const format = (day: dayjs.Dayjs) => { + const ww = day.format("ww") + const mm = day.format("MM") + if (ww === "01" && mm !== "01") { + return `${day.year() + 1}-${ww}` + } else { + return `${day.year()}-${ww}` + } + } + + const calendarLength = 370 + const getCalendarTemp = () => { + const calendar: { + [key: string]: { + day: dayjs.Dayjs + count: number + titles: string[] + }[] + } = {} + for (let i = calendarLength - 1; i >= 0; i--) { + const day = dayjs().subtract(i, "day") + let week = format(day) + if (!calendar[week]) { + calendar[week] = [] + } + calendar[week].push({ + day: day, + count: 0, + titles: [], + }) + } + return calendar + } + + const currentDate = new Date() + currentDate.setUTCHours(0, 0, 0, 0) + currentDate.setUTCDate(currentDate.getUTCDate() - 370) + const utcString = currentDate.toISOString() + + const { data } = await client + .query( + gql` + query getNotes($characterId: Int, $limit: Int, $utcString: DateTime) { + notes( + where: { + characterId: { equals: $characterId } + createdAt: { gte: $utcString } + metadata: { content: { path: "sources", array_contains: "xlog" } } + } + take: $limit + ) { + createdAt + metadata { + content + } + } + } + `, + { + characterId, + limit: 1000, + utcString, + }, + ) + .toPromise() + + let response = { + calendar: getCalendarTemp(), + } + + for (let i = 0; i < data.notes.length; i++) { + const day = dayjs(data.notes[i].createdAt) + let week = format(day) + const today = response.calendar[week].find((item: any) => + item.day.isSame(day, "day"), + ) + if (today) { + today.count++ + today.titles.push( + ( + data.notes[i].metadata.content.title || + data.notes[i].metadata.content.content + ).slice(0, 20), + ) + } else { + console.warn("not found", day) + } + } + + return { + calendar: Object.keys(response.calendar) + .sort() + .map((key) => + response.calendar[key].map((item: any) => { + item.day = item.day.valueOf() + return item + }), + ), + } +} + export async function getSearchPagesBySite(input: { characterId?: number keyword?: string diff --git a/src/queries/page.ts b/src/queries/page.ts index bff45685..dccbb630 100644 --- a/src/queries/page.ts +++ b/src/queries/page.ts @@ -708,3 +708,12 @@ export const usePinnedPage = ({ return { isLoading, noteId: data as number } } + +export const useCalendar = (characterId?: number) => { + return useQuery(["getCalendar", characterId], async () => { + if (!characterId) { + return + } + return pageModel.getCalendar(characterId) + }) +}