diff --git a/bun.lockb b/bun.lockb index 26da72f..0c00167 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index fd86c75..12fb9d3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@mui/material": "^6.3.0", "@mui/material-nextjs": "^6.3.1", "@mui/x-charts": "^7.23.2", + "@tailwindcss/typography": "^0.5.15", "axios": "^1.7.9", "axios-case-converter": "^1.1.1", "cookies-next": "^5.0.2", @@ -24,6 +25,11 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", + "unified": "^11.0.5", "zustand": "^5.0.2" }, "devDependencies": { diff --git a/src/components/CapAppBar.tsx b/src/components/CapAppBar.tsx index 6665098..c600af6 100644 --- a/src/components/CapAppBar.tsx +++ b/src/components/CapAppBar.tsx @@ -41,7 +41,7 @@ function ElevationScroll(props: ElevationAppBarProps) { ? React.cloneElement(props.children, { elevation: trigger ? props.elevation : 0, sx: trigger - ? { backgroundColor: props.color, ...commonStyle } + ? { backgroundColor: props.color, color: 'black', ...commonStyle } : { backgroundColor: 'transparent', ...commonStyle }, }) : null diff --git a/src/pages/posts/[...id].tsx b/src/pages/posts/[...id].tsx new file mode 100644 index 0000000..dfa4300 --- /dev/null +++ b/src/pages/posts/[...id].tsx @@ -0,0 +1,106 @@ +import { getAttachmentUrl, sni } from '@/services/network' +import { SnPost } from '@/services/post' +import { Alert, AlertTitle, Avatar, Box, Collapse, Container, IconButton, Link, Typography } from '@mui/material' +import { GetServerSideProps, InferGetServerSidePropsType } from 'next' +import { useEffect, useMemo, useState } from 'react' +import { unified } from 'unified' +import Image from 'next/image' +import rehypeSanitize from 'rehype-sanitize' +import rehypeStringify from 'rehype-stringify' +import remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' + +import CloseIcon from '@mui/icons-material/Close' + +export const getServerSideProps = (async (context) => { + const id = context.params!.id as string[] + try { + const { data: post } = await sni.get('/cgi/co/posts/' + id.join(':')) + if (post.body.content) { + const out = await unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeSanitize) + .use(rehypeStringify) + .process(post.body.content) + post.body.content = String(out) + } + return { props: { post } } + } catch (err) { + return { + notFound: true, + } + } +}) satisfies GetServerSideProps<{ post: SnPost }> + +export default function Post({ post }: InferGetServerSidePropsType) { + const link = useMemo(() => `https://sn.solsynth.dev/posts/${post.id}`, [post]) + + const [openAppHint, setOpenAppHint] = useState() + + useEffect(() => { + if (!localStorage.getItem('sol_hide_app_hint')) { + setOpenAppHint(true) + } + }, []) + + useEffect(() => { + if (openAppHint === false) { + localStorage.setItem('sol_hide_app_hint', 'yes') + } + }, [openAppHint]) + + return ( + <> + + { + setOpenAppHint(false) + }} + > + + + } + > + Open in Solian + All feature supported, cross-platform, the official app of Solar Network.{' '} + + Launch + + + + + {post.body.thumbnail && ( + + post thumbnail + + )} + + + + + + + {post.publisher.nick} + + @{post.publisher.name} + + + + + + + {post.body.content &&
} + + + + ) +} diff --git a/src/services/attachment.ts b/src/services/attachment.ts new file mode 100644 index 0000000..31f14ac --- /dev/null +++ b/src/services/attachment.ts @@ -0,0 +1,48 @@ +import { sni } from './network' + +export interface SnAttachment { + id: number + createdAt: Date + updatedAt: Date + deletedAt?: Date | null + rid: string + uuid: string + size: number + name: string + alt: string + mimetype: string + hash: string + destination: number + refCount: number + contentRating: number + qualityRating: number + cleanedAt?: Date | null + isAnalyzed: boolean + isSelfRef: boolean + isIndexable: boolean + ref?: SnAttachment | null + refId?: number | null + poolId?: number | null + accountId: number + thumbnailId?: number | null + thumbnail?: SnAttachment | null + compressedId?: number | null + compressed?: SnAttachment | null + usermeta: Record + metadata: Record +} + +async function getAttachment(id: string | number): Promise { + const resp = await sni.get('/cgi/uc/attachments/' + id + '/meta') + return resp.data +} + +async function listAttachment(id: string[]): Promise { + const resp = await sni.get<{ data: SnAttachment[] }>('/cgi/uc/attachments', { + params: { + id: id.join(','), + take: id.length, + }, + }) + return resp.data.data +} diff --git a/src/services/post.ts b/src/services/post.ts new file mode 100644 index 0000000..85e0e48 --- /dev/null +++ b/src/services/post.ts @@ -0,0 +1,85 @@ +export interface SnPost { + id: number + createdAt: Date + updatedAt: Date + deletedAt?: Date | null + type: string + body: SnPostBody & Record + language: string + alias?: string | null + aliasPrefix?: string | null + tags: SnPostTag[] + categories: SnPostCategory[] + replies?: SnPost[] | null + replyId?: number | null + repostId?: number | null + replyTo?: SnPost | null + repostTo?: SnPost | null + visibleUsersList?: number[] | null + invisibleUsersList?: number[] | null + visibility: number + editedAt?: Date | null + pinnedAt?: Date | null + lockedAt?: Date | null + isDraft: boolean + publishedAt?: Date | null + publishedUntil?: Date | null + totalUpvote: number + totalDownvote: number + publisherId: number + publisher: SnPublisher + metric: SnMetric +} + +export interface SnPostTag { + id: number + createdAt: Date + updatedAt: Date + deletedAt?: Date + alias: string + name: string + description: string + posts?: SnPost[] +} + +export interface SnPostCategory { + id: number + createdAt: Date + updatedAt: Date + deletedAt?: Date + alias: string + name: string + description: string + posts?: SnPost[] +} + +export interface SnPostBody { + attachments: string[] + content: string + location?: string + thumbnail?: string + title?: string +} + +export interface SnMetric { + replyCount: number + reactionCount: number + reactionList: Record +} + +export interface SnPublisher { + id: number + createdAt: Date + updatedAt: Date + deletedAt?: Date | null + type: number + name: string + nick: string + description: string + avatar: string + banner: string + totalUpvote: number + totalDownvote: number + realmId?: number | null + accountId: number +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 109807b..4834e9c 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,18 +1,18 @@ -import type { Config } from "tailwindcss"; +import type { Config } from 'tailwindcss' export default { content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { - background: "var(--background)", - foreground: "var(--foreground)", + background: 'var(--background)', + foreground: 'var(--foreground)', }, }, }, - plugins: [], -} satisfies Config; + plugins: [require('@tailwindcss/typography')], +} satisfies Config