Skip to content

Commit

Permalink
[WIP]: Login page
Browse files Browse the repository at this point in the history
  • Loading branch information
wking-io committed May 2, 2024
1 parent 24ba498 commit ccb7612
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 86 deletions.
12 changes: 6 additions & 6 deletions app/components/forms.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useInputControl } from '@conform-to/react'
import React, { useId } from 'react'
import { Checkbox, type CheckboxProps } from './ui/checkbox.tsx'
import { Label } from './ui/fieldset.tsx'
import { Label, Field } from './ui/fieldset.tsx'
import { Input } from './ui/input.tsx'
import { Textarea } from './ui/textarea.tsx'

Expand All @@ -27,7 +27,7 @@ export function ErrorList({
)
}

export function Field({
export function TextField({
labelProps,
inputProps,
errors,
Expand All @@ -42,7 +42,7 @@ export function Field({
const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<Field className={className}>
<Label htmlFor={id} {...labelProps} />
<Input
id={id}
Expand All @@ -53,7 +53,7 @@ export function Field({
<div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
</Field>
)
}

Expand All @@ -72,7 +72,7 @@ export function TextareaField({
const id = textareaProps.id ?? textareaProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<Field className={className}>
<Label htmlFor={id} {...labelProps} />
<Textarea
id={id}
Expand All @@ -83,7 +83,7 @@ export function TextareaField({
<div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
</Field>
)
}

Expand Down
217 changes: 144 additions & 73 deletions app/components/ui/status-button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {
Button as HeadlessButton,
type ButtonProps as HeadlessButtonProps,
} from '@headlessui/react'
import { type LinkProps, Link } from '@remix-run/react'
import clsx from 'clsx'
import * as React from 'react'
import { useSpinDelay } from 'spin-delay'
import { cn } from '#app/utils/misc.tsx'
import { Icon } from './icon.tsx'
import {
Tooltip,
Expand All @@ -10,82 +14,149 @@ import {
TooltipTrigger,
} from './tooltip.tsx'

const styles = {
base: [
// Base
'group relative isolate inline-flex items-center justify-center gap-4 font-medium border',

// Sizing
'py-2 pl-5 pr-6',

// Focus
'focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500',

// Disabled
'data-[disabled]:opacity-50 data-[disabled]:pointer-events-none',
],
solid: ['border-transparent'],
outline: ['border-foreground'],
colors: {
default: 'bg-foreground text-background',
},
}

type Props =
| { color?: keyof typeof styles.colors; outline?: never }
| { color?: never; outline: true }

export const StatusButton = React.forwardRef<
HTMLButtonElement,
React.ComponentPropsWithoutRef<'button'> &
React.PropsWithChildren<{
className?: string
status: 'pending' | 'success' | 'error' | 'idle'
message?: string | null
spinDelay?: Parameters<typeof useSpinDelay>[1]
}>
>(({ message, status, className, children, spinDelay, ...props }, ref) => {
const delayedPending = useSpinDelay(status === 'pending', {
delay: 400,
minDuration: 300,
...spinDelay,
})
const companion = {
pending: delayedPending ? (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="update" className="animate-spin" />
</div>
) : null,
success: (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="check" />
</div>
),
error: null,
idle: null,
}[status]

return (
<button
ref={ref}
className={cn(
status === 'error' && 'border-red',
'group relative flex justify-center gap-4 border border-transparent bg-primary py-2 pl-5 pr-6 font-medium text-primary-foreground',
className,
)}
{...props}
HeadlessButtonProps &
React.PropsWithChildren<
Props & {
className?: string
status: 'pending' | 'success' | 'error' | 'idle'
message?: string | null
spinDelay?: Parameters<typeof useSpinDelay>[1]
}
>
<svg
className="absolute -bottom-px -right-px h-[24px] w-[24px] rotate-90 sm:-top-px sm:bottom-auto sm:rotate-0"
viewBox="0 0 4 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
className="fill-current text-background"
x="0"
y="0"
width="4"
height="4"
/>
<path
d="M 0 0 H 2 V 1 H 3 V 2 H 4 V 4 H 0 V 0 Z"
className="fill-current text-foreground"
>(
(
{
message,
status,
className,
children,
spinDelay,
outline,
color,
...props
},
ref,
) => {
const delayedPending = useSpinDelay(status === 'pending', {
delay: 400,
minDuration: 300,
...spinDelay,
})
const companion = {
pending: delayedPending ? (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="update" className="animate-spin" />
</div>
) : null,
success: (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="check" />
</div>
),
error: null,
idle: null,
}[status]

const classes = clsx(
className,
status === 'error' && 'border-red',
styles.base,
outline
? styles.outline
: clsx(styles.solid, styles.colors[color ?? 'default']),
)

return (
<HeadlessButton ref={ref} className={classes} {...props}>
{outline ? (
<svg
className="absolute -right-px -top-px z-10 h-[24px] w-[24px]"
viewBox="0 0 4 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 2 0 H 2 V 1 H 3 V 2 H 4 V 0 H 0 Z"
className="fill-background"
strokeWidth="0"
/>
<path
d="M 2 0 H 2 V 1 H 3 V 2 H 4"
className="stroke-foreground"
strokeWidth=".125"
/>
</svg>
) : (
<svg
className="absolute -bottom-px -right-px h-[24px] w-[24px] rotate-90 sm:-top-px sm:bottom-auto sm:rotate-0"
viewBox="0 0 4 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
className="fill-background"
x="0"
y="0"
width="4"
height="4"
/>
<path
d="M 0 0 H 2 V 1 H 3 V 2 H 4 V 4 H 0 V 0 Z"
className="fill-foreground"
/>
</svg>
)}
<HoverSVG
className={clsx(
outline ? 'group-hover:opacity-50' : 'group-hover:opacity-100',
'absolute -bottom-px -right-px -scale-y-100 opacity-0 transition duration-200 sm:-top-px sm:bottom-auto sm:scale-y-100',
)}
/>
</svg>
<HoverSVG className="absolute -bottom-px -right-px -scale-y-100 opacity-0 transition duration-200 group-hover:opacity-100 sm:-top-px sm:bottom-auto sm:scale-y-100" />

<span className="relative inline-flex items-center justify-center gap-2">
{children}
</span>
{message ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{companion}</TooltipTrigger>
<TooltipContent>{message}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
companion
)}
</button>
)
})
<span className="relative inline-flex items-center justify-center gap-2">
{children}
</span>
{message ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{companion}</TooltipTrigger>
<TooltipContent>{message}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
companion
)}
</HeadlessButton>
)
},
)
StatusButton.displayName = 'Button'

export const MarketingButton = React.forwardRef<
Expand All @@ -98,7 +169,7 @@ export const MarketingButton = React.forwardRef<
return (
<Link
ref={ref}
className={cn(
className={clsx(
'group relative flex justify-center gap-4 border border-transparent bg-primary py-2 pl-5 pr-6 font-medium text-primary-foreground',
className,
)}
Expand Down
12 changes: 8 additions & 4 deletions app/routes/_auth+/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Form, Link, useActionData, useSearchParams } from '@remix-run/react'
import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx'
import { CheckboxField, ErrorList, TextField } from '#app/components/forms.tsx'
import { StatusButton } from '#app/components/ui/status-button.js'
import { PasswordSchema, HandleSchema } from '#app/utils/account-validation.js'
import { login, requireAnonymous } from '#app/utils/auth.server.ts'
Expand All @@ -21,6 +21,7 @@ import {
} from '#app/utils/connections.tsx'
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
import { useIsPending } from '#app/utils/misc.tsx'
import { seoData } from '#app/utils/seo.js'
import { handleNewSession } from './login.server.ts'

const LoginFormSchema = z.object({
Expand Down Expand Up @@ -110,7 +111,7 @@ export default function LoginPage() {
<div className="mx-auto w-full max-w-md px-8">
<Form method="POST" {...getFormProps(form)}>
<HoneypotInputs />
<Field
<TextField
labelProps={{ children: 'Handle' }}
inputProps={{
...getInputProps(fields.handle, { type: 'text' }),
Expand All @@ -121,7 +122,7 @@ export default function LoginPage() {
errors={fields.handle.errors}
/>

<Field
<TextField
labelProps={{ children: 'Password' }}
inputProps={{
...getInputProps(fields.password, {
Expand Down Expand Up @@ -188,7 +189,10 @@ export default function LoginPage() {
}

export const meta: MetaFunction = () => {
return [{ title: 'Login to Epic Notes' }]
return seoData({
title: 'Login • Craft Lab',
description: 'Login to the Craft Lab community for Design Engineers.',
})
}

export function ErrorBoundary() {
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_auth+/verify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const VerifySchema = z.object({

export const meta: MetaFunction = () =>
seoData({
title: 'Craft Lab Verify one-time code',
title: 'Craft Lab Verify one-time code',
description:
'Help us build a space where we learn and share everything about our craft and have fun doing it.',
})
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_marketing+/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { submitWaitlist } from '#app/utils/waitlist.server.js'

export const meta: MetaFunction = () =>
seoData({
title: 'Craft Lab A community built for Design Engineers',
title: 'Craft Lab A community built for Design Engineers',
description:
'Help us build a space where we learn and share everything about our craft and have fun doing it.',
})
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_marketing+/waitlist.success.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { seoData } from '#app/utils/seo.js'

export const meta: MetaFunction = () =>
seoData({
title: 'Craft Lab A community built for Incredible Design Engineers',
title: 'Craft Lab A community built for Incredible Design Engineers',
description:
'Help us build a space where we learn and share everything about our craft and have fun doing it.',
})
Expand Down
1 change: 1 addition & 0 deletions app/utils/connections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function ProviderConnectionForm({
<StatusButton
type="submit"
className="w-full"
outline
status={isPending ? 'pending' : 'idle'}
>
<span className="inline-flex items-center gap-1.5">
Expand Down

0 comments on commit ccb7612

Please sign in to comment.