Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
87d6e40
use openimg
andrelandgraf Feb 19, 2025
64d45ff
add isAboveFold
andrelandgraf Feb 19, 2025
ec643c5
fmt and review comments
andrelandgraf Feb 19, 2025
4d9eb4e
Update app/routes/users+/$username.tsx
andrelandgraf Feb 19, 2025
3e848a4
Update app/routes/users+/index.tsx
andrelandgraf Feb 19, 2025
5369c82
Update app/routes/users+/$username_+/notes.tsx
andrelandgraf Feb 19, 2025
9fd9a73
Update app/routes/settings+/profile.index.tsx
andrelandgraf Feb 19, 2025
3ae3947
Update app/routes/users+/$username_+/notes.$noteId.tsx
andrelandgraf Feb 19, 2025
f7aac86
Update app/routes/users+/$username_+/__note-editor.tsx
andrelandgraf Feb 19, 2025
cb47439
image docs & image optimization decision doc
andrelandgraf Feb 20, 2025
3d1d0ad
wording changes
andrelandgraf Feb 20, 2025
505685b
fix cache location
andrelandgraf Feb 22, 2025
d9dac31
update docs, add S3 URL to allowlist
andrelandgraf Feb 22, 2025
bc2f913
fix msw warnings and place the images not in a root dir during dev
kentcdodds Feb 22, 2025
2dc20d5
this is what I want
kentcdodds Feb 22, 2025
8cdd396
remove unnecessary note since we have tigris now
kentcdodds Feb 22, 2025
6c26161
a few other updates
kentcdodds Feb 22, 2025
bc1dd11
openimg v0.2
andrelandgraf Feb 22, 2025
ecea2fd
latest openimg, fix types
andrelandgraf Feb 22, 2025
8b68dca
use getSrc to create custom src string for bucket requests
andrelandgraf Feb 23, 2025
2106862
support img download
andrelandgraf Feb 23, 2025
990d8d0
clean up code
andrelandgraf Feb 23, 2025
220c376
fix in openimg
andrelandgraf Feb 23, 2025
1224f4d
reduce code by moving src processing
andrelandgraf Feb 23, 2025
c099b7f
Migrate to objectKey for image references across application
kentcdodds Feb 24, 2025
45b5d3e
Add comment to getImgSrc explaining things
kentcdodds Feb 24, 2025
6e9aa19
Remove redundant Content-Disposition header setting in images route
kentcdodds Feb 24, 2025
c73ba61
fix comment
kentcdodds Feb 24, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ node_modules
/playwright/.cache/
/tests/fixtures/email/
/tests/fixtures/uploaded/
/tests/fixtures/openimg/
/coverage

/other/cache.db
Expand Down
5 changes: 4 additions & 1 deletion app/components/user-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DropdownMenuItem,
} from './ui/dropdown-menu'
import { Icon } from './ui/icon'
import { Img } from 'openimg/react'

export function UserDropdown() {
const user = useUser()
Expand All @@ -25,10 +26,12 @@ export function UserDropdown() {
onClick={(e) => e.preventDefault()}
className="flex items-center gap-2"
>
<img
<Img
className="h-8 w-8 rounded-full object-cover"
alt={user.name ?? user.username}
src={getUserImgSrc(user.image?.id)}
width={256}
height={256}
/>
<span className="text-body-sm font-bold">
{user.name ?? user.username}
Expand Down
10 changes: 7 additions & 3 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useMatches,
} from 'react-router'
import { HoneypotProvider } from 'remix-utils/honeypot/react'
import { OpenImgContextProvider } from 'openimg/react'
import { type Route } from './+types/root.ts'
import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png'
import faviconAssetUrl from './assets/favicons/favicon.svg'
Expand All @@ -33,7 +34,7 @@ import { prisma } from './utils/db.server.ts'
import { getEnv } from './utils/env.server.ts'
import { pipeHeaders } from './utils/headers.server.ts'
import { honeypot } from './utils/honeypot.server.ts'
import { combineHeaders, getDomainUrl } from './utils/misc.tsx'
import { combineHeaders, getDomainUrl, getImgSrc } from './utils/misc.tsx'
import { useNonce } from './utils/nonce-provider.ts'
import { type Theme, getTheme } from './utils/theme.server.ts'
import { makeTimings, time } from './utils/timing.server.ts'
Expand Down Expand Up @@ -194,7 +195,10 @@ function App() {
useToast(data.toast)

return (
<>
<OpenImgContextProvider
optimizerEndpoint="/resources/images"
getSrc={getImgSrc}
>
<div className="flex min-h-screen flex-col justify-between">
<header className="container py-6">
<nav className="flex flex-wrap items-center justify-between gap-4 sm:flex-nowrap md:gap-8">
Expand Down Expand Up @@ -226,7 +230,7 @@ function App() {
</div>
<EpicToaster closeButton position="top-center" theme={theme} />
<EpicProgress />
</>
</OpenImgContextProvider>
)
}

Expand Down
6 changes: 3 additions & 3 deletions app/routes/resources+/download-user-data.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { requireUserId } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { getDomainUrl } from '#app/utils/misc.tsx'
import { getDomainUrl, getNoteImgSrc, getUserImgSrc } from '#app/utils/misc.tsx'
import { type Route } from './+types/download-user-data.ts'

export async function loader({ request }: Route.LoaderArgs) {
Expand Down Expand Up @@ -47,14 +47,14 @@ export async function loader({ request }: Route.LoaderArgs) {
image: user.image
? {
...user.image,
url: `${domain}/resources/user-images/${user.image.id}`,
url: domain + getUserImgSrc(user.image.id),
}
: null,
notes: user.notes.map((note) => ({
...note,
images: note.images.map((image) => ({
...image,
url: `${domain}/resources/note-images/${image.id}`,
url: domain + getNoteImgSrc(image.id),
})),
})),
},
Expand Down
95 changes: 95 additions & 0 deletions app/routes/resources+/images.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { invariantResponse } from '@epic-web/invariant'
import { getImgResponse, ImgSource } from 'openimg/node'
import { prisma } from '#app/utils/db.server.ts'
import { getDomainUrl } from '#app/utils/misc.tsx'
import { getSignedGetRequestInfo } from '#app/utils/storage.server.ts'
import { type Route } from './+types/images'

export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url)
const searchParams = url.searchParams

const headers = new Headers()
headers.set('Cache-Control', 'public, max-age=31536000, immutable')

const userImageId = searchParams.get('userImageId')
const noteImageId = searchParams.get('noteImageId')
const bucketImgId = userImageId || noteImageId
if (bucketImgId) {
headers.set('Content-Disposition', 'inline')
}

return getImgResponse(request, {
headers,
allowlistedOrigins: [
getDomainUrl(request),
process.env.AWS_ENDPOINT_URL_S3,
].filter(Boolean),
cacheFolder:
process.env.NODE_ENV === 'production'
? '/data/images'
: './tests/fixtures/openimg',
getImgSource: () => {
if (bucketImgId) {
if (userImageId) {
return handleUserImage(userImageId)
}
if (noteImageId) {
return handleNoteImage(noteImageId)
}
}
const src = searchParams.get('src')
invariantResponse(src, 'src query parameter is required', { status: 400 })
if (URL.canParse(src)) {
// Fetch image from external URL; will be matched against allowlist
return {
type: 'fetch',
url: src,
}
}
// Retrieve image from filesystem (public folder)
if (src.startsWith('/assets')) {
// Files managed by Vite
return {
type: 'fs',
path: '.' + src,
}
}
// Fallback to files in public folder
return {
type: 'fs',
path: './public' + src,
}
},
})
}

async function handleUserImage(userImageId: string): Promise<ImgSource> {
const userImage = await prisma.userImage.findUnique({
where: { id: userImageId },
select: { objectKey: true },
})
invariantResponse(userImage, 'User image not found', { status: 404 })

const { url, headers } = getSignedGetRequestInfo(userImage.objectKey)
return {
type: 'fetch',
url,
headers,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need the fetch request you make to include these headers.

I also need getImgSource to support returning a promise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Available via openimg v0.2.0! 🎉

}

async function handleNoteImage(noteImageId: string): Promise<ImgSource> {
const noteImage = await prisma.noteImage.findUnique({
where: { id: noteImageId },
select: { objectKey: true },
})
invariantResponse(noteImage, 'Note image not found', { status: 404 })

const { url, headers } = getSignedGetRequestInfo(noteImage.objectKey)
return {
type: 'fetch',
url,
headers,
}
}
24 changes: 0 additions & 24 deletions app/routes/resources+/note-images.$imageId.tsx

This file was deleted.

24 changes: 0 additions & 24 deletions app/routes/resources+/user-images.$imageId.tsx

This file was deleted.

6 changes: 5 additions & 1 deletion app/routes/settings+/profile.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react'
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import { invariantResponse } from '@epic-web/invariant'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import { Img } from 'openimg/react'
import { data, Link, useFetcher } from 'react-router'
import { z } from 'zod'
import { ErrorList, Field } from '#app/components/forms.tsx'
Expand Down Expand Up @@ -101,10 +102,13 @@ export default function EditUserProfile({ loaderData }: Route.ComponentProps) {
<div className="flex flex-col gap-12">
<div className="flex justify-center">
<div className="relative h-52 w-52">
<img
<Img
src={getUserImgSrc(loaderData.user.image?.id)}
alt={loaderData.user.username}
className="h-full w-full rounded-full object-cover"
width={832}
height={832}
isAboveFold
/>
<Button
asChild
Expand Down
5 changes: 4 additions & 1 deletion app/routes/users+/$username.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Link,
useLoaderData,
} from 'react-router'
import { Img } from 'openimg/react'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { Spacer } from '#app/components/spacer.tsx'
import { Button } from '#app/components/ui/button.tsx'
Expand Down Expand Up @@ -48,10 +49,12 @@ export default function ProfileRoute() {
<div className="relative w-52">
<div className="absolute -top-40">
<div className="relative">
<img
<Img
src={getUserImgSrc(data.user.image?.id)}
alt={userDisplayName}
className="h-52 w-52 rounded-full object-cover"
width={832}
height={832}
/>
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion app/routes/users+/$username_+/__note-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { StatusButton } from '#app/components/ui/status-button.tsx'
import { Textarea } from '#app/components/ui/textarea.tsx'
import { cn, getNoteImgSrc, useIsPending } from '#app/utils/misc.tsx'
import { type Info } from './+types/notes.$noteId_.edit.ts'
import { Img } from 'openimg/react'

const titleMinLength = 1
const titleMaxLength = 100
Expand Down Expand Up @@ -188,10 +189,12 @@ function ImageChooser({ meta }: { meta: FieldMetadata<ImageFieldset> }) {
>
{previewImage ? (
<div className="relative">
<img
<Img
src={previewImage}
alt={altText ?? ''}
className="h-32 w-32 rounded-lg object-cover"
width={512}
height={512}
/>
{existingImage ? null : (
<div className="pointer-events-none absolute -right-0.5 -top-0.5 rotate-12 rounded-sm bg-secondary px-2 py-1 text-xs text-secondary-foreground shadow-md">
Expand Down
5 changes: 4 additions & 1 deletion app/routes/users+/$username_+/notes.$noteId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { redirectWithToast } from '#app/utils/toast.server.ts'
import { userHasPermission, useOptionalUser } from '#app/utils/user.ts'
import { type Route, type Info } from './+types/notes.$noteId.ts'
import { type Info as notesInfo } from './+types/notes.ts'
import { Img } from 'openimg/react'

export async function loader({ params }: Route.LoaderArgs) {
const note = await prisma.note.findUnique({
Expand Down Expand Up @@ -106,10 +107,12 @@ export default function NoteRoute({
{loaderData.note.images.map((image) => (
<li key={image.id}>
<a href={getNoteImgSrc(image.id)}>
<img
<Img
src={getNoteImgSrc(image.id)}
alt={image.altText ?? ''}
className="h-32 w-32 rounded-lg object-cover"
width={512}
height={512}
/>
</a>
</li>
Expand Down
5 changes: 4 additions & 1 deletion app/routes/users+/$username_+/notes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { invariantResponse } from '@epic-web/invariant'
import { Link, NavLink, Outlet } from 'react-router'
import { Img } from 'openimg/react'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
import { prisma } from '#app/utils/db.server.ts'
Expand Down Expand Up @@ -39,10 +40,12 @@ export default function NotesRoute({ loaderData }: Route.ComponentProps) {
to={`/users/${loaderData.owner.username}`}
className="flex flex-col items-center justify-center gap-2 bg-muted pb-4 pl-8 pr-4 pt-12 lg:flex-row lg:justify-start lg:gap-4"
>
<img
<Img
src={getUserImgSrc(loaderData.owner.image?.id)}
alt={ownerDisplayName}
className="h-16 w-16 rounded-full object-cover lg:h-24 lg:w-24"
width={256}
height={256}
/>
<h1 className="text-center text-base font-bold md:text-lg lg:text-left lg:text-2xl">
{ownerDisplayName}'s Notes
Expand Down
5 changes: 4 additions & 1 deletion app/routes/users+/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { data, redirect, Link } from 'react-router'
import { z } from 'zod'
import { Img } from 'openimg/react'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { ErrorList } from '#app/components/forms.tsx'
import { SearchBar } from '#app/components/search-bar.tsx'
Expand Down Expand Up @@ -79,10 +80,12 @@ export default function UsersRoute({ loaderData }: Route.ComponentProps) {
to={user.username}
className="flex h-36 w-44 flex-col items-center justify-center rounded-lg bg-muted px-5 py-3"
>
<img
<Img
alt={user.name ?? user.username}
src={getUserImgSrc(user.imageId)}
className="h-16 w-16 rounded-full"
width={256}
height={256}
/>
{user.name ? (
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap text-center text-body-md">
Expand Down
Loading