Skip to content

Commit

Permalink
feat: form styling
Browse files Browse the repository at this point in the history
  • Loading branch information
stagas committed Oct 4, 2024
1 parent 27cbbc0 commit a3e182f
Show file tree
Hide file tree
Showing 15 changed files with 226 additions and 116 deletions.
9 changes: 5 additions & 4 deletions api/actions/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ export async function listUsers(ctx: Context) {
admins(ctx)
return (await db
.selectFrom('user')
.select(['nick', 'email', 'emailVerified', 'oauthGithub', 'createdAt', 'updatedAt'])
.selectAll()
.execute()
).map(item =>
[item.nick, item] as const
)
).map(item => {
item.password = '✓' // mask password
return [item.nick, item] as const
})
}

actions.post.deleteUser = deleteUser
Expand Down
15 changes: 7 additions & 8 deletions api/actions/login-register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,13 @@ export async function logout(ctx: Context) {
}

actions.post.forgotPassword = forgotPassword
export async function forgotPassword(ctx: Context, nickOrEmail: string) {
ctx.log('Forgot password for:', nickOrEmail)
export async function forgotPassword(ctx: Context, email: string) {
ctx.log('Forgot password for:', email)

const user = await getUser(nickOrEmail)
const user = await getUserByEmail(email)

if (!user) {
ctx.log('Forgot password user does not exist:', nickOrEmail)
ctx.log('Forgot password user does not exist:', email)
// fake a delay that would have been a call to an email service
await timeout(2000 + Math.random() * 5000)
return
Expand Down Expand Up @@ -295,15 +295,14 @@ If you did not request a password reset, simply ignore this email.`,
}
}

actions.get.getResetPasswordUser = getResetPasswordUser
export async function getResetPasswordUser(_ctx: Context, token: string) {
actions.get.getResetPasswordUserNick = getResetPasswordUserNick
export async function getResetPasswordUserNick(_ctx: Context, token: string) {
const result = await kv.get<string>(['resetPassword', token])

if (result.value) {
const user = await getUserByNick(result.value)
if (user) {
user.password = null
return user
return user.nick
}
}

Expand Down
5 changes: 5 additions & 0 deletions api/schemas/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export const UserLogin = z.object({
password: z.string(),
})

export type UserForgot = z.infer<typeof UserForgot>
export const UserForgot = z.object({
email: z.string(),
})

export type UserSession = z.infer<typeof UserSession>
export const UserSession = z.object({
nick: z.string(),
Expand Down
122 changes: 83 additions & 39 deletions src/comp/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,106 @@
import { refs, Sigui } from 'sigui'
import { UserLogin } from '../../api/schemas/user.ts'
import { Sigui } from 'sigui'
import { UserForgot, UserLogin } from '../../api/schemas/user.ts'
import * as actions from '../rpc/login-register.ts'
import { parseForm } from '../util/parse-form.ts'
import { Link } from '../ui/Link.tsx'
import { Input, Label } from '../ui/index.ts'

export function Login() {
using $ = Sigui()

const info = $({
mode: 'login' as 'login' | 'forgot',
forgotEmail: null as null | string,

Check warning on line 13 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L12-L13

Added lines #L12 - L13 were not covered by tests
error: ''
})

function onSubmit(ev: Event & { target: HTMLFormElement, submitter: HTMLElement }) {
function submitLogin(ev: Event & { target: HTMLFormElement, submitter: HTMLElement }) {

Check warning on line 17 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L17

Added line #L17 was not covered by tests
ev.preventDefault()
const userLogin = parseForm(ev.target, UserLogin)
actions
.login(userLogin)
.then(actions.loginUser)
.catch(err => info.error = err.message)
return false
}

Check warning on line 25 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L20-L25

Added lines #L20 - L25 were not covered by tests

if (ev.submitter === refs.forgot) {
actions
.forgotPassword(userLogin.nickOrEmail)
.then(() => {
alert('Check your email for a password reset link.')
})
}
else {
actions
.login(userLogin)
.then(actions.loginUser)
.catch(err => info.error = err.message)
}
function submitForgot(ev: Event & { target: HTMLFormElement, submitter: HTMLElement }) {
ev.preventDefault()
const userForgot = parseForm(ev.target, UserForgot)
actions
.forgotPassword(userForgot.email)
.then(() => {
info.forgotEmail = userForgot.email
})

Check warning on line 34 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L27-L34

Added lines #L27 - L34 were not covered by tests
return false
}

return <form method="post" onsubmit={onSubmit}>
<label>
Nick or Email <input
name="nickOrEmail"
required
spellcheck="false"
autocomplete="nickname"
/>
</label>
return <div>
{() => {
switch (info.mode) {
case 'login':
return <form method="post" onsubmit={submitLogin}>
<h2>Login</h2>

<div class="flex flex-col sm:items-end gap-1">
<Label text="Nick or Email">
<Input
name="nickOrEmail"
required
spellcheck="false"
autocomplete="nickname"
/>
</Label>

<Label text="Password">
<Input
name="password"
type="password"
required
autocomplete="current-password"
/>
</Label>

<div class="flex flex-row items-center justify-end gap-2">
<Link onclick={() => info.mode = 'forgot'}>Forgot password</Link>
<button type="submit">Login</button>
</div>

Check warning on line 67 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L38-L67

Added lines #L38 - L67 were not covered by tests

<br />
<span>{() => info.error}</span>
</div>

Check warning on line 70 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L69-L70

Added lines #L69 - L70 were not covered by tests

<label>
Password <input
name="password"
type="password"
required
autocomplete="current-password"
/>
</label>
</form>

Check warning on line 72 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L72

Added line #L72 was not covered by tests

<br />
case 'forgot':
if (info.forgotEmail) {
const site = `https://${info.forgotEmail.split('@')[1]}`
return <div>Done! <a href={site}>Check your email</a> for a reset password link.</div>
}
else {
return <form method="post" onsubmit={submitForgot}>
<h2>Forgot Password</h2>

Check warning on line 81 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L74-L81

Added lines #L74 - L81 were not covered by tests

<button type="submit">Login</button>
<div class="flex flex-col sm:items-end gap-1">
<Label text="Email">
<Input
name="email"
type="email"
required
spellcheck="false"
autocomplete="email"
/>
</Label>

Check warning on line 92 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L83-L92

Added lines #L83 - L92 were not covered by tests

<button ref="forgot" type="submit">Forgot password</button>
<div class="flex flex-row items-center gap-2">
<button type="submit">Send reset link</button>
<span>or <Link onclick={() => info.mode = 'login'}>login using password</Link></span>
</div>

Check warning on line 97 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L94-L97

Added lines #L94 - L97 were not covered by tests

{() => info.error}
</form>
<span>{() => info.error}</span>
</div>
</form>
}
}
}}

