Merge pull request #76 from pennyteenycat/video-progress-edits
Video progress edits
This commit is contained in:
commit
c31325e01a
1480
package-lock.json
generated
1480
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,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",
|
||||
"shaka-player": "^4.16.7",
|
||||
|
||||
@ -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,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
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";
|
||||
|
||||
@ -14,14 +15,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]);
|
||||
|
||||
// TODO: Remove hardcoded URLs when backend is ready
|
||||
// Temporary hardcoded DASH/HLS URLs - using for ALL videos until backend updated
|
||||
@ -54,12 +82,20 @@ export default function VideoGuideView({
|
||||
<VideoPlayer mpd={dashUrl} m3u8={hlsUrl} />
|
||||
</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, { type Components } 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;
|
||||
const components: 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} />,
|
||||
// @ts-expect-error - inline prop is provided by react-markdown
|
||||
code: ({ inline, ...props }) =>
|
||||
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} />
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.markdown} ${className || ""}`}>
|
||||
{parseMarkdown(content)}
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
||||
{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