Skip to content

Commit

Permalink
added starred and published to artwork versions; can toggle starred f…
Browse files Browse the repository at this point in the history
…rom sketch dashboard; can publish from profile artwork view
  • Loading branch information
goodeats committed May 23, 2024
1 parent 7a2f0fd commit 654799b
Show file tree
Hide file tree
Showing 17 changed files with 390 additions and 12 deletions.
7 changes: 5 additions & 2 deletions app/components/layout/card/dashboard-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CardTitle,
} from '#app/components/ui/card'
import { Icon, type IconName } from '#app/components/ui/icon'
import { cn } from '#app/utils/misc'
import { createContainerComponent } from '../utils'

const DashboardCardWrapper = createContainerComponent({
Expand All @@ -34,17 +35,19 @@ const DashboardCard = ({
title,
description,
children,
className,
}: {
title: string
description: string
children: React.ReactNode
className?: string
}) => {
// - fixed width to indicate a smaller card
// - more space between cards on larger screens
const className = 'mb-2 w-80 lg:mb-6'
const cardClassName = cn('mb-2 w-80 lg:mb-6', className)

return (
<Card className={className}>
<Card className={cardClassName}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
Expand Down
2 changes: 2 additions & 0 deletions app/components/ui/icons/name.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type IconName =
| 'cross-1'
| 'crosshair-1'
| 'crosshair-2'
| 'crumpled-paper'
| 'dimensions'
| 'disc'
| 'dots-horizontal'
Expand Down Expand Up @@ -57,6 +58,7 @@ export type IconName =
| 'plus'
| 'question-mark-circled'
| 'reset'
| 'rocket'
| 'ruler-square'
| 'size'
| 'stack'
Expand Down
14 changes: 14 additions & 0 deletions app/components/ui/icons/sprite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions app/models/artwork-version/artwork-version.get.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { prisma } from '#app/utils/db.server'
import {
type IArtworkVersionWithDesignsAndLayers,
type IArtworkVersion,
type IArtworkVersionWithBranch,
} from './artwork-version.server'

export type queryArtworkVersionWhereArgsType = z.infer<typeof whereArgs>
Expand All @@ -14,6 +15,7 @@ const whereArgs = z.object({
branchId: z.string().optional(),
nextId: zodStringOrNull.optional(),
prevId: zodStringOrNull.optional(),
starred: z.boolean().optional(),
})

const includeDesigns = {
Expand Down Expand Up @@ -100,3 +102,25 @@ export const getArtworkVersionWithDesignsAndLayers = async ({
})
return artworkVersion
}

export const getStarredArtworkVersions = async ({
artworkId,
}: {
artworkId: string
}): Promise<IArtworkVersionWithBranch[]> => {
const starredVersions = await prisma.artworkVersion.findMany({
where: {
branch: {
artworkId: artworkId,
},
starred: true,
},
include: {
branch: true,
},
orderBy: {
updatedAt: 'desc',
},
})
return starredVersions
}
11 changes: 10 additions & 1 deletion app/models/artwork-version/artwork-version.server.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { type ArtworkVersion } from '@prisma/client'
import { type DateOrString } from '#app/definitions/prisma-helper'
import { type IArtworkBranch } from '../artwork-branch/artwork-branch.server'
import { type IDesignWithType } from '../design/design.server'
import { type ILayerWithDesigns } from '../layer/layer.server'

// Omitting 'createdAt' and 'updatedAt' from the ArtworkVersion interface
// prisma query returns a string for these fields
type BaseArtworkVersion = Omit<ArtworkVersion, 'createdAt' | 'updatedAt'>
type BaseArtworkVersion = Omit<
ArtworkVersion,
'createdAt' | 'updatedAt' | 'publishedAt'
>

export interface IArtworkVersion extends BaseArtworkVersion {
createdAt: DateOrString
updatedAt: DateOrString
publishedAt: DateOrString | null
}

export interface IArtworkVersionWithDesignsAndLayers extends IArtworkVersion {
designs: IDesignWithType[]
layers: ILayerWithDesigns[]
}

export interface IArtworkVersionWithBranch extends IArtworkVersion {
branch: IArtworkBranch
}
42 changes: 41 additions & 1 deletion app/models/artwork-version/artwork-version.update.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ArtworkVersionHeightSchema,
ArtworkVersionBackgroundSchema,
ArtworkVersionStarredSchema,
ArtworkVersionPublishedSchema,
} from '#app/schema/artwork-version'
import { ValidateArtworkVersionSubmissionStrategy } from '#app/strategies/validate-submission.strategy'
import { validateEntitySubmission } from '#app/utils/conform-utils'
Expand Down Expand Up @@ -65,6 +66,15 @@ export async function validateArtworkVersionStarredSubmission(
})
}

export async function validateArtworkVersionPublishedSubmission(
args: IntentActionArgs,
) {
return validateUpdateSubmission({
...args,
schema: ArtworkVersionPublishedSchema,
})
}

const getArtworkVersionInstance = async ({
id,
}: {
Expand Down Expand Up @@ -149,6 +159,7 @@ export const updateArtworkVersionBackground = async ({
}
}

// unstarring should also unpublish
export const updateArtworkVersionStarred = async ({
id,
}: {
Expand All @@ -158,7 +169,36 @@ export const updateArtworkVersionStarred = async ({
if (!artworkVersion) return { success: false }

try {
artworkVersion.starred = !artworkVersion.starred
const isUnstarring = !artworkVersion.starred
artworkVersion.starred = isUnstarring
if (isUnstarring) {
artworkVersion.published = false
artworkVersion.publishedAt = null
}
artworkVersion.updatedAt = new Date()
await artworkVersion.save()

return { success: true }
} catch (error) {
// consider how to handle this error where this is called
console.error(error)
return { success: false }
}
}

// unpublishing should not impact starred status
export const updateArtworkVersionPublished = async ({
id,
}: {
id: IArtworkVersion['id']
}) => {
const artworkVersion = await getArtworkVersionInstance({ id })
if (!artworkVersion) return { success: false }

try {
const isUnpublishing = !artworkVersion.published
artworkVersion.published = isUnpublishing
artworkVersion.publishedAt = isUnpublishing ? null : new Date()
artworkVersion.updatedAt = new Date()
await artworkVersion.save()

Expand Down
117 changes: 117 additions & 0 deletions app/routes/resources+/api.v1+/artwork-version.update.published.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useForm } from '@conform-to/react'
import { getFieldsetConstraint } from '@conform-to/zod'
import {
json,
type ActionFunctionArgs,
type LoaderFunctionArgs,
} from '@remix-run/node'
import { useFetcher } from '@remix-run/react'
import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { redirectBack } from 'remix-utils/redirect-back'
import { useHydrated } from 'remix-utils/use-hydrated'
import { PanelIconButton } from '#app/components/ui/panel-icon-button'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '#app/components/ui/tooltip'
import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server'
import { validateArtworkVersionPublishedSubmission } from '#app/models/artwork-version/artwork-version.update.server'
import { ArtworkVersionPublishedSchema } from '#app/schema/artwork-version'
import { validateNoJS } from '#app/schema/form-data'
import { updateArtworkVersionPublishedService } from '#app/services/artwork/version/update.service'
import { requireUserId } from '#app/utils/auth.server'
import { useIsPending } from '#app/utils/misc'
import { Routes } from '#app/utils/routes.const'

// https://www.epicweb.dev/full-stack-components

const route = Routes.RESOURCES.API.V1.ARTWORK_VERSION.UPDATE.PUBLISHED
const schema = ArtworkVersionPublishedSchema

export async function loader({ request }: LoaderFunctionArgs) {
await requireUserId(request)
return json({})
}

export async function action({ request }: ActionFunctionArgs) {
const userId = await requireUserId(request)
const formData = await request.formData()
const noJS = validateNoJS({ formData })

let createSuccess = false
const { status, submission } =
await validateArtworkVersionPublishedSubmission({
userId,
formData,
})

if (status === 'success') {
const { success } = await updateArtworkVersionPublishedService({
userId,
...submission.value,
})
createSuccess = success
}

if (noJS) {
throw redirectBack(request, {
fallback: '/',
})
}

return json(
{ status, submission },
{
status: status === 'error' || !createSuccess ? 422 : 201,
},
)
}

export const ArtworkVersionTogglePublished = ({
version,
}: {
version: IArtworkVersion
}) => {
const versionId = version.id
const isPublished = version.published
// these icons 😂
const icon = isPublished ? 'crumpled-paper' : 'rocket'
const iconText = isPublished ? 'Unpublish version' : 'Publish version'

const fetcher = useFetcher<typeof action>()
const lastSubmission = fetcher.data?.submission
const isPending = useIsPending()
let isHydrated = useHydrated()
const [form] = useForm({
id: `artwork-version-toggle-published-${versionId}`,
constraint: getFieldsetConstraint(schema),
lastSubmission,
})

return (
<fetcher.Form method="POST" action={route} {...form.props}>
<AuthenticityTokenInput />

<input type="hidden" name="no-js" value={String(!isHydrated)} />
<input type="hidden" name="id" value={versionId} />

{isHydrated ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<PanelIconButton
type="submit"
iconName={icon}
iconText={iconText}
disabled={isPending}
/>
</TooltipTrigger>
<TooltipContent>{iconText}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : null}
</fetcher.Form>
)
}
27 changes: 21 additions & 6 deletions app/routes/resources+/api.v1+/artwork-version.update.starred.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { AuthenticityTokenInput } from 'remix-utils/csrf/react'
import { redirectBack } from 'remix-utils/redirect-back'
import { useHydrated } from 'remix-utils/use-hydrated'
import { PanelIconButton } from '#app/components/ui/panel-icon-button'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '#app/components/ui/tooltip'
import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server'
import { validateArtworkVersionStarredSubmission } from '#app/models/artwork-version/artwork-version.update.server'
import { ArtworkVersionStarredSchema } from '#app/schema/artwork-version'
Expand Down Expand Up @@ -89,12 +95,21 @@ export const ArtworkVersionToggleStarred = ({
<input type="hidden" name="no-js" value={String(!isHydrated)} />
<input type="hidden" name="id" value={versionId} />

<PanelIconButton
type="submit"
iconName={icon}
iconText={iconText}
disabled={isPending}
/>
{isHydrated ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<PanelIconButton
type="submit"
iconName={icon}
iconText={iconText}
disabled={isPending}
/>
</TooltipTrigger>
<TooltipContent>{iconText}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : null}
</fetcher.Form>
)
}
Loading

0 comments on commit 654799b

Please sign in to comment.