Check warning on line 104 in src/comp/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Login.tsx#L99-L104

Added lines #L99 - L104 were not covered by tests
</div>
}
34 changes: 34 additions & 0 deletions src/comp/OAuthLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { on } from 'utils'
import { whoami } from '../rpc/login-register.ts'
import { state } from '../state.ts'

export function OAuthLogin() {
function oauthLogin(provider: string) {
const h = 700
const w = 500
const x = window.outerWidth / 2 + window.screenX - (w / 2)
const y = window.outerHeight / 2 + window.screenY - (h / 2)

const url = new URL(`${location.origin}/oauth/popup`)
url.searchParams.set('provider', provider)
const popup = window.open(
url,
'oauth',
`width=${w}, height=${h}, top=${y}, left=${x}`
)

if (!popup) alert('Something went wrong')

on(window, 'storage', () => {
popup!.close()
if (localStorage.oauth?.startsWith('complete')) {
whoami().then(user => state.user = user)
}
else {
alert('OAuth failed.\n\nTry logging in using a different method.')
}
}, { once: true })
}

return <button onclick={() => oauthLogin('github')}>Proceed with GitHub</button>
}
57 changes: 37 additions & 20 deletions src/comp/Register.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Sigui } from 'sigui'
import { UserRegister } from '../../api/schemas/user.ts'
import * as actions from '../rpc/login-register.ts'
import { Input, Label } from '../ui/index.ts'
import { parseForm } from '../util/parse-form.ts'

