Skip to content

Commit 51f36c6

Browse files
authored
Merge pull request #214 from huamanraj/oss-programs
feat: OSS programs added with data and ui
2 parents 6e70c37 + f772604 commit 51f36c6

Some content is hidden

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

46 files changed

+3612
-23
lines changed

apps/web/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,19 @@
3838
"react-dom": "^18.2.0",
3939
"react-qr-code": "^2.0.18",
4040
"react-tweet": "^3.2.1",
41+
"sanitize-html": "^2.11.0",
4142
"superjson": "^2.2.5",
4243
"tailwind-merge": "^2.5.4",
4344
"tailwindcss-animate": "^1.0.7",
4445
"zustand": "^5.0.1"
4546
},
4647
"devDependencies": {
47-
"@types/dompurify": "^3.2.0",
4848
"@tailwindcss/line-clamp": "^0.4.4",
49+
"@types/jsdom": "^27.0.0",
4950
"@types/node": "^20",
5051
"@types/react": "^18",
5152
"@types/react-dom": "^18",
53+
"@types/sanitize-html": "^2.16.0",
5254
"depcheck": "^1.4.7",
5355
"eslint": "^8",
5456
"eslint-config-next": "15.0.2",

apps/web/src/app/(main)/dashboard/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function DashboardLayout({
1616
const { showFilters } = useFilterStore();
1717
const { showSidebar, setShowSidebar } = useShowSidebar();
1818
return (
19-
<div className="flex w-screen h-screen bg-dash-base overflow-hidden">
19+
<div className="flex w-full h-screen bg-dash-base overflow-hidden">
2020
{showFilters && <FiltersContainer />}
2121
<aside className="hidden xl:block h-full">
2222
<Sidebar />
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"use client";
2+
3+
import { useState, useMemo } from "react";
4+
import { Program } from "@/data/oss-programs/types";
5+
import { SearchInput, TagFilter, ProgramCard } from "@/components/oss-programs";
6+
7+
interface ProgramsListProps {
8+
programs: Program[];
9+
tags: string[];
10+
}
11+
12+
export default function ProgramsList({ programs, tags }: ProgramsListProps) {
13+
const [searchQuery, setSearchQuery] = useState("");
14+
const [selectedTags, setSelectedTags] = useState<string[]>([]);
15+
16+
const filteredPrograms = useMemo(() => {
17+
return programs.filter((program) => {
18+
const matchesSearch =
19+
program.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
20+
program.tags.some((tag) =>
21+
tag.toLowerCase().includes(searchQuery.toLowerCase())
22+
);
23+
24+
const matchesTags =
25+
selectedTags.length === 0 ||
26+
selectedTags.every((tag) => program.tags.includes(tag));
27+
28+
return matchesSearch && matchesTags;
29+
});
30+
}, [programs, searchQuery, selectedTags]);
31+
32+
return (
33+
<div className="min-h-full w-[99vw] lg:w-[80vw] bg-dash-base text-white p-4 md:p-8 lg:p-12 overflow-x-hidden">
34+
<div className="max-w-6xl mx-auto w-full min-w-0">
35+
{/* Header Section */}
36+
<div className="flex flex-col gap-8 mb-12 min-w-0">
37+
<h1 className="text-3xl md:text-4xl font-bold text-text-primary break-words">
38+
OSS Programs
39+
</h1>
40+
41+
<div className="flex flex-col md:flex-row gap-4 w-full min-w-0">
42+
<SearchInput
43+
value={searchQuery}
44+
onChange={setSearchQuery}
45+
placeholder="Search programs..."
46+
/>
47+
<TagFilter
48+
tags={tags}
49+
selectedTags={selectedTags}
50+
onTagsChange={setSelectedTags}
51+
/>
52+
</div>
53+
</div>
54+
55+
{/* List Section */}
56+
<div className="flex flex-col gap-2 md:gap-3 min-w-0">
57+
{filteredPrograms.length === 0 ? (
58+
<div className="text-center py-20 text-text-muted">
59+
No programs found matching your criteria.
60+
</div>
61+
) : (
62+
filteredPrograms.map((program) => (
63+
<ProgramCard key={program.slug} program={program} />
64+
))
65+
)}
66+
</div>
67+
</div>
68+
</div>
69+
);
70+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { getProgramBySlug, getAllPrograms } from "@/data/oss-programs";
2+
import { notFound } from "next/navigation";
3+
import { marked } from "marked";
4+
import sanitizeHtml from "sanitize-html";
5+
import {
6+
ProgramHeader,
7+
ProgramMetadata,
8+
ProgramSection,
9+
} from "@/components/oss-programs";
10+
import "./program-styles.css";
11+
12+
export const revalidate = 3600;
13+
14+
export async function generateStaticParams() {
15+
const programs = getAllPrograms();
16+
return programs.map((program) => ({
17+
slug: program.slug,
18+
}));
19+
}
20+
21+
export default async function ProgramPage({
22+
params,
23+
}: {
24+
params: Promise<{ slug: string }>;
25+
}) {
26+
const { slug } = await params;
27+
const program = getProgramBySlug(slug);
28+
29+
if (!program) {
30+
notFound();
31+
}
32+
33+
marked.setOptions({
34+
gfm: true,
35+
breaks: true,
36+
});
37+
38+
const renderMarkdown = (markdown: string) => {
39+
const html = marked.parse(markdown) as string;
40+
return sanitizeHtml(html, {
41+
allowedTags: [
42+
"h1",
43+
"h2",
44+
"h3",
45+
"h4",
46+
"h5",
47+
"h6",
48+
"p",
49+
"br",
50+
"strong",
51+
"em",
52+
"u",
53+
"s",
54+
"code",
55+
"pre",
56+
"ul",
57+
"ol",
58+
"li",
59+
"blockquote",
60+
"a",
61+
"img",
62+
"table",
63+
"thead",
64+
"tbody",
65+
"tr",
66+
"th",
67+
"td",
68+
"hr",
69+
"div",
70+
"span",
71+
],
72+
allowedAttributes: {
73+
a: ["href", "title", "target", "rel"],
74+
img: ["src", "alt", "title", "width", "height"],
75+
code: ["class"],
76+
pre: ["class"],
77+
},
78+
allowedSchemes: ["http", "https", "mailto"],
79+
});
80+
};
81+
82+
return (
83+
<main className="min-h-screen w-full bg-dash-base text-white overflow-x-hidden">
84+
<div className="max-w-6xl mx-auto px-4 md:px-8 py-8 md:py-12 w-full">
85+
<ProgramHeader program={program} />
86+
<ProgramMetadata program={program} />
87+
88+
<div className="space-y-10">
89+
{program.sections.map((section) => (
90+
<ProgramSection
91+
key={section.id}
92+
id={section.id}
93+
title={section.title}
94+
contentHtml={renderMarkdown(section.bodyMarkdown)}
95+
/>
96+
))}
97+
</div>
98+
</div>
99+
</main>
100+
);
101+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
:root {
2+
--color-primary: #9455f4;
3+
--color-success: #22c55e;
4+
--color-warning: #f59e0b;
5+
--color-text-primary: #333;
6+
--color-text-secondary: #444;
7+
--color-text-tertiary: #9ca3af;
8+
--color-bg-secondary: #252525;
9+
}
10+
11+
/* Base list styling */
12+
.program-content li {
13+
position: relative;
14+
padding-left: 1.5rem;
15+
margin: 0.5rem 0;
16+
}
17+
18+
.program-content ul li::before {
19+
content: "•";
20+
position: absolute;
21+
left: 0;
22+
color: var(--color-primary);
23+
font-weight: bold;
24+
}
25+
26+
.program-content ol {
27+
counter-reset: item;
28+
}
29+
30+
.program-content ol li {
31+
counter-increment: item;
32+
}
33+
34+
.program-content ol li::before {
35+
content: counter(item) ".";
36+
position: absolute;
37+
left: 0;
38+
color: var(--color-primary);
39+
font-weight: 600;
40+
font-size: 0.875rem;
41+
}
42+
43+
.program-content ul li::marker,
44+
.program-content ol li::marker {
45+
content: "";
46+
}
47+
48+
/* Eligibility section - checkmarks for "good match" list */
49+
.eligibility-section ul:first-of-type li::before {
50+
content: "✓";
51+
color: var(--color-success);
52+
}
53+
54+
/* Keep in mind - simple muted styling */
55+
.eligibility-section p:last-of-type {
56+
color: var(--color-text-tertiary);
57+
font-style: italic;
58+
padding-left: 1rem;
59+
border-left: 2px solid var(--color-text-secondary);
60+
margin-top: 1rem;
61+
}
62+
63+
/* Preparation checklist - numbered steps with subtle background */
64+
.preparation-checklist ol li {
65+
background: var(--color-bg-secondary);
66+
padding: 0.75rem 1rem 0.75rem 2.25rem;
67+
margin: 0.5rem 0;
68+
border-radius: 0.375rem;
69+
border: 1px solid transparent;
70+
transition: border-color 0.2s;
71+
}
72+
73+
.preparation-checklist ol li:hover {
74+
border-color: #333;
75+
}
76+
77+
.preparation-checklist ol li::before {
78+
left: 0.75rem;
79+
top: 0.75rem;
80+
color: var(--color-primary);
81+
font-weight: 700;
82+
}
83+
84+
/* Application process - step indicators */
85+
.application-timeline ul {
86+
padding-left: 0.5rem;
87+
border-left: 2px solid #333;
88+
margin-left: 0.5rem;
89+
}
90+
91+
.application-timeline ul li {
92+
padding: 0.5rem 0 0.5rem 1.25rem;
93+
margin: 0;
94+
}
95+
96+
.application-timeline ul li::before {
97+
content: "";
98+
position: absolute;
99+
left: -0.85rem;
100+
top: 0.85rem;
101+
width: 8px;
102+
height: 8px;
103+
background: var(--color-primary);
104+
border-radius: 50%;
105+
}
106+
107+
.application-timeline ul li:first-child::before,
108+
.application-timeline ul li:last-child::before {
109+
background: var(--color-primary);
110+
}
111+
112+
/* Mask the line above the first dot */
113+
.application-timeline ul li:first-child::after {
114+
content: "";
115+
position: absolute;
116+
left: -2rem;
117+
/* Cover the border area to the left */
118+
top: 0;
119+
width: 2rem;
120+
height: 0.85rem;
121+
/* Height up to the dot */
122+
background: theme('colors.dash.base');
123+
/* Match page background */
124+
z-index: 1;
125+
}
126+
127+
/* Mask the line below the last dot */
128+
.application-timeline ul li:last-child::after {
129+
content: "";
130+
position: absolute;
131+
left: -2rem;
132+
top: calc(0.85rem + 8px);
133+
/* Start after the dot */
134+
bottom: 0;
135+
width: 2rem;
136+
background: theme('colors.dash.base');
137+
z-index: 1;
138+
}
139+
140+
/* What this program is about section styling */
141+
.what-section p {
142+
line-height: 2 !important;
143+
/* Increased line spacing */
144+
margin-bottom: 1.5rem !important;
145+
/* More space between paragraphs */
146+
color: #e5e5e5;
147+
/* Slightly brighter text for clarity */
148+
}
149+
150+
.what-section h3 {
151+
margin-top: 3rem !important;
152+
/* significantly more space before subsections like Duration/Stipend */
153+
margin-bottom: 1rem !important;
154+
color: var(--color-primary);
155+
/* Highlight these headers */
156+
}
157+
158+
.what-section h4 {
159+
margin-top: 2.5rem !important;
160+
margin-bottom: 0.75rem !important;
161+
}
162+
163+
/* If Duration/Stipend are emphasized text at start of lines */
164+
.what-section p strong {
165+
color: white;
166+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { getAllPrograms, getAllTags } from "@/data/oss-programs";
2+
import ProgramsList from "./ProgramsList";
3+
4+
export const revalidate = 3600;
5+
6+
export default function Page() {
7+
const programs = getAllPrograms();
8+
const tags = getAllTags();
9+
10+
return <ProgramsList programs={programs} tags={tags} />;
11+
}

0 commit comments

Comments
 (0)