This commit is contained in:
dev.daminik00 2025-10-28 23:29:19 +01:00
parent 7f9f1af39e
commit 041c3eacd0
9 changed files with 1658 additions and 123 deletions

55
TEST_MARKDOWN.md Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -33,6 +33,7 @@ export default async function VideoGuidePage({
name={videoGuide.name}
description={videoGuide.description}
videoLink={videoGuide.videoLink}
contentUrl={videoGuide.contentUrl}
/>
);
}

View File

@ -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 {

View File

@ -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>

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>;