export function Register() {
Expand All @@ -20,26 +21,42 @@ export function Register() {
}

return <form method="post" onsubmit={onSubmit}>
<label>
Nick <input id="nick" name="nick" required spellcheck="false" autocomplete="nickname" />
</label>
<h2>Register</h2>

<div class="flex flex-col sm:items-end gap-1">
<Label text="Nick">
<Input
name="nick"
required
spellcheck="false"
autocomplete="nickname"
/>
</Label>

<Label text="Email">
<Input
name="email"
type="email"
required
autocomplete="email"
/>
</Label>

<Label text="Password">
<Input
name="password"
type="password"
required
autocomplete="new-password"
/>
</Label>

<div class="flex flex-row items-center justify-end gap-2">
<button type="submit">Register</button>
</div>

<span>{() => info.error}</span>
</div>

Check warning on line 59 in src/comp/Register.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/Register.tsx#L24-L59

Added lines #L24 - L59 were not covered by tests

<br />

<label>
Email <input id="email" name="email" type="email" required autocomplete="email" />
</label>

<br />

<label>
Password <input id="password" name="password" type="password" required autocomplete="new-password" />
</label>

<br />

<button type="submit">Register</button>

{() => info.error}
</form>
}
20 changes: 11 additions & 9 deletions src/comp/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Sigui } from 'sigui'
import { UserResetPassword, type User } from '../../api/schemas/user.ts'
import { UserResetPassword } from '../../api/schemas/user.ts'
import * as actions from '../rpc/login-register.ts'
import { parseForm } from '../util/parse-form.ts'
import { go } from '../ui/Link.tsx'
import { parseForm } from '../util/parse-form.ts'

export function ResetPassword() {
using $ = Sigui()

const info = $({
user: null as User | null,
nick: null as null | string,

Check warning on line 11 in src/comp/ResetPassword.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/ResetPassword.tsx#L11

Added line #L11 was not covered by tests
error: ''
})

Expand All @@ -17,8 +17,10 @@ export function ResetPassword() {
const { token, password } = parseForm(ev.target, UserResetPassword)
actions
.changePassword(token, password)
.then(actions.loginUser)
.then(() => go('/'))
.then(session => {
go('/')
actions.loginUser(session)
})

Check warning on line 23 in src/comp/ResetPassword.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/ResetPassword.tsx#L20-L23

Added lines #L20 - L23 were not covered by tests
.catch(err => info.error = err.message)
return false
}
Expand All @@ -27,14 +29,14 @@ export function ResetPassword() {
if (!token) return <div>Token not found</div>

actions
.getResetPasswordUser(token)
.then(user => info.user = user)
.getResetPasswordUserNick(token)
.then(nick => info.nick = nick)

Check warning on line 33 in src/comp/ResetPassword.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/ResetPassword.tsx#L32-L33

Added lines #L32 - L33 were not covered by tests
.catch(err => info.error = err.message)

return <div>{() => info.user ? <div>
return <div>{() => info.nick ? <div>

Check warning on line 36 in src/comp/ResetPassword.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/ResetPassword.tsx#L36

Added line #L36 was not covered by tests
<h1>Reset Password</h1>

Hello {info.user.nick}! Please enter your new password below:
Hello {info.nick}! Please enter your new password below:

Check warning on line 39 in src/comp/ResetPassword.tsx

View check run for this annotation

Codecov / codecov/patch

src/comp/ResetPassword.tsx#L39

Added line #L39 was not covered by tests

<form method="post" onsubmit={onSubmit}>
<input type="hidden" name="token" value={token} />
Expand Down
Loading

0 comments on commit a3e182f

Please sign in to comment.