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)
+ })
+}