Skip to content

Update to latest epic stack #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 0 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,6 @@ jobs:
file: 'fly.toml'
field: 'app'

# move Dockerfile to root
- name: 🚚 Move Dockerfile
run: |
mv ./other/Dockerfile ./Dockerfile
mv ./other/.dockerignore ./.dockerignore

- name: 🎈 Setup Fly
uses: superfly/flyctl-actions/[email protected]

Expand Down
File renamed without changes
2 changes: 1 addition & 1 deletion app/components/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function GeneralErrorBoundary({
</p>
),
statusHandlers,
unexpectedErrorHandler = error => <p>{getErrorMessage(error)}</p>,
unexpectedErrorHandler = (error) => <p>{getErrorMessage(error)}</p>,
}: {
defaultStatusHandler?: StatusHandler
statusHandlers?: Record<number, StatusHandler>
Expand Down
8 changes: 4 additions & 4 deletions app/components/forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function ErrorList({
if (!errorsToRender?.length) return null
return (
<ul id={id} className="flex flex-col gap-1">
{errorsToRender.map(e => (
{errorsToRender.map((e) => (
<li key={e} className="text-[10px] text-foreground-destructive">
{e}
</li>
Expand Down Expand Up @@ -214,15 +214,15 @@ export function CheckboxField({
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
checked={input.value === checkedValue}
onCheckedChange={state => {
onCheckedChange={(state) => {
input.change(state.valueOf() ? checkedValue : '')
buttonProps.onCheckedChange?.(state)
}}
onFocus={event => {
onFocus={(event) => {
input.focus()
buttonProps.onFocus?.(event)
}}
onBlur={event => {
onBlur={(event) => {
input.blur()
buttonProps.onBlur?.(event)
}}
Expand Down
2 changes: 1 addition & 1 deletion app/components/search-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function SearchBar({
method="GET"
action="/users"
className="flex flex-wrap items-center justify-center gap-2"
onChange={e => autoSubmit && handleFormChange(e.currentTarget)}
onChange={(e) => autoSubmit && handleFormChange(e.currentTarget)}
>
<div className="flex-1">
<Label htmlFor={id} className="sr-only">
Expand Down
2 changes: 1 addition & 1 deletion app/components/ui/input-otp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const InputOTPSlot = React.forwardRef<
<div
ref={ref}
className={cn(
'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
isActive && 'z-10 ring-2 ring-ring ring-offset-background',
className,
)}
Expand Down
2 changes: 1 addition & 1 deletion app/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-input-invalid',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-input-invalid',
className,
)}
ref={ref}
Expand Down
2 changes: 1 addition & 1 deletion app/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-input-invalid',
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-input-invalid',
className,
)}
ref={ref}
Expand Down
13 changes: 4 additions & 9 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PassThrough } from 'stream'
import { PassThrough } from 'node:stream'
import {
createReadableStreamFromReadable,
type LoaderFunctionArgs,
Expand All @@ -10,19 +10,16 @@ import * as Sentry from '@sentry/remix'
import chalk from 'chalk'
import { isbot } from 'isbot'
import { renderToPipeableStream } from 'react-dom/server'
import { init as initCron } from './utils/cron.server.ts'
import { getEnv, init as initEnv } from './utils/env.server.ts'
import { init, getEnv } from './utils/env.server.ts'
import { getInstanceInfo } from './utils/litefs.server.ts'
import { NonceProvider } from './utils/nonce-provider.ts'
import { makeTimings } from './utils/timing.server.ts'

const ABORT_DELAY = 5000

initEnv()
init()
global.ENV = getEnv()

void initCron()

type DocRequestArgs = Parameters<HandleDocumentRequestFunction>

export default async function handleRequest(...args: DocRequestArgs) {
Expand Down Expand Up @@ -99,8 +96,6 @@ export function handleError(
error: unknown,
{ request }: LoaderFunctionArgs | ActionFunctionArgs,
): void {
// Skip capturing if the request is aborted as Remix docs suggest
// Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
if (request.signal.aborted) {
return
}
Expand All @@ -113,7 +108,7 @@ export function handleError(
true,
)
} else {
console.error(chalk.red(error))
console.error(error)
Sentry.captureException(error)
}
}
13 changes: 7 additions & 6 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
} from '@remix-run/react'
import { withSentry } from '@sentry/remix'
import { HoneypotProvider } from 'remix-utils/honeypot/react'
import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png'
import faviconAssetUrl from './assets/favicons/favicon.svg'
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
import { EpicProgress } from './components/progress-bar.tsx'
import { useToast } from './components/toaster.tsx'
Expand All @@ -38,19 +40,18 @@ export const links: LinksFunction = () => {
return [
// Preload svg sprite as a resource to avoid render blocking
{ rel: 'preload', href: iconsHref, as: 'image' },
{ rel: 'mask-icon', href: '/favicons/mask-icon.svg' },
{
rel: 'alternate icon',
type: 'image/png',
href: '/favicons/favicon-32x32.png',
rel: 'icon',
href: '/favicon.ico',
sizes: '48x48',
},
{ rel: 'apple-touch-icon', href: '/favicons/apple-touch-icon.png' },
{ rel: 'icon', type: 'image/svg+xml', href: faviconAssetUrl },
{ rel: 'apple-touch-icon', href: appleTouchIconAssetUrl },
{
rel: 'manifest',
href: '/site.webmanifest',
crossOrigin: 'use-credentials',
} as const, // necessary to make typescript happy
{ rel: 'icon', type: 'image/svg+xml', href: '/favicons/favicon.svg' },
{ rel: 'stylesheet', href: tailwindStyleSheetUrl },
].filter(Boolean)
}
Expand Down
4 changes: 2 additions & 2 deletions app/routes/_seo+/sitemap[.]xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { type ServerBuild, type LoaderFunctionArgs } from '@remix-run/node'
import { getDomainUrl } from '#app/utils/misc.tsx'

export async function loader({ request, context }: LoaderFunctionArgs) {
const serverBuild = (await context.serverBuild) as ServerBuild
return generateSitemap(request, serverBuild.routes, {
const serverBuild = (await context.serverBuild) as { build: ServerBuild }
return generateSitemap(request, serverBuild.build.routes, {
siteUrl: getDomainUrl(request),
headers: {
'Cache-Control': `public, max-age=${60 * 5}`,
Expand Down
2 changes: 1 addition & 1 deletion app/routes/resources+/healthcheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
fetch(`${new URL(request.url).protocol}${host}`, {
method: 'HEAD',
headers: { 'X-Healthcheck': 'true' },
}).then(r => {
}).then((r) => {
if (!r.ok) return Promise.reject(r)
}),
])
Expand Down
25 changes: 17 additions & 8 deletions app/routes/resources+/theme-switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { useForm, getFormProps } from '@conform-to/react'
import { parseWithZod } from '@conform-to/zod'
import { invariantResponse } from '@epic-web/invariant'
import { json, type ActionFunctionArgs } from '@remix-run/node'
import { useFetcher, useFetchers } from '@remix-run/react'
import { redirect, useFetcher, useFetchers } from '@remix-run/react'
import { ServerOnly } from 'remix-utils/server-only'
import { z } from 'zod'
import { Icon } from '#app/components/ui/icon.tsx'
import { useHints } from '#app/utils/client-hints.tsx'
Expand All @@ -11,6 +12,8 @@ import { type Theme, setTheme } from '#app/utils/theme.server.ts'

const ThemeFormSchema = z.object({
theme: z.enum(['system', 'light', 'dark']),
// this is useful for progressive enhancement
redirectTo: z.string().optional(),
})

export async function action({ request }: ActionFunctionArgs) {
Expand All @@ -21,12 +24,16 @@ export async function action({ request }: ActionFunctionArgs) {

invariantResponse(submission.status === 'success', 'Invalid theme received')

const { theme } = submission.value
const { theme, redirectTo } = submission.value

const responseInit = {
headers: { 'set-cookie': setTheme(theme) },
}
return json({ result: submission.reply() }, responseInit)
if (redirectTo) {
return redirect(redirectTo, responseInit)
} else {
return json({ result: submission.reply() }, responseInit)
}
}

export function ThemeSwitch({
Expand All @@ -35,6 +42,7 @@ export function ThemeSwitch({
userPreference?: Theme | null
}) {
const fetcher = useFetcher<typeof action>()
const requestInfo = useRequestInfo()

const [form] = useForm({
id: 'theme-switch',
Expand Down Expand Up @@ -69,6 +77,11 @@ export function ThemeSwitch({
{...getFormProps(form)}
action="/resources/theme-switch"
>
<ServerOnly>
{() => (
<input type="hidden" name="redirectTo" value={requestInfo.path} />
)}
</ServerOnly>
<input type="hidden" name="theme" value={nextMode} />
<div className="flex gap-2">
<button
Expand All @@ -82,14 +95,10 @@ export function ThemeSwitch({
)
}

/**
* If the user's changing their theme mode preference, this will return the
* value it's being changed to.
*/
export function useOptimisticThemeMode() {
const fetchers = useFetchers()
const themeFetcher = fetchers.find(
f => f.formAction === '/resources/theme-switch',
(f) => f.formAction === '/resources/theme-switch',
)

if (themeFetcher && themeFetcher.formData) {
Expand Down
16 changes: 8 additions & 8 deletions app/utils/cache.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'fs'
import fs from 'node:fs'
import {
cachified as baseCachified,
verboseReporter,
Expand Down Expand Up @@ -64,8 +64,8 @@ export const lruCache = {
})
return value
},
get: key => lru.get(key),
delete: key => lru.delete(key),
get: (key) => lru.get(key),
delete: (key) => lru.delete(key),
} satisfies Cache

const cacheEntrySchema = z.object({
Expand Down Expand Up @@ -116,7 +116,7 @@ export const cache: CachifiedCache = {
void updatePrimaryCacheValue({
key,
cacheValue: entry,
}).then(response => {
}).then((response) => {
if (!response.ok) {
console.error(
`Error updating cache value for key "${key}" on primary instance (${primaryInstance}): ${response.status} ${response.statusText}`,
Expand All @@ -135,7 +135,7 @@ export const cache: CachifiedCache = {
void updatePrimaryCacheValue({
key,
cacheValue: undefined,
}).then(response => {
}).then((response) => {
if (!response.ok) {
console.error(
`Error deleting cache value for key "${key}" on primary instance (${primaryInstance}): ${response.status} ${response.statusText}`,
Expand All @@ -151,7 +151,7 @@ export async function getAllCacheKeys(limit: number) {
sqlite: cacheDb
.prepare('SELECT key FROM cache LIMIT ?')
.all(limit)
.map(row => (row as { key: string }).key),
.map((row) => (row as { key: string }).key),
lru: [...lru.keys()],
}
}
Expand All @@ -161,8 +161,8 @@ export async function searchCacheKeys(search: string, limit: number) {
sqlite: cacheDb
.prepare('SELECT key FROM cache WHERE key LIKE ? LIMIT ?')
.all(`%${search}%`, limit)
.map(row => (row as { key: string }).key),
lru: [...lru.keys()].filter(key => key.includes(search)),
.map((row) => (row as { key: string }).key),
lru: [...lru.keys()].filter((key) => key.includes(search)),
}
}

Expand Down
20 changes: 10 additions & 10 deletions app/utils/misc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
if (typeof color === 'string') {
colors.push(key)
} else {
const colorGroup = Object.keys(color).map(subKey =>
const colorGroup = Object.keys(color).map((subKey) =>
subKey === 'DEFAULT' ? '' : subKey,
)
colors.push({ [key]: colorGroup })
Expand Down Expand Up @@ -184,7 +184,7 @@
function callAll<Args extends Array<unknown>>(
...fns: Array<((...args: Args) => unknown) | undefined>
) {
return (...args: Args) => fns.forEach(fn => fn?.(...args))
return (...args: Args) => fns.forEach((fn) => fn?.(...args))
}

/**
Expand All @@ -205,7 +205,7 @@
safeDelayMs = 150,
}: { safeDelayMs?: number } = {}) {
const [doubleCheck, setDoubleCheck] = useState(false)
const [canClick, setCanClick] = useState(false)

Check warning on line 208 in app/utils/misc.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'canClick' is assigned a value but never used. Allowed unused vars must match /^ignored/u

useEffect(() => {
if (doubleCheck) {
Expand All @@ -225,23 +225,23 @@
() => setDoubleCheck(false)

const onClick: React.ButtonHTMLAttributes<HTMLButtonElement>['onClick'] =
doubleCheck && canClick
doubleCheck
? undefined
: e => {
: (e) => {
e.preventDefault()
setDoubleCheck(true)
}

const onKeyUp: React.ButtonHTMLAttributes<HTMLButtonElement>['onKeyUp'] =
e => {
if (e.key === 'Escape') {
setDoubleCheck(false)
}
const onKeyUp: React.ButtonHTMLAttributes<HTMLButtonElement>['onKeyUp'] = (
e,
) => {
if (e.key === 'Escape') {
setDoubleCheck(false)
}
}

return {
...props,
'data-safe-delay': doubleCheck && !canClick,
onBlur: callAll(onBlur, props?.onBlur),
onClick: callAll(onClick, props?.onClick),
onKeyUp: callAll(onKeyUp, props?.onKeyUp),
Expand Down
3 changes: 2 additions & 1 deletion app/utils/misc.use-double-check.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
<output>Default Prevented: {defaultPrevented}</output>
<button
{...dc.getButtonProps({
onClick: e => setDefaultPrevented(e.defaultPrevented ? 'yes' : 'no'),
onClick: (e) =>
setDefaultPrevented(e.defaultPrevented ? 'yes' : 'no'),
})}
>
{dc.doubleCheck ? 'You sure?' : 'Click me'}
Expand All @@ -39,7 +40,7 @@
await user.click(button)
expect(button).toHaveTextContent('You sure?')
expect(status).toHaveTextContent('Default Prevented: yes')
expect(button).toHaveAttribute('data-safe-delay', 'true')

Check failure on line 43 in app/utils/misc.use-double-check.test.tsx

View workflow job for this annotation

GitHub Actions / ⚡ Vitest

app/utils/misc.use-double-check.test.tsx > prevents default on the first click, and does not on the second

Error: expect(element).toHaveAttribute("data-safe-delay", "true") // element.getAttribute("data-safe-delay") === "true" Expected the element to have attribute: data-safe-delay="true" Received: null ❯ app/utils/misc.use-double-check.test.tsx:43:17

// clicking it during the safe delay does nothing
await user.click(button)
Expand Down
4 changes: 2 additions & 2 deletions app/utils/timing.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function getServerTimeHeader(timings?: Timings) {
}, 0)
.toFixed(1)
const desc = timingInfos
.map(t => t.desc)
.map((t) => t.desc)
.filter(Boolean)
.join(' & ')
return [
Expand Down Expand Up @@ -101,7 +101,7 @@ export function cachifiedTimingReporter<Value>(
`${key} cache retrieval`,
)
let getFreshValueTimer: ReturnType<typeof createTimer> | undefined
return event => {
return (event) => {
switch (event.name) {
case 'getFreshValueStart':
getFreshValueTimer = createTimer(
Expand Down
Loading
Loading