Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions app/components/user-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Img } from 'openimg/react'
import { useRef } from 'react'
import { Link, Form } from 'react-router'
import { getUserImgSrc } from '#app/utils/misc.tsx'
Expand Down Expand Up @@ -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)}
src={getUserImgSrc(user.image?.objectKey)}
width={256}
height={256}
/>
<span className="text-body-sm font-bold">
{user.name ?? user.username}
Expand Down
12 changes: 8 additions & 4 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OpenImgContextProvider } from 'openimg/react'
import {
data,
Link,
Expand Down Expand Up @@ -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 @@ -83,7 +84,7 @@ export async function loader({ request }: Route.LoaderArgs) {
id: true,
name: true,
username: true,
image: { select: { id: true } },
image: { select: { objectKey: true } },
roles: {
select: {
name: true,
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.objectKey),
}
: null,
notes: user.notes.map((note) => ({
...note,
images: note.images.map((image) => ({
...image,
url: `${domain}/resources/note-images/${image.id}`,
url: domain + getNoteImgSrc(image.objectKey),
})),
})),
},
Expand Down
62 changes: 62 additions & 0 deletions app/routes/resources+/images.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { invariantResponse } from '@epic-web/invariant'
import { getImgResponse } from 'openimg/node'
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 objectKey = searchParams.get('objectKey')

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 (objectKey) {
const { url: signedUrl, headers: signedHeaders } =
getSignedGetRequestInfo(objectKey)
return {
type: 'fetch',
url: signedUrl,
headers: signedHeaders,
}
}

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,
}
},
})
}
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.

10 changes: 7 additions & 3 deletions 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 @@ -36,7 +37,7 @@ export async function loader({ request }: Route.LoaderArgs) {
username: true,
email: true,
image: {
select: { id: true },
select: { objectKey: true },
},
_count: {
select: {
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
src={getUserImgSrc(loaderData.user.image?.id)}
<Img
src={getUserImgSrc(loaderData.user.image?.objectKey)}
alt={loaderData.user.username}
className="h-full w-full rounded-full object-cover"
width={832}
height={832}
isAboveFold
/>
<Button
asChild
Expand Down
8 changes: 5 additions & 3 deletions app/routes/settings+/profile.photo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function loader({ request }: Route.LoaderArgs) {
id: true,
name: true,
username: true,
image: { select: { id: true } },
image: { select: { objectKey: true } },
},
})
invariantResponse(user, 'User not found', { status: 404 })
Expand Down Expand Up @@ -142,7 +142,9 @@ export default function PhotoRoute({
<img
src={
newImageSrc ??
(loaderData.user ? getUserImgSrc(loaderData.user.image?.id) : '')
(loaderData.user
? getUserImgSrc(loaderData.user.image?.objectKey)
: '')
}
className="h-52 w-52 rounded-full object-cover"
alt={loaderData.user?.name ?? loaderData.user?.username}
Expand Down Expand Up @@ -202,7 +204,7 @@ export default function PhotoRoute({
>
<Icon name="trash">Reset</Icon>
</Button>
{loaderData.user.image?.id ? (
{loaderData.user.image ? (
<StatusButton
className="peer-valid:hidden"
variant="destructive"
Expand Down
9 changes: 6 additions & 3 deletions app/routes/users+/$username.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { invariantResponse } from '@epic-web/invariant'
import { Img } from 'openimg/react'
import {
type LoaderFunctionArgs,
Form,
Expand All @@ -21,7 +22,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
name: true,
username: true,
createdAt: true,
image: { select: { id: true } },
image: { select: { id: true, objectKey: true } },
},
where: {
username: params.username,
Expand All @@ -48,10 +49,12 @@ export default function ProfileRoute() {
<div className="relative w-52">
<div className="absolute -top-40">
<div className="relative">
<img
src={getUserImgSrc(data.user.image?.id)}
<Img
src={getUserImgSrc(data.user.image?.objectKey)}
alt={userDisplayName}
className="h-52 w-52 rounded-full object-cover"
width={832}
height={832}
/>
</div>
</div>
Expand Down
26 changes: 19 additions & 7 deletions app/routes/users+/$username_+/__note-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type FieldMetadata,
} from '@conform-to/react'
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import { Img } from 'openimg/react'
import { useState } from 'react'
import { Form } from 'react-router'
import { z } from 'zod'
Expand Down Expand Up @@ -108,11 +109,11 @@ export function NoteEditor({
<div>
<Label>Images</Label>
<ul className="flex flex-col gap-4">
{imageList.map((image, index) => {
console.log('image.key', image.key)
{imageList.map((imageMeta, index) => {
const image = note?.images[index]
return (
<li
key={image.key}
key={imageMeta.key}
className="relative border-b-2 border-muted-foreground"
>
<button
Expand All @@ -129,7 +130,10 @@ export function NoteEditor({
Remove image {index + 1}
</span>
</button>
<ImageChooser meta={image} />
<ImageChooser
meta={imageMeta}
objectKey={image?.objectKey}
/>
</li>
)
})}
Expand Down Expand Up @@ -165,11 +169,17 @@ export function NoteEditor({
)
}

function ImageChooser({ meta }: { meta: FieldMetadata<ImageFieldset> }) {
function ImageChooser({
meta,
objectKey,
}: {
meta: FieldMetadata<ImageFieldset>
objectKey: string | undefined
}) {
const fields = meta.getFieldset()
const existingImage = Boolean(fields.id.initialValue)
const [previewImage, setPreviewImage] = useState<string | null>(
fields.id.initialValue ? getNoteImgSrc(fields.id.initialValue) : null,
objectKey ? getNoteImgSrc(objectKey) : null,
)
const [altText, setAltText] = useState(fields.altText.initialValue ?? '')

Expand All @@ -188,10 +198,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
Loading