Skip to content

Commit 1494cfc

Browse files
authored
Merge pull request #226 from huamanraj/perf/migrate-oss-sheets-to-md
migrated oss sheets to md
2 parents 79f0d6c + 3b8129c commit 1494cfc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+1951
-1829
lines changed

apps/web/src/app/(main)/dashboard/sheet/page.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useState, useEffect, useMemo, useCallback, memo } from "react";
44
import { useSession } from "next-auth/react";
55
import { trpc } from "@/lib/trpc";
6-
import { sheetModules } from "@/data/sheet";
76
import type { SheetModule } from "@/data/sheet";
87
import {
98
Table,
@@ -22,6 +21,7 @@ import { OpensoxProBadge } from "@/components/sheet/OpensoxProBadge";
2221
import { ProgressBar } from "@/components/sheet/ProgressBar";
2322
import { ActiveTag } from "@/components/ui/ActiveTag";
2423
import { useSubscription } from "@/hooks/useSubscription";
24+
import { SheetSkeleton } from "@/components/sheet/SheetSkeleton";
2525

2626
const tableColumns = [
2727
"S.No",
@@ -39,7 +39,7 @@ const SheetTableRow = memo(function SheetTableRow({
3939
onCheckboxChange,
4040
isPaidUser,
4141
}: {
42-
module: SheetModule;
42+
module: Omit<SheetModule, "docContent">;
4343
index: number;
4444
isCompleted: boolean;
4545
onCheckboxChange: (moduleId: string, checked: boolean) => void;
@@ -129,14 +129,35 @@ const SheetTableRow = memo(function SheetTableRow({
129129
</TableRow>
130130
);
131131
});
132-
133132
export default function SheetPage() {
134133
const { data: session, status } = useSession();
135134
const { isPaidUser } = useSubscription();
136135
const [completedSteps, setCompletedSteps] = useState<string[]>([]);
137136
const [copied, setCopied] = useState(false);
137+
const [sheetModules, setSheetModules] = useState<
138+
Omit<SheetModule, "docContent">[]
139+
>([]);
140+
const [isLoadingModules, setIsLoadingModules] = useState(true);
138141
const utils = trpc.useUtils();
139142

143+
// fetch modules metadata from api
144+
useEffect(() => {
145+
async function fetchModules() {
146+
try {
147+
const response = await fetch("/api/sheet/modules");
148+
if (response.ok) {
149+
const modules = await response.json();
150+
setSheetModules(modules);
151+
}
152+
} catch (error) {
153+
console.error("failed to fetch modules:", error);
154+
} finally {
155+
setIsLoadingModules(false);
156+
}
157+
}
158+
fetchModules();
159+
}, []);
160+
140161
// TypeScript has difficulty narrowing TRPC procedure union types.
141162
// These procedures are correctly typed at runtime (query vs mutation).
142163
const getCompletedStepsProcedure = trpc.user
@@ -215,7 +236,7 @@ export default function SheetPage() {
215236
);
216237

217238
// Memoize computed values
218-
const totalModules = useMemo(() => sheetModules.length, []);
239+
const totalModules = useMemo(() => sheetModules.length, [sheetModules]);
219240
const completedCount = useMemo(() => completedSteps.length, [completedSteps]);
220241

221242
// Memoize download handler
@@ -299,7 +320,7 @@ export default function SheetPage() {
299320
setTimeout(() => {
300321
printWindow.print();
301322
}, 250);
302-
}, [completedCount, totalModules, completedSteps]);
323+
}, [completedCount, totalModules, completedSteps, sheetModules]);
303324

304325
// Memoize share handler
305326
const handleShare = useCallback(async () => {
@@ -313,16 +334,11 @@ export default function SheetPage() {
313334
}
314335
}, []);
315336

316-
// Show loading only if we're actually loading session OR steps
317-
const isLoading =
318-
(status === "loading" || isLoadingSteps) && !completedSteps.length;
337+
// Show loading only if we're actually loading session OR steps OR modules
338+
const isLoading = status === "loading" || isLoadingSteps || isLoadingModules;
319339

320340
if (isLoading) {
321-
return (
322-
<div className="w-full p-6 flex items-center justify-center h-[80vh]">
323-
<p className="text-text-muted">Loading...</p>
324-
</div>
325-
);
341+
return <SheetSkeleton />;
326342
}
327343

328344
return (
Lines changed: 51 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,243 +1,63 @@
1-
"use client";
2-
3-
import { notFound, useParams } from "next/navigation";
4-
import { sheetModules } from "@/data/sheet";
5-
import Link from "next/link";
6-
import { ArrowLeft, Download, Share2, Check } from "lucide-react";
7-
import { Badge } from "@/components/ui/badge";
8-
import { useState, useMemo, useEffect } from "react";
9-
10-
// Cache DOMPurify instance once loaded
11-
let DOMPurifyInstance: typeof import("dompurify").default | null = null;
12-
let DOMPurifyPromise: Promise<typeof import("dompurify").default> | null = null;
13-
14-
// Helper function to get DOMPurify instance - loads asynchronously on first call
15-
const getDOMPurify = async (): Promise<
16-
typeof import("dompurify").default | null
17-
> => {
18-
if (typeof window === "undefined") {
19-
return null;
20-
}
21-
22-
if (DOMPurifyInstance) {
23-
return DOMPurifyInstance;
24-
}
25-
26-
if (DOMPurifyPromise) {
27-
return DOMPurifyPromise;
28-
}
29-
30-
DOMPurifyPromise = import("dompurify").then((mod) => {
31-
DOMPurifyInstance = mod.default;
32-
return DOMPurifyInstance;
33-
});
34-
35-
return DOMPurifyPromise;
36-
};
37-
38-
// Synchronous sanitize function - returns unsanitized if DOMPurify not loaded yet
39-
const sanitizeHTMLSync = (
40-
html: string,
41-
options?: { ALLOWED_TAGS: string[] }
42-
): string => {
43-
if (typeof window === "undefined") {
44-
return html;
45-
}
46-
47-
if (!DOMPurifyInstance) {
48-
// Trigger async load but return unsanitized for now
49-
getDOMPurify();
50-
return html;
51-
}
52-
53-
if (options) {
54-
return DOMPurifyInstance.sanitize(html, options);
55-
}
56-
return DOMPurifyInstance.sanitize(html);
57-
};
58-
59-
export default function ModuleDocPage() {
60-
const params = useParams();
61-
const moduleId = params?.moduleId as string;
62-
const [copied, setCopied] = useState(false);
63-
const [isDOMPurifyLoaded, setIsDOMPurifyLoaded] = useState(false);
64-
65-
const currentModule = sheetModules.find((m) => m.id === moduleId);
66-
67-
// Load DOMPurify on mount
68-
useEffect(() => {
69-
if (typeof window !== "undefined") {
70-
getDOMPurify().then(() => {
71-
setIsDOMPurifyLoaded(true);
72-
});
73-
}
74-
}, []);
75-
76-
const sanitizedDocContent = useMemo(() => {
77-
if (!currentModule?.docContent) return "";
78-
return sanitizeHTMLSync(currentModule.docContent);
79-
}, [currentModule?.docContent, isDOMPurifyLoaded]);
80-
81-
const handleDownloadPDF = () => {
82-
if (!currentModule) return;
83-
84-
const printWindow = window.open("", "_blank");
85-
if (!printWindow) return;
86-
87-
const sanitizedModuleName = sanitizeHTMLSync(currentModule.name, {
88-
ALLOWED_TAGS: [],
89-
});
90-
91-
const htmlContent = `
92-
<!DOCTYPE html>
93-
<html>
94-
<head>
95-
<title>${sanitizedModuleName} - 30 days of Open Source sheet</title>
96-
<style>
97-
body {
98-
font-family: 'Courier New', monospace;
99-
padding: 20px;
100-
background: white;
101-
color: black;
102-
max-width: 800px;
103-
margin: 0 auto;
104-
}
105-
h1 { font-size: 24px; margin-bottom: 10px; }
106-
.content {
107-
line-height: 1.6;
108-
}
109-
.content h1 { font-size: 20px; margin-top: 20px; }
110-
.content h2 { font-size: 18px; margin-top: 15px; }
111-
.content p { margin-bottom: 10px; }
112-
.content ul { margin-left: 20px; }
113-
.content li { margin-bottom: 5px; }
114-
.content pre {
115-
background: #f5f5f5;
116-
padding: 10px;
117-
border-radius: 4px;
118-
overflow-x: auto;
119-
}
120-
.content code {
121-
background: #f5f5f5;
122-
padding: 2px 4px;
123-
border-radius: 2px;
124-
}
125-
</style>
126-
</head>
127-
<body>
128-
<h1>${sanitizedModuleName}</h1>
129-
<div class="content">
130-
${sanitizedDocContent}
131-
</div>
132-
</body>
133-
</html>
134-
`;
135-
136-
printWindow.document.write(htmlContent);
137-
printWindow.document.close();
138-
printWindow.focus();
139-
setTimeout(() => {
140-
printWindow.print();
141-
}, 250);
142-
};
1+
import { notFound } from "next/navigation";
2+
import { getSheetModules } from "@/data/sheet";
3+
import { SheetModuleHeader } from "@/components/sheet/SheetModuleHeader";
4+
import { SheetContentRenderer } from "@/components/sheet/SheetContentRenderer";
5+
import styles from "./sheet-content.module.css";
6+
7+
interface PageProps {
8+
params: Promise<{ moduleId: string }>;
9+
}
14310

144-
const handleShare = async () => {
145-
const url = window.location.href;
146-
try {
147-
await navigator.clipboard.writeText(url);
148-
setCopied(true);
149-
setTimeout(() => setCopied(false), 2000);
150-
} catch (clipboardErr) {
151-
console.error("Failed to copy:", clipboardErr);
152-
}
153-
};
11+
export default async function SheetModulePage({ params }: PageProps) {
12+
const { moduleId } = await params;
13+
const sheetModules = getSheetModules();
14+
const sheetModule = sheetModules.find((m) => m.id === moduleId);
15415

155-
if (!currentModule) {
16+
if (!sheetModule) {
15617
notFound();
15718
}
15819

159-
if (currentModule.comingSoon) {
160-
return (
161-
<div className="min-h-screen bg-ox-header text-text-primary font-DMfont">
162-
<div className="max-w-4xl mx-auto px-6 py-8">
163-
<div className="mb-6">
164-
<Link
165-
href="/dashboard/sheet"
166-
className="inline-flex items-center gap-2 text-brand-purple-light hover:text-brand-purple transition-colors mb-4"
167-
>
168-
<ArrowLeft className="h-4 w-4" />
169-
<span>Back to Sheet</span>
170-
</Link>
171-
<h1 className="text-3xl font-bold text-text-primary mb-2">
172-
{currentModule.name}
173-
</h1>
174-
</div>
175-
176-
<div className="bg-ox-content rounded-lg p-8 border border-ox-header text-center">
177-
<Badge className="bg-ox-purple/20 text-ox-purple border-ox-purple/30 mb-4">
178-
Soon
179-
</Badge>
180-
<p className="text-text-secondary text-lg">
181-
This module is coming very soon. Stay tuned!
182-
</p>
183-
</div>
184-
</div>
185-
</div>
186-
);
187-
}
188-
18920
return (
190-
<div className="min-h-screen bg-ox-header text-text-primary font-DMfont">
191-
<div className="max-w-4xl mx-auto px-6 py-8">
192-
{/* Header with back button and actions */}
193-
<div className="mb-6">
194-
<div className="flex items-center justify-between mb-4 flex-wrap gap-4">
195-
<Link
196-
href="/dashboard/sheet"
197-
className="inline-flex items-center gap-2 text-brand-purple-light hover:text-brand-purple transition-colors"
198-
>
199-
<ArrowLeft className="h-4 w-4" />
200-
<span>Back to Sheet</span>
201-
</Link>
202-
<div className="flex items-center gap-3 flex-shrink-0">
203-
{copied && (
204-
<Badge className="bg-brand-purple text-text-primary border-0 flex items-center gap-1">
205-
<Check className="h-3 w-3" />
206-
Copied
207-
</Badge>
21+
<>
22+
<div className="min-h-screen bg-surface-primary">
23+
<div className="max-w-4xl mx-auto px-4 py-8 md:px-8">
24+
<SheetModuleHeader
25+
moduleName={sheetModule.name}
26+
docContent={sheetModule.docContent}
27+
/>
28+
<article>
29+
<header className="mb-8">
30+
<h1 className="text-3xl md:text-4xl font-bold text-text-primary mb-4">
31+
{sheetModule.name}
32+
</h1>
33+
{sheetModule.videoUrl && (
34+
<div className="mb-6">
35+
<a
36+
href={sheetModule.videoUrl}
37+
target="_blank"
38+
rel="noopener noreferrer"
39+
className="inline-flex items-center gap-2 text-brand-purple-light hover:text-brand-purple transition-colors"
40+
>
41+
<svg
42+
className="w-5 h-5"
43+
fill="currentColor"
44+
viewBox="0 0 24 24"
45+
>
46+
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
47+
</svg>
48+
<span>watch video</span>
49+
</a>
50+
</div>
20851
)}
209-
<button
210-
onClick={handleDownloadPDF}
211-
className="p-2 text-text-primary hover:text-brand-purple transition-colors rounded-md hover:bg-ox-content/50"
212-
title="Download as PDF"
213-
aria-label="Download as PDF"
214-
>
215-
<Download className="h-5 w-5" />
216-
</button>
217-
<button
218-
onClick={handleShare}
219-
className="p-2 text-text-primary hover:text-brand-purple transition-colors rounded-md hover:bg-ox-content/50"
220-
title="Share module"
221-
aria-label="Share module"
222-
>
223-
<Share2 className="h-5 w-5" />
224-
</button>
225-
</div>
226-
</div>
227-
<h1 className="text-3xl font-bold text-text-primary mb-2">
228-
{currentModule.name}
229-
</h1>
230-
</div>
52+
</header>
23153

232-
{/* Content */}
233-
<div className="bg-ox-content rounded-lg p-8 prose prose-invert max-w-none font-DMfont border border-ox-header">
234-
<div
235-
// eslint-disable-next-line react/no-danger -- Safe: docContent is sanitized with DOMPurify before rendering
236-
dangerouslySetInnerHTML={{ __html: sanitizedDocContent }}
237-
className="text-text-primary [&_h1]:text-2xl [&_h1]:font-bold [&_h1]:mb-4 [&_h1]:mt-6 [&_h1]:text-text-primary [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:mb-3 [&_h2]:mt-5 [&_h2]:text-text-primary [&_p]:text-text-secondary [&_p]:mb-4 [&_p]:leading-relaxed [&_ul]:list-disc [&_ul]:ml-6 [&_ul]:mb-4 [&_ul]:text-text-secondary [&_li]:mb-2 [&_pre]:bg-ox-sidebar [&_pre]:p-4 [&_pre]:rounded [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:font-DMfont [&_pre]:border [&_pre]:border-ox-header [&_code]:text-brand-purple [&_code]:bg-ox-sidebar [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:font-DMfont [&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-lg [&_img]:my-5 [&_img]:border [&_img]:border-ox-header"
238-
/>
54+
<SheetContentRenderer
55+
className={styles.sheetContent}
56+
content={sheetModule.docContent}
57+
/>
58+
</article>
23959
</div>
24060
</div>
241-
</div>
61+
</>
24262
);
24363
}

0 commit comments

Comments
 (0)