Skip to content

Commit d44682e

Browse files
committed
feat: add CommentMarkdown component for rendering markdown with user mentions
- Introduced CommentMarkdown component to handle markdown rendering with user mentions. - Updated AccountOverviewPage to use CommentMarkdown for comment body rendering. - Updated DownloadsPage to render game comments using CommentMarkdown. - Updated GameDetailPage to display game comments with CommentMarkdown. - Refactored GameComments component to utilize CommentMarkdown for comment body.
1 parent 7a418f8 commit d44682e

7 files changed

Lines changed: 958 additions & 55 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@
5454
"lucide-react": "^0.454.0",
5555
"react": "19.2.3",
5656
"react-dom": "19.2.3",
57+
"react-markdown": "^10.1.0",
5758
"react-router-dom": "^7.0.2",
59+
"remark-gfm": "^4.0.1",
5860
"tailwind-merge": "^2.5.5"
5961
},
6062
"devDependencies": {

pnpm-lock.yaml

Lines changed: 860 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

renderer/src/app/pages/AccountOverviewPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"
22
import { useNavigate } from "react-router-dom"
33
import { Button } from "@/components/ui/button"
44
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
5+
import { CommentMarkdown } from "@/components/CommentMarkdown"
56
import { Skeleton } from "@/components/ui/skeleton"
67
import { MyRequests } from "@/components/MyRequests"
78
import { apiFetch, apiUrl, getApiBaseUrl } from "@/lib/api"
@@ -378,7 +379,7 @@ export function AccountOverviewPage() {
378379
{new Date(comment.createdAt).toLocaleDateString()}
379380
</span>
380381
</p>
381-
<p className="text-sm text-zinc-100 mt-2 line-clamp-3">{comment.body}</p>
382+
<CommentMarkdown text={comment.body} className="mt-2 max-h-24 overflow-hidden text-zinc-100" />
382383
</div>
383384
))}
384385
</div>

