Skip to content

Commit d689431

Browse files
feat(web): two-column /login — password + GitHub side-by-side
Replaces the single-card layout (GitHub button primary, password form hidden behind a disclosure) with a two-column grid: password on the left, GitHub on the right, both fully expanded. Cutover-time users arriving at /login are now overwhelmingly legacy users; the disclosure made the path they need invisible by default. Layout adjusts to max-w-5xl with a centered header; columns stack on mobile via grid-cols-1 md:grid-cols-2. Forgot-password link stays on the password card. WhyGitHub explainer moves under the GitHub card since it's the long-form explainer for that column. Test updates: drops the disclosure-expansion tests (no longer applicable); adds an "both columns visible" assertion as the entry- point check. Other tests (gated submit, inline 401 error) stay as-is since the field selectors haven't changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dfd4521 commit d689431

2 files changed

Lines changed: 64 additions & 89 deletions

File tree

apps/web/src/pages/LoginPlaceholder.tsx

Lines changed: 54 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
import { Button } from '@/components/ui/button';
1111
import { Input } from '@/components/ui/input';
1212
import { Label } from '@/components/ui/label';
13-
import { Separator } from '@/components/ui/separator';
1413
import { useAuth } from '@/hooks/useAuth';
1514
import { api, ApiError } from '@/lib/api';
1615

