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
13 changes: 9 additions & 4 deletions apps/web/src/components/ActivityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ function UpdateCard({ update }: { update: ProjectUpdateResponse }) {
Update #{update.number}
</Link>
</div>
<span title={formatAbsoluteDate(update.createdAt)} className="text-xs text-muted-foreground">
<time
dateTime={update.createdAt}
title={formatAbsoluteDate(update.createdAt)}
className="text-xs text-muted-foreground"
>
{formatRelativeTime(update.createdAt)}
</span>
</time>
</div>

{update.author && (
Expand Down Expand Up @@ -69,9 +73,9 @@ function BuzzCard({ buzz }: { buzz: ProjectBuzzResponse }) {
{buzz.project.title}
</Link>
<span> · Buzz · </span>
<span title={formatAbsoluteDate(buzz.publishedAt)}>
<time dateTime={buzz.publishedAt} title={formatAbsoluteDate(buzz.publishedAt)}>
{formatAbsoluteDate(buzz.publishedAt, { month: 'short', day: 'numeric', year: 'numeric' })}
</span>
</time>
</span>
</div>

Expand All @@ -89,6 +93,7 @@ function BuzzCard({ buzz }: { buzz: ProjectBuzzResponse }) {
<h3 className="text-base font-semibold mb-0.5">
<a href={buzz.url} target="_blank" rel="noopener noreferrer" className="hover:text-primary">
{buzz.headline}
<span className="sr-only"> (opens in new tab)</span>
</a>
</h3>
<p className="text-xs text-muted-foreground mb-2">{hostname}</p>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/AppFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function AppFooter() {
<a
key={href}
href={href}
aria-label={label}
aria-label={`${label} (opens in new tab)`}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
Expand All @@ -206,6 +206,7 @@ export function AppFooter() {
className="hover:text-foreground transition-colors"
>
Open source &mdash; view this site on GitHub
<span className="sr-only"> (opens in new tab)</span>
</a>
</div>
</div>
Expand Down
166 changes: 108 additions & 58 deletions apps/web/src/components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,10 @@ function AboutDropdown() {
);
}

// `block` so each link fills its row: inside the sheet's <li>s an inline
// anchor would shrink the tap target to the width of its text.
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
`text-sm font-medium transition-colors hover:text-primary ${
`block text-sm font-medium transition-colors hover:text-primary ${
isActive ? 'text-primary' : 'text-muted-foreground'
}`;

Expand All @@ -197,7 +199,7 @@ function GitHubLink() {
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="Code for Philly on GitHub"
aria-label="Code for Philly on GitHub (opens in new tab)"
>
<GitHubIcon />
</a>
Expand Down Expand Up @@ -235,20 +237,33 @@ export function AppHeader() {

{/* Desktop content cluster. The parent gap is the only source of
spacing between children — no per-child margins. */}
{/* A nav is a list of destinations — the <ul>/<li> is what tells a
screen reader how many there are and where you are in them. The
flex/gap spacing moves to the <ul>; the <li>s contribute none. */}
<nav
aria-label="Primary navigation"
className="hidden md:flex items-center gap-2 ml-4 flex-1"
className="hidden md:block ml-4 flex-1"
>
<NavLink to="/projects" className={navLinkClass}>
Projects
</NavLink>
<NavLink to="/help-wanted" className={navLinkClass}>
Help Wanted
</NavLink>
<NavLink to="/members" className={navLinkClass}>
Members
</NavLink>
<AboutDropdown />
<ul className="flex items-center gap-2">
<li>
<NavLink to="/projects" className={navLinkClass}>
Projects
</NavLink>
</li>
<li>
<NavLink to="/help-wanted" className={navLinkClass}>
Help Wanted
</NavLink>
</li>
<li>
<NavLink to="/members" className={navLinkClass}>
Members
</NavLink>
</li>
<li>
<AboutDropdown />
</li>
</ul>
</nav>

{/* Desktop utility cluster: GitHub, search, auth, then the Volunteer
Expand Down Expand Up @@ -295,58 +310,93 @@ export function AppHeader() {
</SheetHeader>
{/* min-h-0 + overflow-y-auto so the list stays reachable on
short viewports instead of overflowing the panel. */}
{/* Three lists with the separators and the group heading
between them, rather than one list interrupted by
non-list children. The nav keeps the flex column so the
gap-2 rhythm between groups is unchanged. */}
<nav
aria-label="Mobile navigation"
className="flex flex-col gap-2 px-4 min-h-0 overflow-y-auto"
>
<NavLink to="/projects" className={navLinkClass}>
Projects
</NavLink>
<NavLink to="/help-wanted" className={navLinkClass}>
Help Wanted
</NavLink>
<NavLink to="/members" className={navLinkClass}>
Members
</NavLink>
<ul className="flex flex-col gap-2">
<li>
<NavLink to="/projects" className={navLinkClass}>
Projects
</NavLink>
</li>
<li>
<NavLink to="/help-wanted" className={navLinkClass}>
Help Wanted
</NavLink>
</li>
<li>
<NavLink to="/members" className={navLinkClass}>
Members
</NavLink>
</li>
</ul>
<Separator />
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wide">
{/* SheetTitle ("Menu") renders a Radix DialogTitle, i.e. an
h2 — so this group label is an h3, not a styled <p>. */}
<h3 className="text-xs text-muted-foreground font-medium uppercase tracking-wide">
About
</p>
<NavLink to="/pages/mission" className={navLinkClass}>
Mission
</NavLink>
<NavLink to="/pages/leadership" className={navLinkClass}>
Leadership
</NavLink>
<NavLink to="/pages/code-of-conduct" className={navLinkClass}>
Code of Conduct
</NavLink>
<NavLink to="/pages/hackathons" className={navLinkClass}>
Hackathons
</NavLink>
<NavLink to="/sponsor" className={navLinkClass}>
Sponsor
</NavLink>
<a
href="mailto:hello@codeforphilly.org"
className="text-sm font-medium text-muted-foreground hover:text-primary"
onClick={() => setMobileOpen(false)}
>
Contact
</a>
</h3>
<ul className="flex flex-col gap-2">
<li>
<NavLink to="/pages/mission" className={navLinkClass}>
Mission
</NavLink>
</li>
<li>
<NavLink to="/pages/leadership" className={navLinkClass}>
Leadership
</NavLink>
</li>
<li>
<NavLink to="/pages/code-of-conduct" className={navLinkClass}>
Code of Conduct
</NavLink>
</li>
<li>
<NavLink to="/pages/hackathons" className={navLinkClass}>
Hackathons
</NavLink>
</li>
<li>
<NavLink to="/sponsor" className={navLinkClass}>
Sponsor
</NavLink>
</li>
<li>
<a
href="mailto:hello@codeforphilly.org"
className="block text-sm font-medium text-muted-foreground hover:text-primary"
onClick={() => setMobileOpen(false)}
>
Contact
</a>
</li>
</ul>
<Separator />
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-muted-foreground hover:text-primary"
onClick={() => setMobileOpen(false)}
>
GitHub
</a>
<NavLink to="/volunteer" className={navLinkClass}>
Volunteer
</NavLink>
<ul className="flex flex-col gap-2">
<li>
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="block text-sm font-medium text-muted-foreground hover:text-primary"
onClick={() => setMobileOpen(false)}
>
GitHub
<span className="sr-only"> (opens in new tab)</span>
</a>
</li>
<li>
<NavLink to="/volunteer" className={navLinkClass}>
Volunteer
</NavLink>
</li>
</ul>
</nav>
<Separator />
<div className="px-4 pb-4">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function Breadcrumbs({ items }: BreadcrumbsProps) {
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<li key={item.label} className="flex items-center gap-1">
<li key={`${index}-${item.label}`} className="flex items-center gap-1">
{index > 0 && (
<span aria-hidden="true" className="text-muted-foreground/50">
&rsaquo;
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/components/ConnectGitHubBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function ConnectGitHubBanner() {
if (dismissed) return null;

return (
<>
<div
role="region"
aria-label="Connect GitHub"
Expand Down Expand Up @@ -58,5 +59,14 @@ export function ConnectGitHubBanner() {
</Button>
</div>
</div>
{/* The banner mounts only once auth resolves, and a landmark arriving
after first paint is never announced on its own — so mirror the
headline in a live region (the Sponsor / ProfileEdit idiom). It stays
a region rather than becoming one: a late-mounted status container
is not read reliably either, and it would swallow the two buttons. */}
<span role="status" className="sr-only">
Connect your GitHub account
</span>
</>
);
}
6 changes: 5 additions & 1 deletion apps/web/src/components/FacetSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, type ReactNode } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { TagChip } from '@/components/TagChip';
import { Link } from 'react-router';
Expand All @@ -12,6 +12,8 @@ interface FacetSidebarProps {
tabs?: Array<'topic' | 'tech' | 'event'>;
limit?: number;
className?: string;
/** Extra filter controls rendered inside the same `Filters` landmark. */
children?: ReactNode;
}

const NS_LABELS = {
Expand Down Expand Up @@ -40,6 +42,7 @@ export function FacetSidebar({
tabs = ['topic', 'tech', 'event'],
limit = 10,
className,
children,
}: FacetSidebarProps) {
const [tab, setTab] = useState<string>(tabs[0] ?? 'topic');
const activeTagSet = new Set(activeTags);
Expand Down Expand Up @@ -103,6 +106,7 @@ export function FacetSidebar({
);
})}
</Tabs>
{children}
</aside>
);
}
9 changes: 7 additions & 2 deletions apps/web/src/components/HelpWantedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { PersonAvatar } from '@/components/PersonAvatar';
import { MarkdownView } from '@/components/MarkdownView';
import { ExpressInterestModal } from '@/components/modals/ExpressInterestModal';
import { useAuth } from '@/hooks/useAuth';
import { formatRelativeTime } from '@/lib/time';
import { formatAbsoluteDate, formatRelativeTime } from '@/lib/time';
import type { HelpWantedRoleResponse } from '@/lib/api';

interface HelpWantedCardProps {
Expand Down Expand Up @@ -64,7 +64,12 @@ export function HelpWantedCard({ role, showProjectLink = true }: HelpWantedCardP
<span>·</span>
</>
)}
<span>posted {formatRelativeTime(role.createdAt)}</span>
<span>
posted{' '}
<time dateTime={role.createdAt} title={formatAbsoluteDate(role.createdAt)}>
{formatRelativeTime(role.createdAt)}
</time>
</span>
</div>

{isSignedIn ? (
Expand Down
Loading