add md
This commit is contained in:
parent
7f9f1af39e
commit
041c3eacd0
55
TEST_MARKDOWN.md
Normal file
55
TEST_MARKDOWN.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Test Markdown Content
|
||||||
|
|
||||||
|
This is a test file to verify markdown parsing improvements.
|
||||||
|
|
||||||
|
## Escaped Characters Test
|
||||||
|
|
||||||
|
**Rule \#6: Set boundaries—and respect them.**
|
||||||
|
|
||||||
|
This should show #6 as text, not as a header.
|
||||||
|
|
||||||
|
### Header Without Space
|
||||||
|
###This should also work as a header
|
||||||
|
|
||||||
|
## Bold and Italic
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
*This is italic text*
|
||||||
|
**Bold with \*escaped\* asterisks inside**
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
- Item 1
|
||||||
|
- Item 2
|
||||||
|
- Item 3
|
||||||
|
|
||||||
|
1. First item
|
||||||
|
2. Second item
|
||||||
|
3. Third item
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
Inline `code` example.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function example() {
|
||||||
|
console.log("Hello World");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
[Click here](https://example.com) to visit example.
|
||||||
|
|
||||||
|
## Blockquotes
|
||||||
|
|
||||||
|
> This is a blockquote with some text.
|
||||||
|
|
||||||
|
## Horizontal Rule
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Special Characters
|
||||||
|
|
||||||
|
Em dash: — (this is different from hyphen -)
|
||||||
|
Escaped asterisk: \* should show as asterisk
|
||||||
1473
package-lock.json
generated
1473
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,8 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-circular-progressbar": "^2.2.0",
|
"react-circular-progressbar": "^2.2.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.89.2",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export default async function VideoGuidePage({
|
|||||||
name={videoGuide.name}
|
name={videoGuide.name}
|
||||||
description={videoGuide.description}
|
description={videoGuide.description}
|
||||||
videoLink={videoGuide.videoLink}
|
videoLink={videoGuide.videoLink}
|
||||||
|
contentUrl={videoGuide.contentUrl}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,6 +67,7 @@
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
flex-shrink: 0; /* Prevent video from shrinking */
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoInner {
|
.videoInner {
|
||||||
@ -88,6 +89,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
position: relative; /* Ensure proper positioning context */
|
||||||
|
flex-shrink: 0; /* Prevent shrinking */
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { Icon, IconName, Typography } from "@/components/ui";
|
import { Icon, IconName, MarkdownText, Typography } from "@/components/ui";
|
||||||
|
|
||||||
import styles from "./VideoGuideView.module.scss";
|
import styles from "./VideoGuideView.module.scss";
|
||||||
|
|
||||||
@ -11,14 +12,41 @@ interface VideoGuideViewProps {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
videoLink: string;
|
videoLink: string;
|
||||||
|
contentUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VideoGuideView({
|
export default function VideoGuideView({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
videoLink,
|
videoLink,
|
||||||
|
contentUrl,
|
||||||
}: VideoGuideViewProps) {
|
}: VideoGuideViewProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [markdownContent, setMarkdownContent] = useState<string | null>(null);
|
||||||
|
const [isLoadingMarkdown, setIsLoadingMarkdown] = useState(false);
|
||||||
|
|
||||||
|
// Load markdown content if contentUrl is provided
|
||||||
|
useEffect(() => {
|
||||||
|
if (!contentUrl) return;
|
||||||
|
|
||||||
|
const loadMarkdown = async () => {
|
||||||
|
setIsLoadingMarkdown(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(contentUrl);
|
||||||
|
if (response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
setMarkdownContent(text);
|
||||||
|
}
|
||||||
|
// Silently fail and show description as fallback
|
||||||
|
} catch {
|
||||||
|
// Silently fail and show description as fallback
|
||||||
|
} finally {
|
||||||
|
setIsLoadingMarkdown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadMarkdown();
|
||||||
|
}, [contentUrl]);
|
||||||
|
|
||||||
// Extract video ID from various YouTube URL formats
|
// Extract video ID from various YouTube URL formats
|
||||||
const getYouTubeVideoId = (url: string): string | null => {
|
const getYouTubeVideoId = (url: string): string | null => {
|
||||||
@ -71,12 +99,20 @@ export default function VideoGuideView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description or Markdown Content */}
|
||||||
{description && (
|
{(isLoadingMarkdown || markdownContent || description) && (
|
||||||
<div className={styles.descriptionWrapper}>
|
<div className={styles.descriptionWrapper}>
|
||||||
<Typography as="p" size="md" className={styles.description}>
|
{isLoadingMarkdown ? (
|
||||||
{description}
|
<Typography as="p" size="md" className={styles.description}>
|
||||||
</Typography>
|
Loading content...
|
||||||
|
</Typography>
|
||||||
|
) : markdownContent ? (
|
||||||
|
<MarkdownText content={markdownContent} />
|
||||||
|
) : description ? (
|
||||||
|
<Typography as="p" size="md" className={styles.description}>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -79,6 +79,60 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Code
|
||||||
|
.codeBlock {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 8px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineCode {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d63384;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blockquote
|
||||||
|
.blockquote {
|
||||||
|
border-left: 4px solid #646464;
|
||||||
|
padding-left: 16px;
|
||||||
|
margin: 8px 0;
|
||||||
|
color: #646464;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal rule
|
||||||
|
.hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links
|
||||||
|
.link {
|
||||||
|
color: #0066cc;
|
||||||
|
text-decoration: underline;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #0052a3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Line breaks
|
// Line breaks
|
||||||
br {
|
br {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
import styles from "./MarkdownText.module.scss";
|
import styles from "./MarkdownText.module.scss";
|
||||||
|
|
||||||
@ -13,120 +14,37 @@ export default function MarkdownText({
|
|||||||
content,
|
content,
|
||||||
className,
|
className,
|
||||||
}: MarkdownTextProps) {
|
}: MarkdownTextProps) {
|
||||||
// Simple markdown parser for basic formatting
|
|
||||||
const parseMarkdown = (text: string): React.ReactNode[] => {
|
|
||||||
const lines = text.split("\n");
|
|
||||||
const elements: React.ReactNode[] = [];
|
|
||||||
let key = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
// Skip empty lines
|
|
||||||
if (line.trim() === "") {
|
|
||||||
elements.push(<br key={`br-${key++}`} />);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers (# ## ###)
|
|
||||||
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
||||||
if (headerMatch) {
|
|
||||||
const level = headerMatch[1].length;
|
|
||||||
const text = headerMatch[2];
|
|
||||||
const HeaderTag = `h${level}` as
|
|
||||||
| "h1"
|
|
||||||
| "h2"
|
|
||||||
| "h3"
|
|
||||||
| "h4"
|
|
||||||
| "h5"
|
|
||||||
| "h6";
|
|
||||||
elements.push(
|
|
||||||
React.createElement(
|
|
||||||
HeaderTag,
|
|
||||||
{ key: `h${level}-${key++}`, className: styles[`h${level}`] },
|
|
||||||
parseInlineMarkdown(text)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unordered lists (- or *)
|
|
||||||
const listMatch = line.match(/^[\*\-]\s+(.+)$/);
|
|
||||||
if (listMatch) {
|
|
||||||
elements.push(
|
|
||||||
<li key={`li-${key++}`} className={styles.listItem}>
|
|
||||||
{parseInlineMarkdown(listMatch[1])}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordered lists (1. 2. etc)
|
|
||||||
const orderedListMatch = line.match(/^\d+\.\s+(.+)$/);
|
|
||||||
if (orderedListMatch) {
|
|
||||||
elements.push(
|
|
||||||
<li key={`oli-${key++}`} className={styles.listItem}>
|
|
||||||
{parseInlineMarkdown(orderedListMatch[1])}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular paragraph
|
|
||||||
elements.push(
|
|
||||||
<p key={`p-${key++}`} className={styles.paragraph}>
|
|
||||||
{parseInlineMarkdown(line)}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse inline markdown (bold, italic, links)
|
|
||||||
const parseInlineMarkdown = (text: string): React.ReactNode[] => {
|
|
||||||
const parts: React.ReactNode[] = [];
|
|
||||||
let remaining = text;
|
|
||||||
let key = 0;
|
|
||||||
|
|
||||||
while (remaining.length > 0) {
|
|
||||||
// Bold (**text** or __text__)
|
|
||||||
const boldMatch = remaining.match(/^(.*?)(\*\*|__)(.*?)\2/);
|
|
||||||
if (boldMatch) {
|
|
||||||
if (boldMatch[1]) parts.push(boldMatch[1]);
|
|
||||||
parts.push(
|
|
||||||
<strong key={`bold-${key++}`} className={styles.bold}>
|
|
||||||
{boldMatch[3]}
|
|
||||||
</strong>
|
|
||||||
);
|
|
||||||
remaining = remaining.substring(boldMatch[0].length);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Italic (*text* or _text_)
|
|
||||||
const italicMatch = remaining.match(/^(.*?)(\*|_)(.*?)\2/);
|
|
||||||
if (italicMatch) {
|
|
||||||
if (italicMatch[1]) parts.push(italicMatch[1]);
|
|
||||||
parts.push(
|
|
||||||
<em key={`italic-${key++}`} className={styles.italic}>
|
|
||||||
{italicMatch[3]}
|
|
||||||
</em>
|
|
||||||
);
|
|
||||||
remaining = remaining.substring(italicMatch[0].length);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No more markdown, add remaining text
|
|
||||||
parts.push(remaining);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.markdown} ${className || ""}`}>
|
<div className={`${styles.markdown} ${className || ""}`}>
|
||||||
{parseMarkdown(content)}
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
h1: ({ ...props }) => <h1 className={styles.h1} {...props} />,
|
||||||
|
h2: ({ ...props }) => <h2 className={styles.h2} {...props} />,
|
||||||
|
h3: ({ ...props }) => <h3 className={styles.h3} {...props} />,
|
||||||
|
h4: ({ ...props }) => <h4 className={styles.h4} {...props} />,
|
||||||
|
h5: ({ ...props }) => <h5 className={styles.h5} {...props} />,
|
||||||
|
h6: ({ ...props }) => <h6 className={styles.h6} {...props} />,
|
||||||
|
p: ({ ...props }) => <p className={styles.paragraph} {...props} />,
|
||||||
|
li: ({ ...props }) => <li className={styles.listItem} {...props} />,
|
||||||
|
strong: ({ ...props }) => <strong className={styles.bold} {...props} />,
|
||||||
|
em: ({ ...props }) => <em className={styles.italic} {...props} />,
|
||||||
|
pre: ({ ...props }) => <pre className={styles.codeBlock} {...props} />,
|
||||||
|
code: ({ inline, ...props }: any) =>
|
||||||
|
inline ? (
|
||||||
|
<code className={styles.inlineCode} {...props} />
|
||||||
|
) : (
|
||||||
|
<code className={styles.code} {...props} />
|
||||||
|
),
|
||||||
|
blockquote: ({ ...props }) => <blockquote className={styles.blockquote} {...props} />,
|
||||||
|
hr: ({ ...props }) => <hr className={styles.hr} {...props} />,
|
||||||
|
a: ({ ...props }) => (
|
||||||
|
<a className={styles.link} target="_blank" rel="noopener noreferrer" {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,7 @@ export const VideoGuideSchema = z.object({
|
|||||||
discount: z.number(),
|
discount: z.number(),
|
||||||
isPurchased: z.boolean(),
|
isPurchased: z.boolean(),
|
||||||
videoLink: z.string().optional(),
|
videoLink: z.string().optional(),
|
||||||
|
contentUrl: z.string().optional(), // URL to markdown content file
|
||||||
});
|
});
|
||||||
export type VideoGuide = z.infer<typeof VideoGuideSchema>;
|
export type VideoGuide = z.infer<typeof VideoGuideSchema>;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user