Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions app/(dashboard)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,24 @@ export async function setAssetOwnersAction(assetId: string, productSlug: string,
if (!accessible.some((a) => a.id === assetId)) throw new Error('Asset not found or not accessible')
await setAssetOwners(assetId, userIds)
revalidatePath(`/products/${productSlug}`)
revalidatePath(`/assets/${assetId}`)
revalidatePath('/my-work')
}

/** Update just the long-form content (description / notes) from the asset detail page. */
export async function updateAssetContentAction(
assetId: string,
productSlug: string,
content: { description?: string; notes?: string },
) {
const authUser = await requireUser()
const accessible = await getAssetOptions(authUser.id)
if (!accessible.some((a) => a.id === assetId)) throw new Error('Asset not found or not accessible')
await updateAsset(assetId, content)
revalidatePath(`/assets/${assetId}`)
revalidatePath(`/products/${productSlug}`)
}

// ---------------------------------------------------------------------------
// Code Plans
// ---------------------------------------------------------------------------
Expand Down
93 changes: 93 additions & 0 deletions app/(dashboard)/assets/[id]/asset-content-cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client'

import { useState, useTransition } from 'react'
import ReactMarkdown from 'react-markdown'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { AlignLeft, NotebookPen, Pencil } from 'lucide-react'
import { toast } from 'sonner'
import { updateAssetContentAction } from '../../actions'

type EditableCardProps = {
assetId: string
productSlug: string
field: 'description' | 'notes'
value: string
}

const cardMeta = {
description: { title: 'Description', icon: AlignLeft, placeholder: 'What does this asset do?', empty: 'No description yet.' },
notes: {
title: 'Notes & Ideation',
icon: NotebookPen,
placeholder: 'Design thinking, migration ideas, known quirks… (markdown)',
empty: 'No notes yet — capture design thinking, migration ideas, and known quirks here.',
},
} as const

/** Long-form asset content with click-to-edit. Notes render as markdown (markdown stays the canonical format). */
export function AssetContentCard({ assetId, productSlug, field, value }: EditableCardProps) {
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState(value)
const [isPending, startTransition] = useTransition()
const meta = cardMeta[field]
const Icon = meta.icon

function save() {
startTransition(async () => {
await updateAssetContentAction(assetId, productSlug, { [field]: draft })
toast.success('Saved')
setEditing(false)
})
}

return (
<Card className="bg-card border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Icon className="h-4 w-4" />
{meta.title}
{!editing && (
<Button
variant="ghost"
size="icon"
className="ml-auto h-6 w-6 text-muted-foreground"
title={`Edit ${meta.title.toLowerCase()}`}
onClick={() => { setDraft(value); setEditing(true) }}
>
<Pencil className="h-3.5 w-3.5" />
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent>
{editing ? (
<div className="space-y-2">
<Textarea
value={draft}
onChange={(e) => setDraft(e.target.value)}
rows={field === 'notes' ? 10 : 4}
placeholder={meta.placeholder}
autoFocus
/>
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={() => setEditing(false)} disabled={isPending}>Cancel</Button>
<Button size="sm" onClick={save} disabled={isPending}>{isPending ? 'Saving…' : 'Save'}</Button>
</div>
</div>
) : value ? (
field === 'notes' ? (
<div className="prose prose-sm prose-invert max-w-none [&_h1]:text-base [&_h2]:text-sm [&_h3]:text-sm [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded [&_code]:text-xs [&_table]:text-xs">
<ReactMarkdown>{value}</ReactMarkdown>
</div>
) : (
<p className="text-sm text-muted-foreground whitespace-pre-line">{value}</p>
)
) : (
<p className="text-sm text-muted-foreground italic">{meta.empty}</p>
)}
</CardContent>
</Card>
)
}
Loading
Loading