@@ -125,52 +124,64 @@ export function LoginPlaceholder() {
125124
? `/api/auth/github/start?return=${encodeURIComponent(returnPath)}`
126125
: '/api/auth/github/start';
127126

128-
return (
129-
<div className="flex justify-center py-16 px-4">
130-
<Card className="w-full max-w-[480px]">
131-
<CardHeader className="text-center">
132-
<CardTitle className="text-2xl">Sign in to Code for Philly</CardTitle>
133-
<CardDescription className="mt-2 text-sm">
134-
We use <strong>GitHub</strong> for sign-in. If you do not have a
135-
GitHub account yet, it is free and takes about a minute.
136-
</CardDescription>
137-
<WhyGitHub />
138-
</CardHeader>
139-
<CardContent className="flex flex-col gap-4">
140-
{errorCode && ERROR_MESSAGES[errorCode] && (
141-
<div
142-
role="alert"
143-
className="text-sm text-destructive bg-destructive/10 rounded-md px-4 py-3"
144-
>
145-
{ERROR_MESSAGES[errorCode]}
146-
</div>
147-
)}
127+
const handleLegacySuccess = () => {
128+
const target =
129+
returnPath && returnPath.startsWith('/') ? returnPath : '/';
130+
void navigate(target, { replace: true });
131+
};
148132

149-
<Button asChild size="lg" className="w-full gap-2">
150-
<a href={startUrl}>
151-
<GitHubIcon />
152-
Sign in with GitHub
153-
</a>
154-
</Button>
133+
return (
134+
<div className="container mx-auto px-4 py-12 max-w-5xl">
135+
<div className="text-center mb-8">
136+
<h1 className="text-3xl font-bold">Sign in to Code for Philly</h1>
137+
<p className="text-sm text-muted-foreground mt-2">
138+
Returning member? Use the password you had before our 2026 switch to
139+
GitHub. New here? Sign in with GitHub.
140+
</p>
141+
</div>
155142

156-
<Separator />
143+
{errorCode && ERROR_MESSAGES[errorCode] && (
144+
<div
145+
role="alert"
146+
className="text-sm text-destructive bg-destructive/10 rounded-md px-4 py-3 mb-6 max-w-2xl mx-auto"
147+
>
148+
{ERROR_MESSAGES[errorCode]}
149+
</div>
150+
)}
157151

158-
<p className="text-sm text-muted-foreground text-center">
159-
<strong>Returning Code for Philly member?</strong> If you had an
160-
account before our 2026 switch to GitHub sign-in, you can sign in
161-
with your old password below — or use GitHub if your old email
162-
matches.
163-
</p>
152+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
153+
<Card>
154+
<CardHeader>
155+
<CardTitle>Returning member</CardTitle>
156+
<CardDescription>
157+
Sign in with the username (or email) and password you used at
158+
codeforphilly.org before our switch to GitHub sign-in.
159+
</CardDescription>
160+
</CardHeader>
161+
<CardContent>
162+
<LegacyPasswordLogin onSuccess={handleLegacySuccess} />
163+
</CardContent>
164+
</Card>
164165

165-
<LegacyPasswordLogin
166-
onSuccess={() => {
167-
const target =
168-
returnPath && returnPath.startsWith('/') ? returnPath : '/';
169-
void navigate(target, { replace: true });
170-
}}
171-
/>
172-
</CardContent>
173-
</Card>
166+
<Card>
167+
<CardHeader>
168+
<CardTitle>New here?</CardTitle>
169+
<CardDescription>
170+
We use GitHub for all new sign-ups. It is free and takes about a
171+
minute if you do not have an account yet.
172+
</CardDescription>
173+
</CardHeader>
174+
<CardContent className="flex flex-col gap-4">
175+
<Button asChild size="lg" className="w-full gap-2">
176+
<a href={startUrl}>
177+
<GitHubIcon />
178+
Sign in with GitHub
179+
</a>
180+
</Button>
181+
<WhyGitHub />
182+
</CardContent>
183+
</Card>
184+
</div>
174185
</div>
175186
);
176187
}
@@ -180,7 +191,6 @@ interface LegacyPasswordLoginProps {
180191
}
181192

182193
function LegacyPasswordLogin({ onSuccess }: LegacyPasswordLoginProps) {
183-
const [open, setOpen] = useState(false);
184194
const [usernameOrEmail, setUsernameOrEmail] = useState('');
185195
const [password, setPassword] = useState('');
186196
const [submitting, setSubmitting] = useState(false);
@@ -213,19 +223,6 @@ function LegacyPasswordLogin({ onSuccess }: LegacyPasswordLoginProps) {
213223
}
214224
}
215225

216-
if (!open) {
217-
return (
218-
<button
219-
type="button"
220-
onClick={() => setOpen(true)}
221-
className="text-sm text-muted-foreground hover:text-foreground underline-offset-2 hover:underline self-center"
222-
aria-expanded="false"
223-
>
224-
🔑 Or sign in with your Code for Philly password
225-
</button>
226-
);
227-
}
228-
229226
return (
230227
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
231228
<div className="space-y-1.5">

apps/web/tests/LoginPlaceholder.test.tsx

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,42 +29,25 @@ describe('LoginPlaceholder', () => {
2929
);
3030
}
3131

32-
it('renders the primary GitHub button + a collapsed password disclosure', async () => {
32+
it('renders both columns side-by-side: password form on the left + GitHub button on the right', async () => {
3333
render();
34+
// Password form fields are visible immediately — no disclosure to expand.
3435
await waitFor(() => {
35-
expect(screen.getByRole('link', { name: /sign in with github/i })).toBeInTheDocument();
36+
expect(screen.getByLabelText(/username or email/i)).toBeInTheDocument();
3637
});
37-
// Disclosure exists but is closed — fields are not yet in the DOM
38-
expect(
39-
screen.getByRole('button', { name: /sign in with your code for philly password/i }),
40-
).toBeInTheDocument();
41-
expect(screen.queryByLabelText(/username or email/i)).not.toBeInTheDocument();
42-
});
43-
44-
it('expanding the disclosure reveals the password form', async () => {
45-
render();
46-
await waitFor(() => {
47-
expect(
48-
screen.getByRole('button', { name: /sign in with your code for philly password/i }),
49-
).toBeInTheDocument();
50-
});
51-
fireEvent.click(
52-
screen.getByRole('button', { name: /sign in with your code for philly password/i }),
53-
);
54-
expect(screen.getByLabelText(/username or email/i)).toBeInTheDocument();
5538
expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument();
39+
expect(screen.getByRole('button', { name: /^sign in$/i })).toBeInTheDocument();
40+
// GitHub button is also visible.
41+
expect(screen.getByRole('link', { name: /sign in with github/i })).toBeInTheDocument();
42+
// Forgot-password link is on the password card.
43+
expect(screen.getByRole('link', { name: /forgot your password/i })).toBeInTheDocument();
5644
});
5745

5846
it('keeps submit disabled until both fields are filled', async () => {
5947
render();
6048
await waitFor(() => {
61-
expect(
62-
screen.getByRole('button', { name: /sign in with your code for philly password/i }),
63-
).toBeInTheDocument();
49+
expect(screen.getByLabelText(/username or email/i)).toBeInTheDocument();
6450
});
65-
fireEvent.click(
66-
screen.getByRole('button', { name: /sign in with your code for philly password/i }),
67-
);
6851
const submitBtn = screen.getByRole('button', { name: /^sign in$/i });
6952
expect(submitBtn).toBeDisabled();
7053

@@ -100,13 +83,8 @@ describe('LoginPlaceholder', () => {
10083

10184
render();
10285
await waitFor(() => {
103-
expect(
104-
screen.getByRole('button', { name: /sign in with your code for philly password/i }),
105-
).toBeInTheDocument();
86+
expect(screen.getByLabelText(/username or email/i)).toBeInTheDocument();
10687
});
107-
fireEvent.click(
108-
screen.getByRole('button', { name: /sign in with your code for philly password/i }),
109-
);
11088
fireEvent.change(screen.getByLabelText(/username or email/i), {
11189
target: { value: 'jane' },
11290
});

0 commit comments

Comments
 (0)