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

View file

@ -4,54 +4,54 @@ import { execSync } from 'child_process'
const prisma = new PrismaClient() const prisma = new PrismaClient()
async function isDatabaseInitialized(): Promise<boolean> { async function isDatabaseInitialized(): Promise<boolean> {
try { try {
// Check if we have any completed migrations // Check if we have any completed migrations
const migrationCount = await prisma.$queryRaw<[{ count: bigint }]>` const migrationCount = await prisma.$queryRaw<[{ count: bigint }]>`
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM _prisma_migrations FROM _prisma_migrations
WHERE finished_at IS NOT NULL WHERE finished_at IS NOT NULL
` `
return migrationCount[0].count > 0n return migrationCount[0].count > 0n
} catch (error: any) { } catch (error: any) {
// If the table doesn't exist, database is not initialized // If the table doesn't exist, database is not initialized
console.log('📊 Migration table check failed (expected on first deploy):', error.message) console.log('📊 Migration table check failed (expected on first deploy):', error.message)
return false return false
} }
} }
async function initializeDatabase() { async function initializeDatabase() {
console.log('🔍 Checking database initialization status...') console.log('🔍 Checking database initialization status...')
// Give the database a moment to be ready // Give the database a moment to be ready
await new Promise(resolve => setTimeout(resolve, 2000)) await new Promise((resolve) => setTimeout(resolve, 2000))
try { try {
const isInitialized = await isDatabaseInitialized() const isInitialized = await isDatabaseInitialized()
if (!isInitialized) { if (!isInitialized) {
console.log('📦 First time setup detected. Initializing database...') console.log('📦 First time setup detected. Initializing database...')
// Run migrations // Run migrations
console.log('🔄 Running database migrations...') console.log('🔄 Running database migrations...')
execSync('npx prisma migrate deploy', { stdio: 'inherit' }) execSync('npx prisma migrate deploy', { stdio: 'inherit' })
// Run seeds // Run seeds
console.log('🌱 Seeding database...') console.log('🌱 Seeding database...')
execSync('npx prisma db seed', { stdio: 'inherit' }) execSync('npx prisma db seed', { stdio: 'inherit' })
console.log('✅ Database initialization complete!') console.log('✅ Database initialization complete!')
} else { } else {
console.log('✅ Database already initialized. Running migrations only...') console.log('✅ Database already initialized. Running migrations only...')
execSync('npx prisma migrate deploy', { stdio: 'inherit' }) execSync('npx prisma migrate deploy', { stdio: 'inherit' })
} }
} catch (error) { } catch (error) {
console.error('❌ Database initialization failed:', error) console.error('❌ Database initialization failed:', error)
process.exit(1) process.exit(1)
} finally { } finally {
await prisma.$disconnect() await prisma.$disconnect()
} }
} }
// Run the initialization // Run the initialization
initializeDatabase() initializeDatabase()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,38 +13,7 @@ export const GET: RequestHandler = async (event) => {
try { try {
const post = await prisma.post.findUnique({ const post = await prisma.post.findUnique({
where: { slug }, 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
}
}
}
}) })
if (!post) { if (!post) {