renderer/src/app/pages/DownloadsPage.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { AlertTriangle, Download, HardDrive, PauseCircle, Play, XCircle, Square
1111
import { ExePickerModal } from "@/components/ExePickerModal"
1212
import { DesktopShortcutModal } from "@/components/DesktopShortcutModal"
1313
import { GameLaunchPreflightModal, type LaunchPreflightResult } from "@/components/GameLaunchPreflightModal"
14+
import { CommentMarkdown } from "@/components/CommentMarkdown"
1415
import { gameLogger } from "@/lib/logger"
1516

1617
function formatBytes(bytes: number) {
@@ -1527,9 +1528,10 @@ export function DownloadsPage() {
15271528
<span>{finishedAt ? new Date(finishedAt).toLocaleDateString() : "Completed"}</span>
15281529
</div>
15291530
{game?.comment && (
1530-
<p className="mt-1.5 max-w-md text-[11px] text-amber-400/80">
1531-
Note: {game.comment}
1532-
</p>
1531+
<div className="mt-1.5 max-w-md text-[11px] text-amber-400/80">
1532+
<strong className="text-amber-300">Note:</strong>
1533+
<CommentMarkdown text={game.comment} className="mt-1 text-[11px] text-amber-400/80" />
1534+
</div>
15331535
)}
15341536
</div>
15351537
</div>

renderer/src/app/pages/GameDetailPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
77
import { Input } from "@/components/ui/input"
88
import { GameCard } from "@/components/GameCard"
99
import { GameComments } from "@/components/GameComments"
10+
import { CommentMarkdown } from "@/components/CommentMarkdown"
1011
import { useDownloads } from "@/context/downloads-context"
1112
import { apiUrl, apiFetch } from "@/lib/api"
1213
import { getPreferredDownloadHost, setPreferredDownloadHost, requestDownloadToken, type PreferredDownloadHost, type DownloadConfig } from "@/lib/downloads"
@@ -1363,7 +1364,7 @@ export function GameDetailPage() {
13631364
</div>
13641365
<div>
13651366
<h3 className={`font-bold mb-1 ${game.hasHv ? 'text-red-300' : 'text-white'}`}>Important Note</h3>
1366-
<p className={`text-sm font-medium leading-relaxed ${game.hasHv ? 'text-red-200' : 'text-zinc-300'}`}>{game.comment}</p>
1367+
<CommentMarkdown text={game.comment} className={`text-sm font-medium ${game.hasHv ? 'text-red-200' : 'text-zinc-300'}`} />
13671368
</div>
13681369
</div>
13691370
</div>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { ReactNode } from "react"
2+
import ReactMarkdown from "react-markdown"
3+
import remarkGfm from "remark-gfm"
4+
import { Link } from "react-router-dom"
5+
6+
const USER_MENTION_RE = /(^|[^\w/])@([A-Za-z0-9_.-]{2,32})(?=$|[^\w.-])/g
7+
8+
function linkifyUserMentions(text: string): string {
9+
return String(text || "").replace(USER_MENTION_RE, (match, prefix, username) => {
10+
if (!username) return match
11+
return `${prefix}[@${username}](/user/${encodeURIComponent(username)})`
12+
})
13+
}
14+
15+
const markdownComponents = {
16+
h1: ({ children }: { children?: ReactNode }) => (
17+
<h1 className="mt-4 mb-2 text-xl font-black leading-tight first:mt-0 sm:text-2xl">{children}</h1>
18+
),
19+
h2: ({ children }: { children?: ReactNode }) => (
20+
<h2 className="mt-4 mb-2 text-lg font-bold leading-tight first:mt-0 sm:text-xl">{children}</h2>
21+
),
22+
h3: ({ children }: { children?: ReactNode }) => (
23+
<h3 className="mt-3 mb-1.5 text-base font-bold leading-tight sm:text-lg">{children}</h3>
24+
),
25+
p: ({ children }: { children?: ReactNode }) => (
26+
<p className="my-1.5 leading-relaxed [overflow-wrap:anywhere]">{children}</p>
27+
),
28+
ul: ({ children }: { children?: ReactNode }) => (
29+
<ul className="my-2 ml-5 list-disc space-y-1 marker:text-primary">{children}</ul>
30+
),
31+
ol: ({ children }: { children?: ReactNode }) => (
32+
<ol className="my-2 ml-5 list-decimal space-y-1 marker:text-primary">{children}</ol>
33+
),
34+
li: ({ children }: { children?: ReactNode }) => <li className="pl-1">{children}</li>,
35+
blockquote: ({ children }: { children?: ReactNode }) => (
36+
<blockquote className="my-3 rounded-r-lg border-l-2 border-white/10 bg-white/[0.03] px-3 py-2 text-inherit/80">
37+
{children}
38+
</blockquote>
39+
),
40+
hr: () => <hr className="my-3 border-white/10" />,
41+
code: ({ children }: { children?: ReactNode }) => (
42+
<code className="rounded border border-white/10 bg-black/20 px-1.5 py-0.5 font-mono text-[0.8rem] text-primary/90">
43+
{children}
44+
</code>
45+
),
46+
pre: ({ children }: { children?: ReactNode }) => (
47+
<pre className="my-3 overflow-x-auto rounded-xl border border-white/10 bg-black/30 p-3 text-[0.85rem] text-zinc-100">
48+
{children}
49+
</pre>
50+
),
51+
strong: ({ children }: { children?: ReactNode }) => <strong className="font-semibold">{children}</strong>,
52+
em: ({ children }: { children?: ReactNode }) => <em className="italic">{children}</em>,
53+
a: ({ href, children }: { href?: string; children?: ReactNode }) => {
54+
if (typeof href === "string" && href.startsWith("/")) {
55+
return (
56+
<Link to={href} className="font-semibold text-primary underline underline-offset-4 hover:text-primary/80">
57+
{children}
58+
</Link>
59+
)
60+
}
61+
62+
const isExternal = typeof href === "string" && /^https?:\/\//.test(href)
63+
return (
64+
<a
65+
href={href}
66+
className="font-semibold text-primary underline underline-offset-4 hover:text-primary/80"
67+
target={isExternal ? "_blank" : undefined}
68+
rel={isExternal ? "noopener noreferrer" : undefined}
69+
>
70+
{children}
71+
</a>
72+
)
73+
},
74+
}
75+
76+
export function CommentMarkdown({ text, className = "" }: { text: string; className?: string }) {
77+
return (
78+
<div className={`w-full min-w-0 max-w-full text-sm leading-relaxed [overflow-wrap:anywhere] ${className}`.trim()}>
79+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
80+
{linkifyUserMentions(text)}
81+
</ReactMarkdown>
82+
</div>
83+
)
84+
}

renderer/src/components/GameComments.tsx

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SelectValue,
2121
} from "@/components/ui/select"
2222
import { DiscordAvatar } from "@/components/DiscordAvatar"
23+
import { CommentMarkdown } from "@/components/CommentMarkdown"
2324

2425
import { PaginationBar } from "@/components/PaginationBar"
2526
import { apiFetch, apiUrl, getApiBaseUrl } from "@/lib/api"
@@ -608,27 +609,6 @@ export function GameComments({
608609
})
609610
}
610611

611-
const renderBodyWithMentions = (text: string) => {
612-
const parts = text.split(/(@[A-Za-z0-9_]{2,32})/g)
613-
return parts.map((part, idx) => {
614-
const mention = /^@([A-Za-z0-9_]{2,32})$/.exec(part)
615-
if (!mention) {
616-
return <span key={`txt-${idx}`}>{part}</span>
617-
}
618-
const username = mention[1]
619-
return (
620-
<button
621-
key={`mention-${username}-${idx}`}
622-
type="button"
623-
className="font-semibold text-primary hover:underline"
624-
onClick={() => navigate(`/user/${encodeURIComponent(username)}`)}
625-
>
626-
@{username}
627-
</button>
628-
)
629-
})
630-
}
631-
632612
// Reddit-style thread line colors by depth
633613
const threadLineColors = [
634614
"border-blue-500/40",
@@ -748,9 +728,7 @@ export function GameComments({
748728

749729
{isContentRevealed ? (
750730
<div className="mt-2">
751-
<p className="text-sm text-zinc-400/70 whitespace-pre-wrap break-words leading-relaxed italic">
752-
{renderBodyWithMentions(comment.body)}
753-
</p>
731+
<CommentMarkdown text={comment.body} className="text-zinc-400/70 italic" />
754732
<button
755733
type="button"
756734
onClick={toggleRevealDeleted}
@@ -818,9 +796,7 @@ export function GameComments({
818796
</span>
819797
</div>
820798
</div>
821-
<p className="mt-2 text-sm text-zinc-400 whitespace-pre-wrap break-words leading-relaxed">
822-
{renderBodyWithMentions(comment.body)}
823-
</p>
799+
<CommentMarkdown text={comment.body} className="mt-2 text-zinc-400" />
824800
<div className="mt-3 flex flex-wrap items-center gap-2">
825801
<Button
826802
variant="ghost"

0 commit comments

Comments
 (0)