Skip to content

Commit

Permalink
asset image pannel can delete, toggle visible, name as main form (dis…
Browse files Browse the repository at this point in the history
…abled), popover with name field
  • Loading branch information
goodeats committed Jun 14, 2024
1 parent ba595e7 commit d6ffdae
Show file tree
Hide file tree
Showing 36 changed files with 1,156 additions and 85 deletions.
134 changes: 87 additions & 47 deletions app/components/templates/form/fetcher-image-select.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { conform, useForm } from '@conform-to/react'
import { getFieldsetConstraint } from '@conform-to/zod'
import { getFieldsetConstraint, parse } from '@conform-to/zod'
import { type FetcherWithComponents } from '@remix-run/react'
import { useEffect, useState } from 'react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
Expand All @@ -9,9 +9,8 @@ import {
ImagePreviewContainer,
ImagePreviewLabel,
ImagePreviewWrapper,
ImageUploadInput,
} from '#app/components/image'
import { FlexColumn } from '#app/components/layout'
import { FlexColumn, FlexRow } from '#app/components/layout'
import {
DialogContentGrid,
DialogFormsContainer,
Expand All @@ -26,13 +25,14 @@ import {
DialogTrigger,
} from '#app/components/ui/dialog'
import { type IconName } from '#app/components/ui/icon'
import { Label } from '#app/components/ui/label'
import { Input } from '#app/components/ui/input'
import { PanelIconButton } from '#app/components/ui/panel-icon-button'
import { StatusButton } from '#app/components/ui/status-button'
import { type IAssetParent } from '#app/models/asset/asset.server'
import { type IAssetImage } from '#app/models/asset/image/image.server'
import { sizeInMB } from '#app/models/asset/image/utils'
import { type IAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy'
import { useIsPending } from '#app/utils/misc'
import { cn, useIsPending } from '#app/utils/misc'
import { TooltipHydrated } from '../tooltip'

export const FetcherImageSelect = ({
Expand Down Expand Up @@ -67,13 +67,24 @@ export const FetcherImageSelect = ({
children: JSX.Element
}) => {
const [open, setOpen] = useState(false)
const [selectedImageId, setSelectedImageId] = useState<
IAssetImage['id'] | null
>(null)

const lastSubmission = fetcher.data?.submission
const isPending = useIsPending()
const [form, fields] = useForm({
const [form, { assetImageId }] = useForm({
id: formId,
constraint: getFieldsetConstraint(schema),
lastSubmission,
shouldValidate: 'onInput',
onValidate({ formData }) {
const parsed = parse(formData, { schema })
setSelectedImageId(
(parsed.payload.assetImageId as IAssetImage['id']) ?? null,
)
return parsed
},
onSubmit: async (event, { formData }) => {
event.preventDefault()
fetcher.submit(formData, {
Expand Down Expand Up @@ -114,55 +125,84 @@ export const FetcherImageSelect = ({
{children}

<DialogFormsContainer>
<Label>Images</Label>
{images.map(image => {
const { id, name, description, attributes } = image
const { altText } = attributes
const imgSrc = strategy.getAssetSrc({
parentId: parent.id,
assetId: id,
})
<fieldset>
<FlexColumn className="gap-4">
{conform
.collection(assetImageId, {
type: 'radio',
options: images.map(image => image.id),
})
.map((props, index) => {
const image = images.find(
image => image.id === props.value,
) as IAssetImage
const isSelectedImage = selectedImageId === image.id

const { id, name, attributes } = image
const { altText, height, width, size } = attributes
const imgSrc = strategy.getAssetSrc({
parentId: parent.id,
assetId: id,
})

return (
<FlexColumn key={image.id}>
<ImagePreviewContainer>
<ImagePreviewWrapper>
<ImagePreviewLabel htmlFor={fields.file.id}>
<div className="relative">
<ImagePreview
src={imgSrc}
alt={altText ?? 'No alt text'}
/>
</div>
</ImagePreviewLabel>
</ImagePreviewWrapper>
</ImagePreviewContainer>
<ImageUploadInput
className="hidden"
disabled={true}
aria-label="Image"
accept="image/*"
{...conform.input(fields.file, {
type: 'file',
ariaAttributes: true,
})}
/>
<input type="hidden" name="name" value={name} />
<input
type="hidden"
name="description"
value={description ?? ''}
/>
</FlexColumn>
)
})}
return (
<FlexRow
key={index}
className={cn(
'flex-1 gap-4 p-4',
isSelectedImage
? 'rounded-lg border border-secondary-foreground bg-secondary'
: '',
)}
>
<ImagePreviewContainer>
<ImagePreviewWrapper>
<ImagePreviewLabel htmlFor={props.id}>
<div className="relative">
<div>
<ImagePreview
src={imgSrc}
alt={altText ?? 'No alt text'}
/>
</div>
</div>
<Input
id={props.id}
{...props}
className="hidden"
/>
</ImagePreviewLabel>
</ImagePreviewWrapper>
</ImagePreviewContainer>
<FlexRow className="flex-1 items-end">
<FlexColumn className="h-full flex-1 items-center justify-between">
<FlexColumn className="self-end">
<FlexRow className="justify-end">
{name}
</FlexRow>
<FlexRow className="justify-end">
{width}x{height}
</FlexRow>
<FlexRow className="justify-end">
<div>{sizeInMB(size)} MB</div>
</FlexRow>
</FlexColumn>
</FlexColumn>
</FlexRow>
</FlexRow>
)
})}
</FlexColumn>
</fieldset>
</DialogFormsContainer>
</fetcher.Form>
</DialogContentGrid>
<DialogFooter>
<StatusButton
form={form.id}
type="submit"
name="intent"
value="clone"
disabled={isPending}
status={isPending ? 'pending' : 'idle'}
>
Expand Down
115 changes: 115 additions & 0 deletions app/components/templates/form/fetcher-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useForm, conform } from '@conform-to/react'
import { getFieldsetConstraint, parse } from '@conform-to/zod'
import { type FetcherWithComponents } from '@remix-run/react'
import { useRef } from 'react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { type z } from 'zod'
import { Icon, type IconName } from '#app/components/ui/icon'
import { Input } from '#app/components/ui/input'
import { Label } from '#app/components/ui/label'
import { useOptimisticValue } from '#app/utils/forms'
import { useDebounce, useIsPending } from '#app/utils/misc'
import { TooltipHydrated } from '../tooltip'

export const FetcherText = ({
fetcher,
fetcherKey,
route,
schema,
formId,
fieldName,
fieldValue,
placeholder,
tooltipText,
isHydrated,
disabled,
children,
icon,
}: {
fetcher: FetcherWithComponents<any>
fetcherKey: string
route: string
schema: z.ZodSchema<any>
formId: string
fieldName: string
fieldValue: string
placeholder: string
tooltipText: string
isHydrated: boolean
disabled?: boolean
children: JSX.Element
icon?: IconName
}) => {
const optimisticValue = useOptimisticValue(fetcherKey, schema, fieldName)
const value = optimisticValue ?? fieldValue ?? 0
const lastSubmission = fetcher.data?.submission
const isPending = useIsPending()
const [form, fields] = useForm({
id: formId,
constraint: getFieldsetConstraint(schema),
lastSubmission,
shouldValidate: 'onInput',
shouldRevalidate: 'onInput',
onValidate: ({ formData }) => {
return parse(formData, { schema: schema })
},
onSubmit: async (event, { formData }) => {
event.preventDefault()
fetcher.submit(formData, {
method: 'POST',
action: route,
})
},
defaultValue: {
[fieldName]: value,
},
})

// hack to submit select form on change
// through conform-to and fetcher
const submitRef = useRef<HTMLButtonElement>(null)
const handleChangeSubmit = useDebounce((f: HTMLFormElement) => {
submitRef.current?.click()
}, 400)

return (
<fetcher.Form
method="POST"
action={route}
onChange={e => handleChangeSubmit(e.currentTarget)}
{...form.props}
className="flex-1"
>
<AuthenticityTokenInput />
<input type="hidden" name="no-js" value={String(!isHydrated)} />
{/* hidden field values */}
{children}

{/* need this div class for icon */}
<div className="flex w-full items-center space-x-2">
{/* icon might be for artwork height, width */}
{icon && (
<Label htmlFor={fields[fieldName].id} className="w-5 flex-shrink-0">
<Icon name={icon} className="h-5 w-5" />
</Label>
)}
<TooltipHydrated tooltipText={tooltipText} isHydrated={isHydrated}>
<Input
type="text"
className="flex h-8"
autoComplete="off"
placeholder={placeholder}
disabled={disabled || isPending}
{...conform.input(fields[fieldName], {
ariaAttributes: true,
})}
/>
</TooltipHydrated>
</div>

<button type="submit" ref={submitRef} className="hidden">
Submit
</button>
</fetcher.Form>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { memo, useCallback } from 'react'
import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server'
import { type IAssetImage } from '#app/models/asset/image/image.server'
import { type IDesign } from '#app/models/design/design.server'
import { ArtworkVersionDesignDelete } from '#app/routes/resources+/api.v1+/artwork-version.design.delete'
import { AssetImageArtworkVersionDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork-version.delete'
import { LayerDesignDelete } from '#app/routes/resources+/api.v1+/layer.design.delete'
import {
type entityParentTypeEnum,
Expand All @@ -22,6 +25,13 @@ interface DeleteChildEntityFormProps {
const ArtworkVersionDeleteChildEntityForm = memo(
({ entityType, entity, parent }: DeleteChildEntityFormProps) => {
switch (entityType) {
case EntityType.ASSET:
return (
<AssetImageArtworkVersionDelete
image={entity as IAssetImage}
artworkVersion={parent as IArtworkVersion}
/>
)
case EntityType.DESIGN:
return (
<ArtworkVersionDesignDelete
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { memo, useCallback } from 'react'
import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server'
import { type IAssetImage } from '#app/models/asset/image/image.server'
import { type IDesign } from '#app/models/design/design.server'
import { type ILayer } from '#app/models/layer/layer.server'
import { ArtworkVersionDesignToggleVisible } from '#app/routes/resources+/api.v1+/artwork-version.design.update.visible'
import { ArtworkVersionLayerToggleVisible } from '#app/routes/resources+/api.v1+/artwork-version.layer.update.visible'
import { AssetImageArtworkVersionUpdateVisible } from '#app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible'
import { LayerDesignToggleVisible } from '#app/routes/resources+/api.v1+/layer.design.update.visible'
import {
type entityParentTypeEnum,
Expand All @@ -25,6 +27,13 @@ interface ToggleVisibleChildEntityFormProps {
const ArtworkVersionToggleVisibleChildEntityForm = memo(
({ entityType, entity, parent }: ToggleVisibleChildEntityFormProps) => {
switch (entityType) {
case EntityType.ASSET:
return (
<AssetImageArtworkVersionUpdateVisible
image={entity as IAssetImage}
artworkVersion={parent as IArtworkVersion}
/>
)
case EntityType.DESIGN:
return (
<ArtworkVersionDesignToggleVisible
Expand Down
20 changes: 13 additions & 7 deletions app/components/templates/panel/dashboard-entity-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ export const DashboardEntityPanel = ({
strategyEntityNew,
strategyReorder,
strategyActions,
skipReorder,
}: {
type: IEntityType
parent: IEntityParentType
entities: IEntity[]
strategyEntityNew: IDashboardPanelCreateEntityStrategy
strategyReorder: IDashboardPanelUpdateEntityOrderStrategy
strategyActions: IDashboardPanelEntityActionStrategy
skipReorder?: boolean
}) => {
const entityCount = entities.length

Expand All @@ -41,13 +43,17 @@ export const DashboardEntityPanel = ({
{entities.map((entity, index) => {
return (
<SidebarPanelRow key={entity.id}>
<PanelEntityRowReorder
entity={entity}
parent={parent}
atTop={index === 0}
atBottom={index === entityCount - 1}
strategyReorder={strategyReorder}
/>
{skipReorder ? (
<div className="w-4"></div>
) : (
<PanelEntityRowReorder
entity={entity}
parent={parent}
atTop={index === 0}
atBottom={index === entityCount - 1}
strategyReorder={strategyReorder}
/>
)}
<SidebarPanelRowContainer>
<PanelEntityValues type={type} entity={entity} parent={parent} />
<PanelEntityRowActions
Expand Down
Loading

0 comments on commit d6ffdae

Please sign in to comment.