Skip to content

Commit

Permalink
Merge pull request #184 from goodeats/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
goodeats authored Jun 17, 2024
2 parents 4cfa1ba + da0bbbc commit 2ecea59
Show file tree
Hide file tree
Showing 245 changed files with 10,797 additions and 2,457 deletions.
59 changes: 59 additions & 0 deletions app/components/image/image-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createContainerComponent } from '../layout/utils'

const ImagePreviewContainer = createContainerComponent({
defaultTagName: 'div',
defaultClassName: 'w-32',
displayName: 'ImagePreviewContainer',
})

const ImagePreviewWrapper = createContainerComponent({
defaultTagName: 'div',
defaultClassName: 'relative h-32 w-32',
displayName: 'ImagePreviewWrapper',
})

const ImagePreviewLabel = createContainerComponent({
defaultTagName: 'label',
defaultClassName: 'group absolute h-32 w-32 rounded-lg',
displayName: 'ImagePreviewLabel',
})

const ImagePreview = createContainerComponent({
defaultTagName: 'img',
defaultClassName: 'h-32 w-32 rounded-lg object-cover',
displayName: 'ImagePreview',
})

const noImagePreviewClassName =
'bg-accent opacity-40 focus-within:opacity-100 hover:opacity-100'

const ImagePreviewSkeleton = createContainerComponent({
defaultTagName: 'div',
defaultClassName:
'flex h-32 w-32 items-center justify-center rounded-lg border border-muted-foreground text-4xl text-muted-foreground',
displayName: 'ImagePreviewSkeleton',
})

const ImageUploadInput = createContainerComponent({
defaultTagName: 'input',
defaultClassName:
'absolute left-0 top-0 z-0 h-32 w-32 cursor-pointer opacity-0',
displayName: 'ImageUploadInput',
})

const ImageFull = createContainerComponent({
defaultTagName: 'img',
defaultClassName: 'w-full object-contain',
displayName: 'ImageFull',
})

export {
ImagePreviewContainer,
ImagePreviewWrapper,
ImagePreviewLabel,
ImagePreview,
noImagePreviewClassName,
ImagePreviewSkeleton,
ImageUploadInput,
ImageFull,
}
1 change: 1 addition & 0 deletions app/components/image/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './image-preview'
22 changes: 22 additions & 0 deletions app/components/layout/sidebar/image-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createContainerComponent } from '../utils'

const ImageSidebar = createContainerComponent({
defaultTagName: 'div',
defaultClassName:
'relative flex-1 flex w-full flex-col overflow-y-scroll p-4 gap-4',
displayName: 'ImageSidebar',
})

const ImageSidebarList = createContainerComponent({
defaultTagName: 'ul',
defaultClassName: 'flex flex-col gap-4 py-5',
displayName: 'ImageSidebarList',
})

const ImageSidebarListItem = createContainerComponent({
defaultTagName: 'li',
defaultClassName: 'flex flex-col',
displayName: 'ImageSidebarListItem',
})

export { ImageSidebar, ImageSidebarList, ImageSidebarListItem }
1 change: 1 addition & 0 deletions app/components/layout/sidebar/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './image-sidebar'
export * from './sidebar'
export * from './tabbed-sidebar'
export * from './nav-sidebar'
44 changes: 44 additions & 0 deletions app/components/templates/canvas/artwork-canvas.footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { memo, useCallback } from 'react'
import { FlexRow } from '#app/components/layout'
import { PanelIconButton } from '#app/components/ui/panel-icon-button'
import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator'
import { TooltipHydrated } from '../tooltip'
import { LinkToEditor } from './artwork-canvas.link-to-editor'
import { DownloadCanvas, ShareCanvas } from '.'

interface CanvasFooterProps {
isHydrated: boolean
handleRefresh: () => void
canvasRef: React.RefObject<HTMLCanvasElement>
generator: IArtworkVersionGenerator
}

export const CanvasFooter = memo(
({ isHydrated, handleRefresh, canvasRef, generator }: CanvasFooterProps) => {
const { metadata } = generator

const linkToEditor = useCallback(
() =>
metadata ? (
<LinkToEditor metadata={metadata} isHydrated={isHydrated} />
) : null,
[metadata, isHydrated],
)

return (
<FlexRow className="gap-2">
<TooltipHydrated tooltipText="Reload" isHydrated={isHydrated}>
<PanelIconButton
iconName="reload"
iconText="Reload"
onClick={handleRefresh}
/>
</TooltipHydrated>
<DownloadCanvas canvasRef={canvasRef} isHydrated={isHydrated} />
<ShareCanvas canvasRef={canvasRef} isHydrated={isHydrated} />
{linkToEditor()}
</FlexRow>
)
},
)
CanvasFooter.displayName = 'CanvasFooter'
35 changes: 35 additions & 0 deletions app/components/templates/canvas/artwork-canvas.link-to-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { memo } from 'react'
import { PanelIconLink } from '#app/components/ui/panel-icon-link'
import { type IArtworkVersionGeneratorMetadata } from '#app/definitions/artwork-generator'
import { useOptionalUser } from '#app/utils/user'
import { TooltipHydrated } from '../tooltip'

