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-circular-progressbar": "^2.2.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sass": "^1.89.2",
|
||||
"server-only": "^0.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
|
||||
@ -33,6 +33,7 @@ export default async function VideoGuidePage({
|
||||
name={videoGuide.name}
|
||||
description={videoGuide.description}
|
||||
videoLink={videoGuide.videoLink}
|
||||
contentUrl={videoGuide.contentUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -67,6 +67,7 @@
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
flex-shrink: 0; /* Prevent video from shrinking */
|
||||
}
|
||||
|
||||
.videoInner {
|
||||
@ -88,6 +89,8 @@
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
padding: 0;
|
||||
position: relative; /* Ensure proper positioning context */
|
||||
flex-shrink: 0; /* Prevent shrinking */
|
||||
}
|
||||
|
||||
.description {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
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";
|
||||
|
||||
@ -11,14 +12,41 @@ interface VideoGuideViewProps {
|
||||
name: string;
|
||||
description: string;
|
||||
videoLink: string;
|
||||
contentUrl?: string;
|
||||
}
|
||||
|
||||
export default function VideoGuideView({
|
||||
name,
|
||||
description,
|
||||
videoLink,
|
||||
contentUrl,
|
||||
}: VideoGuideViewProps) {
|
||||
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
|
||||
const getYouTubeVideoId = (url: string): string | null => {
|
||||
@ -71,12 +99,20 @@ export default function VideoGuideView({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
{/* Description or Markdown Content */}
|
||||
{(isLoadingMarkdown || markdownContent || description) && (
|
||||
<div className={styles.descriptionWrapper}>
|
||||
<Typography as="p" size="md" className={styles.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
{isLoadingMarkdown ? (
|
||||
<Typography as="p" size="md" className={styles.description}>
|
||||
Loading content...
|
||||
</Typography>
|
||||
) : markdownContent ? (
|
||||
<MarkdownText content={markdownContent} />
|
||||
) : description ? (
|
||||
<Typography as="p" size="md" className={styles.description}>
|
||||
{description}
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -79,6 +79,60 @@
|
||||
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
|
||||
br {
|
||||
display: block;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
import styles from "./MarkdownText.module.scss";
|
||||
|
||||
@ -13,120 +14,37 @@ export default function MarkdownText({
|
||||
content,
|
||||
className,
|
||||
}: 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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -75,6 +75,7 @@ export const VideoGuideSchema = z.object({
|
||||
discount: z.number(),
|
||||
isPurchased: z.boolean(),
|
||||
videoLink: z.string().optional(),
|
||||
contentUrl: z.string().optional(), // URL to markdown content file
|
||||
});
|
||||
export type VideoGuide = z.infer<typeof VideoGuideSchema>;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user