Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@
"react-dom": "^18.2.0",
"react-qr-code": "^2.0.18",
"react-tweet": "^3.2.1",
"sanitize-html": "^2.11.0",
"superjson": "^2.2.5",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.1"
},
"devDependencies": {
"@types/dompurify": "^3.2.0",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/jsdom": "^27.0.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/sanitize-html": "^2.16.0",
"depcheck": "^1.4.7",
"eslint": "^8",
"eslint-config-next": "15.0.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/(main)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function DashboardLayout({
const { showFilters } = useFilterStore();
const { showSidebar, setShowSidebar } = useShowSidebar();
return (
<div className="flex w-screen h-screen bg-dash-base overflow-hidden">
<div className="flex w-full h-screen bg-dash-base overflow-hidden">
{showFilters && <FiltersContainer />}
<aside className="hidden xl:block h-full">
<Sidebar />
Expand Down
70 changes: 70 additions & 0 deletions apps/web/src/app/(main)/dashboard/oss-programs/ProgramsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { useState, useMemo } from "react";
import { Program } from "@/data/oss-programs/types";
import { SearchInput, TagFilter, ProgramCard } from "@/components/oss-programs";

interface ProgramsListProps {
programs: Program[];
tags: string[];
}

export default function ProgramsList({ programs, tags }: ProgramsListProps) {
const [searchQuery, setSearchQuery] = useState("");
const [selectedTags, setSelectedTags] = useState<string[]>([]);

const filteredPrograms = useMemo(() => {
return programs.filter((program) => {
const matchesSearch =
program.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
program.tags.some((tag) =>
tag.toLowerCase().includes(searchQuery.toLowerCase())
);

const matchesTags =
selectedTags.length === 0 ||
selectedTags.every((tag) => program.tags.includes(tag));

return matchesSearch && matchesTags;
});
}, [programs, searchQuery, selectedTags]);

return (
<div className="min-h-full w-full max-w-[100vw] bg-[#1e1e1e] text-white p-4 md:p-8 lg:p-12 overflow-x-hidden">
<div className="max-w-6xl mx-auto w-full">
{/* Header Section */}
<div className="flex flex-col gap-8 mb-12">
<h1 className="text-3xl md:text-4xl font-bold text-white break-words">
OSS Programs
</h1>

<div className="flex flex-col md:flex-row gap-4 w-full">
<SearchInput
value={searchQuery}
onChange={setSearchQuery}
placeholder="Search programs..."
/>
<TagFilter
tags={tags}
selectedTags={selectedTags}
onTagsChange={setSelectedTags}
/>
</div>
</div>

{/* List Section */}
<div className="flex flex-col gap-2 md:gap-3">
{filteredPrograms.length === 0 ? (
<div className="text-center py-20 text-gray-500">
No programs found matching your criteria.
</div>
) : (
filteredPrograms.map((program) => (
<ProgramCard key={program.slug} program={program} />
))
)}
</div>
</div>
</div>
);
}
101 changes: 101 additions & 0 deletions apps/web/src/app/(main)/dashboard/oss-programs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { getProgramBySlug, getAllPrograms } from "@/data/oss-programs";
import { notFound } from "next/navigation";
import { marked } from "marked";
import sanitizeHtml from "sanitize-html";
import {
ProgramHeader,
ProgramMetadata,
ProgramSection,
} from "@/components/oss-programs";
import "./program-styles.css";

export const revalidate = 3600;

export async function generateStaticParams() {
const programs = getAllPrograms();
return programs.map((program) => ({
slug: program.slug,
}));
}

export default async function ProgramPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const program = getProgramBySlug(slug);

if (!program) {
notFound();
}

marked.setOptions({
gfm: true,
breaks: true,
});

const renderMarkdown = (markdown: string) => {
const html = marked.parse(markdown) as string;
return sanitizeHtml(html, {
allowedTags: [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"p",
"br",
"strong",
"em",
"u",
"s",
"code",
"pre",
"ul",
"ol",
"li",
"blockquote",
"a",
"img",
"table",
"thead",
"tbody",
"tr",
"th",
"td",
"hr",
"div",
"span",
],
allowedAttributes: {
a: ["href", "title", "target", "rel"],
img: ["src", "alt", "title", "width", "height"],
code: ["class"],
pre: ["class"],
},
allowedSchemes: ["http", "https", "mailto"],
});
};

return (
<main className="min-h-screen w-full bg-dash-base text-white overflow-x-hidden">
<div className="max-w-6xl mx-auto px-4 md:px-8 py-8 md:py-12 w-full">
<ProgramHeader program={program} />
<ProgramMetadata program={program} />

<div className="space-y-10">
{program.sections.map((section) => (
<ProgramSection
key={section.id}
id={section.id}
title={section.title}
contentHtml={renderMarkdown(section.bodyMarkdown)}
/>
))}
</div>
</div>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
:root {
--color-primary: #9455f4;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-text-primary: #333;
--color-text-secondary: #444;
--color-text-tertiary: #9ca3af;
--color-bg-secondary: #252525;
}

/* Base list styling */
.program-content li {
position: relative;
padding-left: 1.5rem;
margin: 0.5rem 0;
}

.program-content ul li::before {
content: "•";
position: absolute;
left: 0;
color: var(--color-primary);
font-weight: bold;
}

.program-content ol {
counter-reset: item;
}

.program-content ol li {
counter-increment: item;
}

.program-content ol li::before {
content: counter(item) ".";
position: absolute;
left: 0;
color: var(--color-primary);
font-weight: 600;
font-size: 0.875rem;
}

.program-content ul li::marker,
.program-content ol li::marker {
content: "";
}

/* Eligibility section - checkmarks for "good match" list */
.eligibility-section ul:first-of-type li::before {
content: "✓";
color: var(--color-success);
}

/* Keep in mind - simple muted styling */
.eligibility-section p:last-of-type {
color: var(--color-text-tertiary);
font-style: italic;
padding-left: 1rem;
border-left: 2px solid var(--color-text-secondary);
margin-top: 1rem;
}

/* Preparation checklist - numbered steps with subtle background */
.preparation-checklist ol li {
background: var(--color-bg-secondary);
padding: 0.75rem 1rem 0.75rem 2.25rem;
margin: 0.5rem 0;
border-radius: 0.375rem;
border: 1px solid transparent;
transition: border-color 0.2s;
}

.preparation-checklist ol li:hover {
border-color: #333;
}

.preparation-checklist ol li::before {
left: 0.75rem;
top: 0.75rem;
color: var(--color-primary);
font-weight: 700;
}

/* Application process - step indicators */
.application-timeline ul {
padding-left: 0.5rem;
border-left: 2px solid #333;
margin-left: 0.5rem;
}

.application-timeline ul li {
padding: 0.5rem 0 0.5rem 1.25rem;
margin: 0;
}

.application-timeline ul li::before {
content: "";
position: absolute;
left: -0.85rem;
top: 0.85rem;
width: 8px;
height: 8px;
background: var(--color-primary);
border-radius: 50%;
}

.application-timeline ul li:first-child::before,
.application-timeline ul li:last-child::before {
background: var(--color-primary);
}

/* Mask the line above the first dot */
.application-timeline ul li:first-child::after {
content: "";
position: absolute;
left: -2rem;
/* Cover the border area to the left */
top: 0;
width: 2rem;
height: 0.85rem;
/* Height up to the dot */
background: theme('colors.dash.base');
/* Match page background */
z-index: 1;
}

/* Mask the line below the last dot */
.application-timeline ul li:last-child::after {
content: "";
position: absolute;
left: -2rem;
top: calc(0.85rem + 8px);
/* Start after the dot */
bottom: 0;
width: 2rem;
background: theme('colors.dash.base');
z-index: 1;
}

/* What this program is about section styling */
.what-section p {
line-height: 2 !important;
/* Increased line spacing */
margin-bottom: 1.5rem !important;
/* More space between paragraphs */
color: #e5e5e5;
/* Slightly brighter text for clarity */
}

.what-section h3 {
margin-top: 3rem !important;
/* significantly more space before subsections like Duration/Stipend */
margin-bottom: 1rem !important;
color: var(--color-primary);
/* Highlight these headers */
}

.what-section h4 {
margin-top: 2.5rem !important;
margin-bottom: 0.75rem !important;
}

/* If Duration/Stipend are emphasized text at start of lines */
.what-section p strong {
color: white;
}
11 changes: 11 additions & 0 deletions apps/web/src/app/(main)/dashboard/oss-programs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getAllPrograms, getAllTags } from "@/data/oss-programs";
import ProgramsList from "./ProgramsList";

export const revalidate = 3600;

export default function Page() {
const programs = getAllPrograms();
const tags = getAllTags();

return <ProgramsList programs={programs} tags={tags} />;
}
Loading