Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.16.0",
"marked": "^18.0.4",
"radix-ui": "^1.4.3",
"react": "^19.2.6",
"react-dom": "^19.2.6",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Volunteer } from '@/screens/Volunteer';
import { Sponsor } from '@/screens/Sponsor';
import { ComingSoon } from '@/pages/ComingSoon';
import { Contact } from '@/pages/Contact';
import { StaticPage } from '@/pages/StaticPage';
import { NotFound } from '@/pages/NotFound';
import { LoginPlaceholder } from '@/pages/LoginPlaceholder';
import { AccountClaim } from '@/pages/AccountClaim';
Expand Down Expand Up @@ -61,7 +62,7 @@ const router = createBrowserRouter([
{ path: '/sponsor', element: <Sponsor /> },
{ path: '/account', element: <Account /> },
{ path: '/search', element: <SearchRedirect /> },
{ path: '/pages/:slug', element: <ComingSoon /> },
{ path: '/pages/:slug', element: <StaticPage /> },
{ path: '/contact', element: <Contact /> },
{ path: '/login', element: <LoginPlaceholder /> },
{ path: '/account-claim', element: <AccountClaim /> },
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/content/pages/code-of-conduct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Code of Conduct

Code for Philly is committed to providing a welcoming, inclusive
experience for everyone who participates in our community —
regardless of background, identity, or experience level.

## Expected behavior

- **Be respectful.** Disagreement on ideas is fine; personal attacks
are not.
- **Be inclusive.** Use language that welcomes people from different
backgrounds; avoid jargon when introducing yourself and your work.
- **Be patient with newcomers.** Everyone was new once.
- **Give credit.** Acknowledge others' contributions, especially in
team projects.

## Unacceptable behavior

- Harassment, discrimination, or exclusionary jokes targeting any
protected class.
- Unwanted sexual attention or advances.
- Doxxing or sharing others' private information without consent.
- Sustained disruption of meetings, channels, or events.

## Reporting

If you witness or experience a violation, please contact the
leadership team at
[hello@codeforphilly.org](mailto:hello@codeforphilly.org). Reports
will be handled discreetly and acted on quickly.

---

*This page is awaiting its full content port from the legacy site.
See [issue tracking](https://github.com/CodeForPhilly/codeforphilly-ng/issues/83)
for status.*
35 changes: 35 additions & 0 deletions apps/web/src/content/pages/hackathons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Hackathons

Code for Philly hosts and co-hosts hackathons throughout the year —
weekend-long events where volunteers form teams around civic-tech
ideas, prototype together, and demo to the community.

## What to expect

- **Open to all.** No prior experience required. We pair newcomers
with mentors so everyone can contribute meaningfully.
- **Real-world themes.** Each event picks a focus — housing,
transit, public health, accessibility — sourced from community
partners with actual problems to solve.
- **Free.** Sponsors cover venue, food, and prizes. Bring a laptop.

## Recent events

We're rebuilding this section as we relaunch the directory. For an
up-to-date calendar, watch the
[#announcements channel in Slack](/chat?channel=announcements) or
sign up for our newsletter.

## Want to organize one?

Reach out at
[hello@codeforphilly.org](mailto:hello@codeforphilly.org). We have
playbooks for venue selection, sponsor outreach, judging panels, and
post-event followups.

---

*This page is awaiting its full content port from the legacy site,
including the event archive. See
[issue tracking](https://github.com/CodeForPhilly/codeforphilly-ng/issues/83)
for status.*
24 changes: 24 additions & 0 deletions apps/web/src/content/pages/leadership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Leadership

Code for Philly is run by a volunteer leadership team that handles
the organizational work — fiscal sponsorship, sponsor relationships,
event logistics, and the shared infrastructure every project depends
on (this directory, our Slack, our meeting spaces).

Day-to-day project decisions stay with each project's maintainers.
Leadership exists to make those projects easier to run, not to direct
them.

## Get in touch

- [Email us](mailto:hello@codeforphilly.org) for partnership,
sponsorship, or press inquiries.
- For operational questions about a specific project, the project's
Slack channel is the fastest path.

---

*This page is awaiting its full content port from the legacy site,
including the current leadership roster. See
[issue tracking](https://github.com/CodeForPhilly/codeforphilly-ng/issues/83)
for status.*
28 changes: 28 additions & 0 deletions apps/web/src/content/pages/mission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Mission

Code for Philly is a civic-tech community building software that
improves life in the Philadelphia region. We're volunteer-run,
project-driven, and welcoming to anyone — technologist or not — who
wants to put their skills toward public good.

## What we do

We organize around **projects** in this directory: groups of volunteers
collaborating on tools, datasets, and services that fill gaps the
public sector or private market hasn't reached. Projects pick their
own scope, tooling, and cadence; we provide the meeting space, the
collaboration channels, and an audience.

## Where to start

- Browse [projects](/projects) to find one whose mission resonates.
- Read [Help Wanted](/help-wanted) to see what specific skills active
projects are looking for.
- Join our [Slack workspace](/chat) — every project has a channel,
and `#welcome` is the place to introduce yourself.

---

*This page is awaiting its full content port from the legacy site.
See [issue tracking](https://github.com/CodeForPhilly/codeforphilly-ng/issues/83)
for status.*
74 changes: 74 additions & 0 deletions apps/web/src/pages/StaticPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Static content pages — `/pages/:slug`.
*
* Content lives in `apps/web/src/content/pages/*.md` and is loaded as
* raw text at build time via Vite's `import.meta.glob`. We use `marked`
* to parse the markdown source on render. This is safe to do client-
* side because the content is bundle-time-static — it can't be
* influenced by users.
*
* Per specs/behaviors/app-shell.md → "The /pages/* URLs serve static
* content pages authored as MDX/Markdown in the code repo".
*/
import { useMemo } from 'react';
import { useParams } from 'react-router';
import { marked } from 'marked';
import { NotFound } from '@/pages/NotFound';
import { cn } from '@/lib/utils';

// Eagerly load all .md files in the content/pages directory as raw text.
// Vite turns this into a slug→source map at build time.
const RAW_PAGES = import.meta.glob('../content/pages/*.md', {
query: '?raw',
import: 'default',
eager: true,
}) as Record<string, string>;

// Strip the directory + extension so the map is keyed by slug only.
const PAGES: Record<string, string> = Object.fromEntries(
Object.entries(RAW_PAGES).map(([path, source]) => {
const match = /\/([^/]+)\.md$/.exec(path);
return [match?.[1] ?? path, source];
}),
);

// Marked config: GFM features (tables, autolinks) on; no breaks-on-newline
// because the content is hand-authored markdown, not chat input.
marked.setOptions({ gfm: true, breaks: false });

export function StaticPage() {
const { slug } = useParams<{ slug: string }>();
const source = slug ? PAGES[slug] : undefined;

const html = useMemo(() => {
if (!source) return null;
// marked.parse() returns string | Promise<string>. With async: false
// (the default in v18+ for synchronous extensions), it's a string.
return marked.parse(source, { async: false }) as string;
}, [source]);

if (!source || html === null) return <NotFound />;

return (
<article className="container mx-auto px-4 py-8 max-w-3xl">
<div
className={cn(
'prose prose-sm max-w-none dark:prose-invert sm:prose-base',
'[&_a]:text-primary [&_a]:underline hover:[&_a]:no-underline',
'[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mb-6',
'[&_h2]:text-xl [&_h2]:font-semibold [&_h2]:mt-8 [&_h2]:mb-3',
'[&_h3]:text-lg [&_h3]:font-semibold [&_h3]:mt-6 [&_h3]:mb-2',
'[&_p]:mb-4 [&_p]:leading-relaxed',
'[&_ul]:list-disc [&_ul]:ml-6 [&_ul]:mb-4',
'[&_ol]:list-decimal [&_ol]:ml-6 [&_ol]:mb-4',
'[&_li]:mb-1',
'[&_hr]:my-8 [&_hr]:border-border',
'[&_em]:italic',
'[&_strong]:font-semibold',
'[&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm',
)}
dangerouslySetInnerHTML={{ __html: html }}
/>
</article>
);
}
57 changes: 57 additions & 0 deletions apps/web/tests/StaticPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { screen } from '@testing-library/react';
import { Routes, Route } from 'react-router';
import { renderScreen } from './test-utils.js';
import { StaticPage } from '../src/pages/StaticPage.js';

describe('StaticPage', () => {
function renderAt(path: string) {
renderScreen(
<Routes>
<Route path="/pages/:slug" element={<StaticPage />} />
</Routes>,
{ initialEntries: [path] },
);
}

it('renders the Mission page H1', () => {
renderAt('/pages/mission');
expect(screen.getByRole('heading', { name: /^mission$/i, level: 1 })).toBeInTheDocument();
});

it('renders the Leadership page', () => {
renderAt('/pages/leadership');
expect(screen.getByRole('heading', { name: /^leadership$/i, level: 1 })).toBeInTheDocument();
});

it('renders the Code of Conduct page', () => {
renderAt('/pages/code-of-conduct');
expect(
screen.getByRole('heading', { name: /^code of conduct$/i, level: 1 }),
).toBeInTheDocument();
});

it('renders the Hackathons page', () => {
renderAt('/pages/hackathons');
expect(screen.getByRole('heading', { name: /^hackathons$/i, level: 1 })).toBeInTheDocument();
});

it('renders NotFound for an unknown slug', () => {
renderAt('/pages/nonexistent-page');
// The NotFound screen renders a recognizable not-found message.
expect(screen.getByText(/page not found|not found/i)).toBeInTheDocument();
});

it('renders markdown headings as semantic h2/h3', () => {
renderAt('/pages/mission');
// The Mission page has a "What we do" h2.
expect(screen.getByRole('heading', { name: /what we do/i, level: 2 })).toBeInTheDocument();
});

it('renders inline links from markdown', () => {
renderAt('/pages/mission');
// [projects](/projects) → an <a href="/projects">projects</a>.
const link = screen.getByRole('link', { name: /^projects$/ });
expect(link).toHaveAttribute('href', '/projects');
});
});
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading