Skip to content

Commit a6f3317

Browse files
authored
Chore/leaderboard markdown improvs (#73)
* feat: changed markdown-preview with react-markdown lib * feat: integrated new markdown component using react-markdown * fix: make markdown editor next.js compatible * chore: leaderboard improvements * added changeset
1 parent 8450b60 commit a6f3317

File tree

10 files changed

+281
-91
lines changed

10 files changed

+281
-91
lines changed

.changeset/sweet-crabs-care.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@gitcoin/ui": patch
3+
---
4+
5+
chore: leaderboard impovements
6+
chore: start using react-markdown instead of markdown-preview
7+
fix: markdown editor to work with next.js

packages/ui/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@
249249
"@storybook/addon-actions": "8.3.5",
250250
"@tanstack/react-query": "^5.59.15",
251251
"@types/papaparse": "^5.3.15",
252-
"@uiw/react-markdown-preview": "^5.1.3",
253252
"@uiw/react-md-editor": "^4.0.4",
254253
"cmdk": "1.0.0",
255254
"decimal.js": "^10.5.0",
@@ -264,14 +263,17 @@
264263
"papaparse": "^5.4.1",
265264
"react-day-picker": "9.3.2",
266265
"react-hook-form": "^7.53.0",
267-
"react-markdown": "^9.0.1",
266+
"react-markdown": "^9.0.3",
268267
"react-number-format": "^5.4.3",
269268
"react-resizable-panels": "^2.1.4",
270269
"react-use": "^17.6.0",
271270
"recharts": "^2.15.0",
271+
"rehype-highlight": "^7.0.2",
272+
"rehype-katex": "^7.0.1",
272273
"rehype-raw": "^7.0.0",
273274
"rehype-sanitize": "^6.0.0",
274275
"remark-gfm": "^4.0.0",
276+
"remark-math": "^6.0.0",
275277
"sonner": "^1.5.0",
276278
"storybook-mock-date-decorator": "^2.0.6",
277279
"tailwind-variants": "^0.2.1",

packages/ui/src/features/retrofunding/components/Leaderboard/Leaderboard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ const leaderboardVariants = tv({
3030
"3xl": "rounded-3xl",
3131
},
3232
size: {
33-
default: "",
33+
default:
34+
"[&_td:first-child]:mt-1 [&_td:first-child]:w-12 [&_td:first-child]:flex-1 [&_td:first-child]:flex-col ",
3435
slim: "[&_.leaderboard-rank-default]:size-6 [&_.leaderboard-rank-default]:text-base [&_.leaderboard-table-headers]:text-lg [&_.leaderboard-table-icons]:size-4 [&_.leaderboard-table-items]:text-base [&_.metric-value]:text-base [&_.metric-value]:leading-6 [&_.project-logo]:size-7 [&_.project-name]:text-lg [&_.project-name]:leading-6 [&_td:first-child]:flex [&_td:first-child]:w-12 [&_td:first-child]:justify-center [&_td:not(:nth-child(2))]:text-center [&_td]:px-2 [&_th:first-child]:w-12 [&_th:not(:nth-child(2))]:text-center [&_th:nth-child(2)]:min-w-[300px] [&_th]:px-2",
3536
},
3637
},

packages/ui/src/features/retrofunding/components/Leaderboard/components/DesktopLeaderboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const DesktopLeaderboard = ({
101101
<Icon type={IconType.SORT} className={icon()} />
102102
</div>
103103
</TableHead>
104-
<TableHead className="max-w-80 px-7">
104+
<TableHead className="min-w-96 px-7">
105105
<span className={headerText()}>Project name</span>
106106
</TableHead>
107107
{metricIds.map((metricId) => (

packages/ui/src/features/retrofunding/components/Leaderboard/components/ProjectInfo.tsx

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@ import { useMediaQuery } from "usehooks-ts";
44
import { IconLabel } from "@/components/IconLabel";
55
import { cn } from "@/lib";
66
import { IconType } from "@/primitives/Icon";
7+
import { Markdown } from "@/primitives/Markdown";
78

89
const projectInfoVariants = tv({
910
slots: {
10-
container: "flex flex-col gap-4 pb-4 pl-4",
11+
container: "flex w-full flex-col gap-4 pb-4 pl-4",
1112
title:
1213
"leaderboard-project-info-link cursor-pointer truncate font-ui-sans text-sm font-normal leading-7 hover:underline",
1314
description:
14-
"leaderboard-project-info-description line-clamp-3 font-ui-sans font-normal leading-7",
15+
"leaderboard-project-info-description max-h-[350px] overflow-y-auto text-left font-ui-sans font-normal leading-7 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
1516
icon: "leaderboard-project-info-icons size-5 shrink-0 ",
1617
},
1718
});
1819

1920
interface ProjectInfoProps {
2021
project: {
2122
website?: string;
23+
projectProfile?: string;
2224
projectTwitter?: string;
2325
projectGithub?: string;
2426
description?: string;
@@ -31,47 +33,57 @@ export const ProjectInfo = ({ project }: ProjectInfoProps) => {
3133
return (
3234
<div className={cn(container(), isDesktopView && "pl-6")}>
3335
<div className={cn("flex w-fit flex-col gap-1", isDesktopView && "flex-row gap-6")}>
34-
<IconLabel
35-
type="social"
36-
platform="website"
37-
link={project.website}
38-
iconVariant={icon()}
39-
textVariant={title()}
40-
className="shrink-0 items-center"
41-
/>
42-
<IconLabel
43-
type="default"
44-
iconType={IconType.EXPLORER_RAW}
45-
label={
46-
<a href={project.website} target="_blank" rel="noreferrer" className={title()}>
47-
Project profile
48-
</a>
49-
}
50-
iconVariant={icon()}
51-
className="shrink-0 items-center"
52-
/>
53-
<IconLabel
54-
type="social"
55-
platform="twitter"
56-
link={`https://twitter.com/${project.projectTwitter}`}
57-
isVerified={true}
58-
textVariant={title()}
59-
iconVariant={icon()}
60-
className="shrink-0 items-center"
61-
/>
36+
{project.website && (
37+
<IconLabel
38+
type="social"
39+
platform="website"
40+
link={project.website}
41+
iconVariant={icon()}
42+
textVariant={title()}
43+
className="shrink-0 items-center"
44+
/>
45+
)}
46+
{project.projectProfile && (
47+
<IconLabel
48+
type="default"
49+
iconType={IconType.EXPLORER_RAW}
50+
label={
51+
<a href={project.projectProfile} target="_blank" rel="noreferrer" className={title()}>
52+
Project profile
53+
</a>
54+
}
55+
iconVariant={icon()}
56+
className="shrink-0 items-center"
57+
/>
58+
)}
59+
{project.projectTwitter && (
60+
<IconLabel
61+
type="social"
62+
platform="twitter"
63+
link={`https://twitter.com/${project.projectTwitter}`}
64+
isVerified={true}
65+
textVariant={title()}
66+
iconVariant={icon()}
67+
className="shrink-0 items-center"
68+
/>
69+
)}
6270

63-
<IconLabel
64-
type="social"
65-
platform="github"
66-
link={`https://github.com/${project.projectGithub}`}
67-
isVerified={true}
68-
textVariant={title()}
69-
iconVariant={icon()}
70-
className="shrink-0 items-center"
71-
/>
71+
{project.projectGithub && (
72+
<IconLabel
73+
type="social"
74+
platform="github"
75+
link={`https://github.com/${project.projectGithub}`}
76+
isVerified={true}
77+
textVariant={title()}
78+
iconVariant={icon()}
79+
className="shrink-0 items-center"
80+
/>
81+
)}
7282
</div>
7383

74-
<span className={description()}>{project.description}</span>
84+
<div className={cn(description(), "w-[calc(100vw-328px)]")}>
85+
<Markdown>{project.description}</Markdown>
86+
</div>
7587
</div>
7688
);
7789
};

packages/ui/src/features/retrofunding/components/Leaderboard/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface LeaderboardProps {
66
name: string;
77
logoImg?: string;
88
website?: string;
9+
projectProfile?: string;
910
projectTwitter?: string;
1011
projectGithub?: string;
1112
description?: string;
Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
"use client";
22

3-
import MarkdownPreview from "@uiw/react-markdown-preview";
3+
import ReactMarkdown from "react-markdown";
4+
5+
import rehypeHighlight from "rehype-highlight";
6+
import rehypeKatex from "rehype-katex";
7+
import remarkGfm from "remark-gfm";
8+
import remarkMath from "remark-math";
49

510
import { withSSR } from "@/lib/withSSR";
611

7-
const MarkdownComponent = ({ children }: { children: string }) => {
12+
export const MarkdownComponent = ({ children }: { children?: string }) => {
813
return (
9-
<MarkdownPreview
10-
source={children}
11-
wrapperElement={{
12-
"data-color-mode": "light",
13-
}}
14-
/>
14+
<ReactMarkdown
15+
remarkPlugins={[remarkGfm, remarkMath]}
16+
rehypePlugins={[rehypeKatex, rehypeHighlight]}
17+
>
18+
{children}
19+
</ReactMarkdown>
1520
);
1621
};
17-
1822
export const Markdown = withSSR(MarkdownComponent);

packages/ui/src/primitives/MarkdownEditor/MarkdownEditor.stories.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ type Story = StoryObj<typeof MarkdownEditor>;
2222
const MarkdownEditorWithHooks = (args: React.ComponentProps<typeof MarkdownEditor>) => {
2323
const [value, setValue] = useState(args.value || "");
2424

25-
const handleChange = (valueOrEvent: string | React.FormEvent<HTMLDivElement>) => {
26-
if (typeof valueOrEvent === "string") {
27-
setValue(valueOrEvent);
25+
const handleChange = (value?: string, event?: React.ChangeEvent<HTMLTextAreaElement>, state?: any) => {
26+
if (value !== undefined) {
27+
setValue(value);
2828
}
2929
};
3030

packages/ui/src/primitives/MarkdownEditor/MarkdownEditor.tsx

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
"use client";
22

3-
import { ComponentProps, forwardRef, Ref, useContext } from "react";
3+
import { forwardRef, useContext } from "react";
4+
import { Suspense, lazy } from "react";
5+
import { useState, useEffect, ComponentType } from "react";
46

5-
import MDEditor, { commands, EditorContext } from "@uiw/react-md-editor";
7+
import { EditorContext, commands, MDEditorProps } from "@uiw/react-md-editor";
68
import rehypeSanitize from "rehype-sanitize";
79

810
import { IconType } from "@/index";
911
import { cn } from "@/lib";
1012
import { Icon } from "@/primitives/Icon";
11-
import { Markdown } from "@/types";
1213

1314
import "./markdown_editor.css";
1415

15-
export interface MarkdownEditorProps extends Markdown {
16-
onChange?: (value: string) => void;
17-
value?: string;
18-
}
16+
const MDEditor = lazy(() => import("@uiw/react-md-editor"));
1917

2018
const WriteButton = () => {
2119
const { preview, dispatch } = useContext(EditorContext);
@@ -94,30 +92,46 @@ const customItalicCommand = {
9492
icon: <Icon type={IconType.ITALIC} className="size-fit text-grey-900" />,
9593
};
9694

97-
export const MarkdownEditor = forwardRef(function MarkdownEditorComponent(
98-
{ ...props }: MarkdownEditorProps & ComponentProps<"div">,
99-
ref: Ref<HTMLDivElement>,
100-
) {
95+
export const MarkdownEditor_ = forwardRef(function MarkdownEditorComponent({
96+
...props
97+
}: MDEditorProps) {
10198
return (
102-
<MDEditor
103-
commands={[editPreviewCommand, customPreviewCommand]}
104-
// TODO: Customize commands Icons
105-
extraCommands={[
106-
customTitleCommand,
107-
customBoldCommand,
108-
customItalicCommand,
109-
commands.divider,
110-
commands.link,
111-
commands.image,
112-
]}
113-
ref={ref}
114-
preview="edit"
115-
previewOptions={{
116-
rehypePlugins: [[rehypeSanitize]],
117-
}}
118-
value={props.value ?? props.placeholder}
119-
onChange={(val) => props.onChange?.(val ?? "")}
120-
data-color-mode="light"
121-
/>
99+
<Suspense fallback={null}>
100+
<MDEditor
101+
commands={[editPreviewCommand, customPreviewCommand]}
102+
extraCommands={[
103+
customTitleCommand,
104+
customBoldCommand,
105+
customItalicCommand,
106+
commands.divider,
107+
commands.link,
108+
commands.image,
109+
]}
110+
preview="edit"
111+
previewOptions={{
112+
rehypePlugins: [[rehypeSanitize]],
113+
}}
114+
value={props.value}
115+
onChange={(val) => props.onChange?.(val ?? "")}
116+
data-color-mode="light"
117+
/>
118+
</Suspense>
122119
);
123120
});
121+
122+
export const MarkdownEditor = (props: MDEditorProps) => (
123+
<DynamicLoader component={MarkdownEditor_} {...props} />
124+
);
125+
126+
export function DynamicLoader<T extends object>({
127+
component: Component,
128+
...props
129+
}: { component: ComponentType<T> } & T) {
130+
const [isMounted, setIsMounted] = useState(false);
131+
132+
useEffect(() => {
133+
setIsMounted(true);
134+
}, []);
135+
if (!isMounted) return null;
136+
return <Component {...(props as T)} />;
137+
}

0 commit comments

Comments
 (0)