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:
parent
3d993d76ed
commit
c6ce13a530
14 changed files with 109 additions and 128 deletions
|
|
@ -0,0 +1 @@
|
|||
-- This is an empty migration.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- Update existing postType values
|
||||
UPDATE "Post" SET "postType" = 'essay' WHERE "postType" = 'blog';
|
||||
UPDATE "Post" SET "postType" = 'post' WHERE "postType" = 'microblog';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,54 +4,54 @@ import { execSync } from 'child_process'
|
|||
const prisma = new PrismaClient()
|
||||
|
||||
async function isDatabaseInitialized(): Promise<boolean> {
|
||||
try {
|
||||
// Check if we have any completed migrations
|
||||
const migrationCount = await prisma.$queryRaw<[{ count: bigint }]>`
|
||||
try {
|
||||
// Check if we have any completed migrations
|
||||
const migrationCount = await prisma.$queryRaw<[{ count: bigint }]>`
|
||||
SELECT COUNT(*) as count
|
||||
FROM _prisma_migrations
|
||||
WHERE finished_at IS NOT NULL
|
||||
`
|
||||
|
||||
return migrationCount[0].count > 0n
|
||||
} catch (error: any) {
|
||||
// If the table doesn't exist, database is not initialized
|
||||
console.log('📊 Migration table check failed (expected on first deploy):', error.message)
|
||||
return false
|
||||
}
|
||||
|
||||
return migrationCount[0].count > 0n
|
||||
} catch (error: any) {
|
||||
// If the table doesn't exist, database is not initialized
|
||||
console.log('📊 Migration table check failed (expected on first deploy):', error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeDatabase() {
|
||||
console.log('🔍 Checking database initialization status...')
|
||||
|
||||
// Give the database a moment to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
try {
|
||||
const isInitialized = await isDatabaseInitialized()
|
||||
|
||||
if (!isInitialized) {
|
||||
console.log('📦 First time setup detected. Initializing database...')
|
||||
|
||||
// Run migrations
|
||||
console.log('🔄 Running database migrations...')
|
||||
execSync('npx prisma migrate deploy', { stdio: 'inherit' })
|
||||
|
||||
// Run seeds
|
||||
console.log('🌱 Seeding database...')
|
||||
execSync('npx prisma db seed', { stdio: 'inherit' })
|
||||
|
||||
console.log('✅ Database initialization complete!')
|
||||
} else {
|
||||
console.log('✅ Database already initialized. Running migrations only...')
|
||||
execSync('npx prisma migrate deploy', { stdio: 'inherit' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization failed:', error)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
console.log('🔍 Checking database initialization status...')
|
||||
|
||||
// Give the database a moment to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
try {
|
||||
const isInitialized = await isDatabaseInitialized()
|
||||
|
||||
if (!isInitialized) {
|
||||
console.log('📦 First time setup detected. Initializing database...')
|
||||
|
||||
// Run migrations
|
||||
console.log('🔄 Running database migrations...')
|
||||
execSync('npx prisma migrate deploy', { stdio: 'inherit' })
|
||||
|
||||
// Run seeds
|
||||
console.log('🌱 Seeding database...')
|
||||
execSync('npx prisma db seed', { stdio: 'inherit' })
|
||||
|
||||
console.log('✅ Database initialization complete!')
|
||||
} else {
|
||||
console.log('✅ Database already initialized. Running migrations only...')
|
||||
execSync('npx prisma migrate deploy', { stdio: 'inherit' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization failed:', error)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Run the initialization
|
||||
initializeDatabase()
|
||||
initializeDatabase()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -92,4 +95,4 @@
|
|||
{onAddTag}
|
||||
{onRemoveTag}
|
||||
{onClose}
|
||||
/>
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,23 +49,15 @@
|
|||
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, '')
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-generate slug when title changes (only if slug is empty)
|
||||
$effect(() => {
|
||||
if (title && (!slug || slug === '')) {
|
||||
generateSlug()
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue