Simplify posts

We had a lot of unnecessary complexity here due to post types that never ended up getting used.

We also made the post slug field reactive and bound to the title field.

We also fixed filters on the Universe admin page so we can filter by unpublished posts too (WIP)

We also fixed the hover state of BackButton
This commit is contained in:
Justin Edmund 2025-06-11 00:53:54 -07:00
parent 3d993d76ed
commit c6ce13a530
14 changed files with 109 additions and 128 deletions

View file

@ -0,0 +1 @@
-- This is an empty migration.

View file

@ -0,0 +1,3 @@
-- Update existing postType values
UPDATE "Post" SET "postType" = 'essay' WHERE "postType" = 'blog';
UPDATE "Post" SET "postType" = 'post' WHERE "postType" = 'microblog';

View file

@ -43,9 +43,9 @@ model Project {
model Post {
id Int @id @default(autoincrement())
slug String @unique @db.VarChar(255)
postType String @db.VarChar(50) // blog, microblog
title String? @db.VarChar(255) // Optional for microblog posts
content Json? // BlockNote JSON for blog/microblog
postType String @db.VarChar(50) // post, essay
title String? @db.VarChar(255) // Optional for post type
content Json? // JSON content for posts and essays
featuredImage String? @db.VarChar(500)
attachments Json? // Array of media IDs for photo attachments

View file

@ -24,7 +24,7 @@ async function initializeDatabase() {
console.log('🔍 Checking database initialization status...')
// Give the database a moment to be ready
await new Promise(resolve => setTimeout(resolve, 2000))
await new Promise((resolve) => setTimeout(resolve, 2000))
try {
const isInitialized = await isDatabaseInitialized()

View file

@ -35,7 +35,7 @@
display: inline-flex;
align-items: center;
gap: $unit-half;
padding: $unit $unit-2x;
padding: $unit 0;
font-size: 0.875rem;
font-weight: 500;
color: $red-60;
@ -46,8 +46,6 @@
transition: all 0.2s ease;
&:hover {
background: rgba($red-60, 0.08);
:global(.arrow-icon) {
transform: translateX(-3px);
}

View file

@ -25,7 +25,6 @@
{/if}
</header>
{#if post.album && post.album.photos && post.album.photos.length > 0}
<!-- Album slideshow -->
<div class="post-album">
@ -78,7 +77,7 @@
.post-content {
display: flex;
flex-direction: column;
max-width: 784px;
width: 100%;
gap: $unit-3x;
margin: 0 auto;
@ -95,6 +94,8 @@
}
&.essay {
max-width: 100%; // Full width for essays
.post-body {
font-size: 1rem;
line-height: 1.4;

View file

@ -98,7 +98,7 @@
const payload = {
title,
slug,
type: 'blog', // 'blog' is the database value for essays
type: 'essay', // No mapping needed anymore
status,
content,
tags
@ -261,7 +261,13 @@
}}
>
<div class="form-section">
<Input label="Title" size="jumbo" bind:value={title} required placeholder="Essay title" />
<Input
label="Title"
size="jumbo"
bind:value={title}
required
placeholder="Essay title"
/>
<Input label="Slug" bind:value={slug} placeholder="essay-url-slug" />

View file

@ -25,10 +25,7 @@
const postTypeLabels: Record<string, string> = {
post: 'Post',
essay: 'Essay',
// Map database types to display names
blog: 'Essay',
microblog: 'Post'
essay: 'Essay'
}
function handlePostClick() {

View file

@ -12,6 +12,7 @@
onRemoveTag: (tag: string) => void
onDelete: () => void
onClose?: () => void
onFieldUpdate?: (key: string, value: any) => void
}
let {
@ -24,12 +25,14 @@
onAddTag,
onRemoveTag,
onDelete,
onClose = () => {}
onClose = () => {},
onFieldUpdate
}: Props = $props()
function handleFieldUpdate(key: string, value: any) {
if (key === 'slug') {
slug = value
onFieldUpdate?.(key, value)
} else if (key === 'tagInput') {
tagInput = value
}

View file

@ -77,8 +77,11 @@
}
function switchToEssay() {
const contentParam = content ? encodeURIComponent(JSON.stringify(content)) : ''
goto(`/admin/posts/new?type=essay${contentParam ? `&content=${contentParam}` : ''}`)
// Store content in sessionStorage to avoid messy URLs
if (content && content.content && content.content.length > 0) {
sessionStorage.setItem('draft_content', JSON.stringify(content))
}
goto('/admin/posts/new?type=essay')
}
function generateSlug(title: string): string {
@ -92,7 +95,6 @@
essaySlug = generateSlug(essayTitle)
}
function handlePhotoUpload() {
fileInput.click()
}
@ -201,7 +203,7 @@
if (postType === 'essay') {
postData = {
...postData,
type: 'blog', // 'blog' is the database value for essays
type: 'essay', // No mapping needed anymore
title: essayTitle,
slug: essaySlug,
excerpt: essayExcerpt,
@ -211,7 +213,7 @@
// All other content is just a "post" with attachments
postData = {
...postData,
type: 'microblog' // 'microblog' is for shorter posts
type: 'post' // No mapping needed anymore
}
}
@ -301,7 +303,6 @@
class="composer-editor"
/>
{#if attachedPhotos.length > 0}
<div class="attached-photos">
{#each attachedPhotos as photo}
@ -335,7 +336,6 @@
<div class="composer-footer">
<div class="footer-left">
<Button
variant="ghost"
iconOnly
@ -499,7 +499,6 @@
class="inline-composer-editor"
/>
{#if attachedPhotos.length > 0}
<div class="attached-photos">
{#each attachedPhotos as photo}
@ -533,7 +532,6 @@
<div class="composer-footer">
<div class="footer-left">
<Button
variant="ghost"
iconOnly

View file

@ -127,9 +127,7 @@
if (selectedFilter === 'all') {
filteredPosts = posts
} else if (selectedFilter === 'post') {
filteredPosts = posts.filter((post) =>
['post', 'microblog'].includes(post.postType)
)
filteredPosts = posts.filter((post) => ['post', 'microblog'].includes(post.postType))
} else if (selectedFilter === 'essay') {
filteredPosts = posts.filter((post) => ['essay', 'blog'].includes(post.postType))
} else {

View file

@ -15,6 +15,7 @@
let loading = $state(true)
let saving = $state(false)
let loadError = $state('')
let contentReady = $state(false)
let title = $state('')
let postType = $state<'post' | 'essay'>('post')
@ -254,7 +255,7 @@
// Populate form fields
title = post.title || ''
postType = post.postType || 'post'
postType = post.postType // No mapping needed anymore
status = post.status || 'draft'
slug = post.slug || ''
excerpt = post.excerpt || ''
@ -269,6 +270,9 @@
}
tags = post.tags || []
// Set content ready after all data is loaded
contentReady = true
} else {
if (response.status === 404) {
loadError = 'Post not found'
@ -315,12 +319,10 @@
const postData = {
title: config?.showTitle ? title : null,
slug,
type: postType,
type: postType, // No mapping needed anymore
status: newStatus || status,
content: config?.showContent ? saveContent : null,
excerpt: postType === 'essay' ? excerpt : undefined,
link_url: undefined,
linkDescription: undefined,
tags
}
@ -476,7 +478,7 @@
<input type="text" bind:value={title} placeholder="Title" class="title-input" />
{/if}
{#if config?.showContent}
{#if config?.showContent && contentReady}
<div class="editor-wrapper">
<Editor bind:data={content} placeholder="Continue writing..." />
</div>

View file

@ -16,6 +16,7 @@
let postType = $state<'post' | 'essay'>('post')
let status = $state<'draft' | 'published'>('draft')
let slug = $state('')
let slugManuallySet = $state(false)
let excerpt = $state('')
let content = $state<JSONContent>({ type: 'doc', content: [] })
let tags = $state<string[]>([])
@ -23,6 +24,17 @@
let showMetadata = $state(false)
let metadataButtonRef: HTMLButtonElement
// Auto-generate slug from title when title changes and slug hasn't been manually set
$effect(() => {
if (title && !slugManuallySet) {
slug = title
.toLowerCase()
.replace(/[^a-z0-9\s]+/g, '') // Remove special characters but keep spaces
.replace(/\s+/g, '-') // Replace spaces with dashes
.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
}
})
const postTypeConfig = {
post: { icon: '💭', label: 'Post', showTitle: false, showContent: true },
essay: { icon: '📝', label: 'Essay', showTitle: true, showContent: true }
@ -37,24 +49,16 @@
postType = type as typeof postType
}
// Generate initial slug based on title
generateSlug()
})
function generateSlug() {
if (title) {
slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
// Check for draft content in sessionStorage
const draftContent = sessionStorage.getItem('draft_content')
if (draftContent) {
try {
content = JSON.parse(draftContent)
sessionStorage.removeItem('draft_content') // Clean up after use
} catch (e) {
console.error('Failed to parse draft content:', e)
}
}
// Auto-generate slug when title changes (only if slug is empty)
$effect(() => {
if (title && (!slug || slug === '')) {
generateSlug()
}
})
function addTag() {
@ -75,15 +79,11 @@
return
}
if (!slug) {
generateSlug()
}
saving = true
const postData = {
title: config?.showTitle ? title : null,
slug: slug || `post-${Date.now()}`,
postType,
type: postType, // No mapping needed anymore
status: publishStatus || status,
content: config?.showContent ? content : null,
excerpt: postType === 'essay' ? excerpt : undefined,
@ -170,6 +170,11 @@
onRemoveTag={removeTag}
onDelete={() => {}}
onClose={() => (showMetadata = false)}
onFieldUpdate={(key, value) => {
if (key === 'slug') {
slugManuallySet = true
}
}}
/>
{/if}
</div>

View file

@ -13,38 +13,7 @@ export const GET: RequestHandler = async (event) => {
try {
const post = await prisma.post.findUnique({
where: { slug },
include: {
album: {
select: {
id: true,
slug: true,
title: true,
description: true,
photos: {
orderBy: { displayOrder: 'asc' },
select: {
id: true,
url: true,
thumbnailUrl: true,
caption: true,
width: true,
height: true
}
}
}
},
photo: {
select: {
id: true,
url: true,
thumbnailUrl: true,
caption: true,
width: true,
height: true
}
}
}
where: { slug }
})
if (!post) {