export const LinkToEditor = memo(
({
metadata,
isHydrated,
}: {
metadata: IArtworkVersionGeneratorMetadata
isHydrated: boolean
}) => {
const { projectSlug, artworkSlug, branchSlug, versionSlug, ownerId } =
metadata
const user = useOptionalUser()
const isOwner = user?.id === ownerId
if (!isOwner) return null

const editorPath = `/editor/projects/${projectSlug}/artworks/${artworkSlug}/${branchSlug}/${versionSlug}`
return (
<div className="ml-auto">
<TooltipHydrated tooltipText="Editor" isHydrated={isHydrated}>
<PanelIconLink
to={editorPath}
iconName="magic-wand"
iconText="Editor"
/>
</TooltipHydrated>
</div>
)
},
)
LinkToEditor.displayName = 'LinkToEditor'
94 changes: 18 additions & 76 deletions app/components/templates/canvas/artwork-canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,17 @@
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useHydrated } from 'remix-utils/use-hydrated'
import { FlexColumn, FlexRow } from '#app/components/layout'
import { PanelIconButton } from '#app/components/ui/panel-icon-button'
import { PanelIconLink } from '#app/components/ui/panel-icon-link'
import {
type IArtworkVersionGeneratorMetadata,
type IArtworkVersionGenerator,
} from '#app/definitions/artwork-generator'
import { FlexColumn } from '#app/components/layout'
import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator'
import { canvasDrawService } from '#app/services/canvas/draw.service'
import { downloadCanvasToImg } from '#app/utils/download'
import { useOptionalUser } from '#app/utils/user'
import { TooltipHydrated } from '../tooltip'

const LinkToEditor = memo(
({
metadata,
isHydrated,
}: {
metadata: IArtworkVersionGeneratorMetadata
isHydrated: boolean
}) => {
const { projectSlug, artworkSlug, branchSlug, versionSlug, ownerId } =
metadata
const user = useOptionalUser()
const isOwner = user?.id === ownerId
if (!isOwner) return null

const editorPath = `/editor/projects/${projectSlug}/artworks/${artworkSlug}/${branchSlug}/${versionSlug}`
return (
<div className="ml-auto">
<TooltipHydrated tooltipText="Editor" isHydrated={isHydrated}>
<PanelIconLink
to={editorPath}
iconName="magic-wand"
iconText="Editor"
/>
</TooltipHydrated>
</div>
)
},
)
LinkToEditor.displayName = 'LinkToEditor'
import { CanvasFooter } from './artwork-canvas.footer'

// The ArtworkCanvas component is wrapped in React.memo to optimize performance by memoizing the component.
// This prevents unnecessary re-renders when the props passed to the component have not changed.
// Specifically, since this component involves canvas drawing operations which can be computationally expensive,
// memoizing ensures that these operations are only re-executed when necessary, such as when the 'generator' prop changes.
export const ArtworkCanvas = memo(
({ generator }: { generator: IArtworkVersionGenerator }) => {
const { metadata, settings } = generator
const { settings } = generator
const { width, height, background } = settings
const canvasRef = useRef<HTMLCanvasElement>(null)
const [refresh, setRefresh] = useState(0)
Expand All @@ -61,24 +24,19 @@ export const ArtworkCanvas = memo(
}
}, [canvasRef, generator, refresh])

const linkToEditor = useCallback(
() =>
metadata ? (
<LinkToEditor metadata={metadata} isHydrated={isHydrated} />
) : null,
[metadata, isHydrated],
)

const handleRefresh = () => {
setRefresh(prev => prev + 1)
}

const handleDownload = () => {
const canvas = canvasRef.current

if (!canvas) return
downloadCanvasToImg({ canvas })
}
const canvasFooter = useCallback(() => {
const handleRefresh = () => {
setRefresh(prev => prev + 1)
}
return (
<CanvasFooter
isHydrated={isHydrated}
handleRefresh={handleRefresh}
canvasRef={canvasRef}
generator={generator}
/>
)
}, [isHydrated, canvasRef, generator])

return (
<FlexColumn className="gap-2">
Expand All @@ -90,23 +48,7 @@ export const ArtworkCanvas = memo(
style={{ backgroundColor: `#${background}` }}
className="h-full w-full"
/>
<FlexRow className="gap-2">
<TooltipHydrated tooltipText="Reload" isHydrated={isHydrated}>
<PanelIconButton
iconName="reload"
iconText="Reload"
onClick={handleRefresh}
/>
</TooltipHydrated>
<TooltipHydrated tooltipText="Download" isHydrated={isHydrated}>
<PanelIconButton
iconName="download"
iconText="Download"
onClick={handleDownload}
/>
</TooltipHydrated>
{generator.metadata && linkToEditor()}
</FlexRow>
{canvasFooter()}
</FlexColumn>
)
},
Expand Down
57 changes: 57 additions & 0 deletions app/components/templates/canvas/canvas-download.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { memo, useEffect, useState } from 'react'
import { PanelIconButton } from '#app/components/ui/panel-icon-button'
import { downloadCanvasToImg, downloadImageFileName } from '#app/utils/download'
import { TooltipHydrated } from '../tooltip'

export const DownloadCanvas = memo(
({
canvasRef,
isHydrated,
}: {
canvasRef: React.RefObject<HTMLCanvasElement>
isHydrated: boolean
}) => {
const [canDownload, setCanDownload] = useState(false)

useEffect(() => {
const checkShareCapability = () => {
const canvas = canvasRef.current
if (!canvas) return false

canvas.toBlob(blob => {
if (!blob) return
const file = new File([blob], downloadImageFileName(), {
type: 'image/png',
})

// if you can share then don't display download button
const canShare =
navigator.canShare && navigator.canShare({ files: [file] })
setCanDownload(!canShare)
}, 'image/png')
}

checkShareCapability()
}, [canvasRef])

const handleDownload = () => {
const canvas = canvasRef.current

if (!canvas) return
downloadCanvasToImg({ canvas })
}

if (!canDownload) return null

return (
<TooltipHydrated tooltipText="Download" isHydrated={isHydrated}>
<PanelIconButton
iconName="download"
iconText="Download"
onClick={handleDownload}
/>
</TooltipHydrated>
)
},
)
DownloadCanvas.displayName = 'DownloadCanvas'
Loading

0 comments on commit 2ecea59

Please sign in to comment.