Skip to content

Commit

Permalink
Merge pull request #31 from nathanvale/kent
Browse files Browse the repository at this point in the history
Merge remote-tracking branch 'epic-web'
  • Loading branch information
nathanvale committed Jun 29, 2023
2 parents a9de442 + 0726ce5 commit acd69bf
Show file tree
Hide file tree
Showing 79 changed files with 4,309 additions and 2,059 deletions.
7 changes: 0 additions & 7 deletions .dockerignore

This file was deleted.

5 changes: 5 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ap-southeast-2
# move Dockerfile to root
- name: 🚚 Move Dockerfile
run: |
mv ./other/Dockerfile ./Dockerfile
mv ./other/.dockerignore ./.dockerignore
- name: 🚀 Deploy Staging
if: ${{ github.ref == 'refs/heads/dev' }}
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ source library (appropriate attribution is appreciated). Then come back and make
a PR to use your new library.

NOTE: Actual adoption of your library is not guaranteed. Offloading maintenance
and adaptability is a delecate balance.
and adaptability is a delicate balance.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
npx create-remix@latest --typescript --install --template epicweb-dev/epic-stack
```

[![The Epic Stack](https://github.com/epicweb-dev/epic-stack/assets/1500684/1b00286c-aa3d-44b2-9ef2-04f694eb3592)](https://www.epicweb.dev/epic-stack)
[![The Epic Stack](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/246885449-1b00286c-aa3d-44b2-9ef2-04f694eb3592.png)](https://www.epicweb.dev/epic-stack)

[The Epic Stack](https://www.epicweb.dev/epic-stack)

<hr />

## Watch Kent's Introduction to The Epic Stack

[![screenshot of a YouTube video](https://github.com/epicweb-dev/epic-stack/assets/1500684/6beafa78-41c6-47e1-b999-08d3d3e5cb57)](https://www.youtube.com/watch?v=yMK5SVRASxM)
[![screenshot of a YouTube video](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/242088051-6beafa78-41c6-47e1-b999-08d3d3e5cb57.png)](https://www.youtube.com/watch?v=yMK5SVRASxM)

["The Epic Stack" by Kent C. Dodds at #RemixConf 2023 💿](https://www.youtube.com/watch?v=yMK5SVRASxM)

Expand Down
158 changes: 158 additions & 0 deletions app/components/forms.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useInputEvent } from '@conform-to/react'
import React, { useId, useRef } from 'react'
import { Input } from '~/components/ui/input.tsx'
import { Label } from '~/components/ui/label.tsx'
import { Checkbox } from '~/components/ui/checkbox.tsx'
import { Textarea } from '~/components/ui/textarea.tsx'
import { AlertCircle } from 'lucide-react'
import { Alert, AlertTitle, AlertDescription } from './ui/alert.tsx'

export type ListOfErrors = Array<string | null | undefined> | null | undefined

export function ErrorList({
id,
errors,
title = 'Error',
}: {
errors?: ListOfErrors
id?: string
title?: string
}) {
const errorsToRender = errors?.filter(Boolean)
if (!errorsToRender?.length) return null
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>{title}</AlertTitle>
<AlertDescription>
<ul id={id} className="space-y-2">
{errorsToRender.map(e => (
<li key={e} className="text-sm font-medium text-destructive">
{e}
</li>
))}
</ul>
</AlertDescription>
</Alert>
)
}

export function Field({
labelProps,
inputProps,
errors,
className,
}: {
labelProps: Omit<React.LabelHTMLAttributes<HTMLLabelElement>, 'className'>
inputProps: Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className'>
errors?: ListOfErrors
className?: string
}) {
const fallbackId = useId()
const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Input
id={id}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...inputProps}
/>
<div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}

export function TextareaField({
labelProps,
textareaProps,
errors,
className,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
textareaProps: React.InputHTMLAttributes<HTMLTextAreaElement>
errors?: ListOfErrors
className?: string
}) {
const fallbackId = useId()
const id = textareaProps.id ?? textareaProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Textarea
id={id}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...textareaProps}
/>
<div className="px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}

export function CheckboxField({
labelProps,
buttonProps,
errors,
className,
}: {
labelProps: JSX.IntrinsicElements['label']
buttonProps: any
errors?: ListOfErrors
className?: string
}) {
const fallbackId = useId()
const buttonRef = useRef<HTMLButtonElement>(null)
// To emulate native events that Conform listen to:
// See https://conform.guide/integrations
const control = useInputEvent({
// Retrieve the checkbox element by name instead as Radix does not expose the internal checkbox element
// See https://github.com/radix-ui/primitives/discussions/874
ref: () =>
buttonRef.current?.form?.elements.namedItem(buttonProps.name ?? ''),
onFocus: () => buttonRef.current?.focus(),
})
const id = buttonProps.id ?? buttonProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<div className="flex gap-2">
<Checkbox
id={id}
ref={buttonRef}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...buttonProps}
onCheckedChange={state => {
control.change(Boolean(state.valueOf()))
buttonProps.onCheckedChange?.(state)
}}
onFocus={event => {
control.focus()
buttonProps.onFocus?.(event)
}}
onBlur={event => {
control.blur()
buttonProps.onBlur?.(event)
}}
type="button"
/>
<label
htmlFor={id}
{...labelProps}
className="text-body-xs self-center text-muted-foreground"
/>
</div>
<div className="px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}
110 changes: 53 additions & 57 deletions app/components/site-header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { useSubmit, Form, Link } from '@remix-run/react'
import { useRef } from 'react'
import { getUserImgSrc } from '~/utils/misc.ts'
import { useUser } from '~/utils/user.ts'
import { ButtonLink } from './ui/button-link.tsx'
import { ThemeSwitch } from '~/routes/resources+/theme/index.tsx'

import { useRef } from 'react'
import { Button } from '~/components/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuTrigger,
} from '~/components/ui/dropdown-menu.tsx'
import { Icon } from '~/components/ui/icon.tsx'
export interface SiteHeaderProps {
user?: {
id: string
Expand Down Expand Up @@ -50,69 +57,58 @@ function UserDropdown() {
const submit = useSubmit()
const formRef = useRef<HTMLFormElement>(null)
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Link
to={`/users/${user.username}`}
// this is for progressive enhancement
onClick={e => e.preventDefault()}
className="bg-brand-500 hover:bg-brand-400 focus:bg-brand-400 radix-state-open:bg-brand-400 flex items-center gap-2 rounded-full py-2 pl-2 pr-4 outline-none"
>
<img
className="h-8 w-8 rounded-full object-cover"
alt={user.name ?? user.username}
src={getUserImgSrc(user.imageId)}
/>
<span className="text-body-sm font-bold text-white">
{user.name ?? user.username}
</span>
</Link>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
sideOffset={8}
align="start"
className="flex flex-col rounded-3xl bg-[#323232]"
>
<DropdownMenu.Item asChild>
<Link
prefetch="intent"
to={`/users/${user.username}`}
className="hover:bg-brand-500 radix-highlighted:bg-brand-500 rounded-t-3xl px-7 py-5 outline-none"
>
Profile
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button asChild variant="secondary">
<Link
to={`/users/${user.username}`}
// this is for progressive enhancement
onClick={e => e.preventDefault()}
className="flex items-center gap-2"
>
<img
className="h-8 w-8 rounded-full object-cover"
alt={user.name ?? user.username}
src={getUserImgSrc(user.imageId)}
/>
<span className="text-body-sm font-bold">
{user.name ?? user.username}
</span>
</Link>
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent sideOffset={8} align="start">
<DropdownMenuItem asChild>
<Link prefetch="intent" to={`/users/${user.username}`}>
<Icon className="text-body-md" name="avatar">
Profile
</Icon>
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<Link
prefetch="intent"
to={`/users/${user.username}/notes`}
className="hover:bg-brand-500 radix-highlighted:bg-brand-500 px-7 py-5 outline-none"
>
Notes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link prefetch="intent" to={`/users/${user.username}/notes`}>
<Icon className="text-body-md" name="pencil-2">
Notes
</Icon>
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item
</DropdownMenuItem>
<DropdownMenuItem
asChild
// this prevents the menu from closing before the form submission is completed
onSelect={event => {
event.preventDefault()
submit(formRef.current)
}}
>
<Form
action="/logout"
method="POST"
className="radix-highlighted:bg-brand-500 rounded-b-3xl outline-none"
ref={formRef}
>
<button type="submit" className="px-7 py-5">
Logout
</button>
<Form action="/logout" method="POST" ref={formRef}>
<Icon className="text-body-md" name="exit">
<button type="submit">Logout</button>
</Icon>
</Form>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
)
}
7 changes: 7 additions & 0 deletions app/components/ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# shadcn/ui

Some components in this directory are downloaded via the
[shadcn/ui](https://ui.shadcn.com) [CLI](https://ui.shadcn.com/docs/cli). Feel
free to customize them to your needs. It's important to know that shadcn/ui is
not a library of components you install, but instead it's a registry of prebuilt
components which you can download and customize.

0 comments on commit acd69bf

Please sign in to comment.