Componentization and styling
This commit is contained in:
parent
5c32be88c5
commit
cc09c9cd3f
8 changed files with 334 additions and 473 deletions
43
src/lib/components/admin/AdminByline.svelte
Normal file
43
src/lib/components/admin/AdminByline.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
sections: string[]
|
||||
}
|
||||
|
||||
let { sections }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="admin-byline">
|
||||
{#each sections as section, index}
|
||||
<span class="byline-section">{section}</span>
|
||||
{#if index < sections.length - 1}
|
||||
<span class="separator">·</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.admin-byline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-40;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.byline-section {
|
||||
// Remove text-transform: capitalize to allow proper sentence case
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: $grey-40;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 480px) {
|
||||
.admin-byline {
|
||||
font-size: 0.875rem;
|
||||
gap: $unit-half;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import AvatarSimple from '$lib/components/AvatarSimple.svelte'
|
||||
import DashboardIcon from '$icons/dashboard.svg?component'
|
||||
import WorkIcon from '$icons/work.svg?component'
|
||||
import UniverseIcon from '$icons/universe.svg?component'
|
||||
import PhotosIcon from '$icons/photos.svg?component'
|
||||
|
|
@ -15,7 +14,6 @@
|
|||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ text: 'Dashboard', href: '/admin', icon: DashboardIcon },
|
||||
{ text: 'Projects', href: '/admin/projects', icon: WorkIcon },
|
||||
{ text: 'Universe', href: '/admin/posts', icon: UniverseIcon },
|
||||
{ text: 'Albums', href: '/admin/albums', icon: PhotosIcon },
|
||||
|
|
@ -24,17 +22,15 @@
|
|||
|
||||
// Calculate active index based on current path
|
||||
const activeIndex = $derived(
|
||||
currentPath === '/admin'
|
||||
currentPath.startsWith('/admin/projects')
|
||||
? 0
|
||||
: currentPath.startsWith('/admin/projects')
|
||||
: currentPath.startsWith('/admin/posts')
|
||||
? 1
|
||||
: currentPath.startsWith('/admin/posts')
|
||||
: currentPath.startsWith('/admin/albums')
|
||||
? 2
|
||||
: currentPath.startsWith('/admin/albums')
|
||||
: currentPath.startsWith('/admin/media')
|
||||
? 3
|
||||
: currentPath.startsWith('/admin/media')
|
||||
? 4
|
||||
: -1
|
||||
: -1
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -71,65 +71,47 @@
|
|||
</Button>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="dropdown-menu">
|
||||
<ul class="dropdown-menu">
|
||||
{#each postTypes as type}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onclick={() => handleSelection(type.value)}
|
||||
class="dropdown-item"
|
||||
fullWidth
|
||||
pill={false}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div class="dropdown-icon">
|
||||
{#if type.value === 'essay'}
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M3 5C3 3.89543 3.89543 3 5 3H11L17 9V15C17 16.1046 16.1046 17 15 17H5C3.89543 17 3 16.1046 3 15V5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path d="M11 3V9H17" stroke="currentColor" stroke-width="1.5" />
|
||||
<path
|
||||
d="M7 13H13"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M7 10H13"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
{:else if type.value === 'post'}
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M4 3C2.89543 3 2 3.89543 2 5V11C2 12.1046 2.89543 13 4 13H6L8 16V13H13C14.1046 13 15 12.1046 15 11V5C15 3.89543 14.1046 3 13 3H4Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M5 7H12"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M5 9H10"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
<li class="dropdown-item" onclick={() => handleSelection(type.value)}>
|
||||
<div class="dropdown-icon">
|
||||
{#if type.value === 'essay'}
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M3 5C3 3.89543 3.89543 3 5 3H11L17 9V15C17 16.1046 16.1046 17 15 17H5C3.89543 17 3 16.1046 3 15V5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path d="M11 3V9H17" stroke="currentColor" stroke-width="1.5" />
|
||||
<path
|
||||
d="M7 13H13"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M7 10H13"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
{:else if type.value === 'post'}
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M4 3C2.89543 3 2 3.89543 2 5V11C2 12.1046 2.89543 13 4 13H6L8 16V13H13C14.1046 13 15 12.1046 15 11V5C15 3.89543 14.1046 3 13 3H4Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path d="M5 7H12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
<path d="M5 9H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="dropdown-label">{type.label}</span>
|
||||
</Button>
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -163,17 +145,41 @@
|
|||
border: 1px solid $grey-85;
|
||||
border-radius: $unit-2x;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
min-width: 220px;
|
||||
min-width: 140px;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
// Override Button component styles for dropdown items
|
||||
:global(.dropdown-item) {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
padding: $unit-2x $unit-3x;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-95;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: $unit-2x $unit-2x 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 $unit-2x $unit-2x;
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
border-radius: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
|
|
|
|||
183
src/lib/components/admin/PostListItem.svelte
Normal file
183
src/lib/components/admin/PostListItem.svelte
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import AdminByline from './AdminByline.svelte'
|
||||
|
||||
interface Post {
|
||||
id: number
|
||||
slug: string
|
||||
postType: string
|
||||
title: string | null
|
||||
content: any // JSON content
|
||||
excerpt: string | null
|
||||
status: string
|
||||
tags: string[] | null
|
||||
linkUrl: string | null
|
||||
linkDescription: string | null
|
||||
featuredImage: string | null
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
post: Post
|
||||
}
|
||||
|
||||
let { post }: Props = $props()
|
||||
|
||||
const postTypeLabels: Record<string, string> = {
|
||||
post: 'Post',
|
||||
essay: 'Essay',
|
||||
// Legacy types for backward compatibility
|
||||
blog: 'Essay',
|
||||
microblog: 'Post',
|
||||
link: 'Post',
|
||||
photo: 'Post',
|
||||
album: 'Album'
|
||||
}
|
||||
|
||||
function handlePostClick() {
|
||||
goto(`/admin/posts/${post.id}/edit`)
|
||||
}
|
||||
|
||||
function getPostSnippet(post: Post): string {
|
||||
// Try excerpt first
|
||||
if (post.excerpt) {
|
||||
return post.excerpt.length > 150 ? post.excerpt.substring(0, 150) + '...' : post.excerpt
|
||||
}
|
||||
|
||||
// Try to extract text from content JSON
|
||||
if (post.content) {
|
||||
let textContent = ''
|
||||
|
||||
if (typeof post.content === 'object' && post.content.content) {
|
||||
// BlockNote/TipTap format
|
||||
function extractText(node: any): string {
|
||||
if (node.text) return node.text
|
||||
if (node.content && Array.isArray(node.content)) {
|
||||
return node.content.map(extractText).join(' ')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
textContent = extractText(post.content)
|
||||
} else if (typeof post.content === 'string') {
|
||||
textContent = post.content
|
||||
}
|
||||
|
||||
if (textContent) {
|
||||
return textContent.length > 150 ? textContent.substring(0, 150) + '...' : textContent
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to link description for link posts
|
||||
if (post.linkDescription) {
|
||||
return post.linkDescription.length > 150
|
||||
? post.linkDescription.substring(0, 150) + '...'
|
||||
: post.linkDescription
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return `${postTypeLabels[post.postType] || post.postType} without content`
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
const diffTime = now.getTime() - date.getTime()
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) {
|
||||
return 'today'
|
||||
} else if (diffDays === 1) {
|
||||
return 'yesterday'
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} ${diffDays === 1 ? 'day' : 'days'} ago`
|
||||
} else {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="post-item" onclick={handlePostClick}>
|
||||
{#if post.title}
|
||||
<h3 class="post-title">{post.title}</h3>
|
||||
{/if}
|
||||
|
||||
<div class="post-content">
|
||||
{#if post.linkUrl}
|
||||
<p class="post-link-url">{post.linkUrl}</p>
|
||||
{/if}
|
||||
<p class="post-preview">{getPostSnippet(post)}</p>
|
||||
</div>
|
||||
|
||||
<AdminByline
|
||||
sections={[
|
||||
postTypeLabels[post.postType] || post.postType,
|
||||
post.status === 'published' ? 'Published' : 'Draft',
|
||||
post.status === 'published' && post.publishedAt
|
||||
? `published ${formatDate(post.publishedAt)}`
|
||||
: `created ${formatDate(post.createdAt)}`
|
||||
]}
|
||||
/>
|
||||
</article>
|
||||
|
||||
<style lang="scss">
|
||||
.post-item {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: $unit-2x;
|
||||
padding: $unit-2x;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
|
||||
&:hover {
|
||||
background: $grey-95;
|
||||
}
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.post-link-url {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: $blue-60;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.post-preview {
|
||||
margin: 0;
|
||||
font-size: 0.925rem;
|
||||
line-height: 1.5;
|
||||
color: $grey-30;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.post-item {
|
||||
padding: $unit-2x;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import AdminByline from './AdminByline.svelte'
|
||||
|
||||
interface Project {
|
||||
id: number
|
||||
|
|
@ -9,9 +10,11 @@
|
|||
year: number
|
||||
client: string | null
|
||||
status: string
|
||||
projectType: string
|
||||
logoUrl: string | null
|
||||
backgroundColor: string | null
|
||||
highlightColor: string | null
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
|
@ -36,11 +39,21 @@
|
|||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
||||
|
||||
if (diffInSeconds < 60) return 'just now'
|
||||
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`
|
||||
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`
|
||||
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`
|
||||
if (diffInSeconds < 31536000) return `${Math.floor(diffInSeconds / 2592000)} months ago`
|
||||
return `${Math.floor(diffInSeconds / 31536000)} years ago`
|
||||
|
||||
const minutes = Math.floor(diffInSeconds / 60)
|
||||
if (diffInSeconds < 3600) return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`
|
||||
|
||||
const hours = Math.floor(diffInSeconds / 3600)
|
||||
if (diffInSeconds < 86400) return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`
|
||||
|
||||
const days = Math.floor(diffInSeconds / 86400)
|
||||
if (diffInSeconds < 2592000) return `${days} ${days === 1 ? 'day' : 'days'} ago`
|
||||
|
||||
const months = Math.floor(diffInSeconds / 2592000)
|
||||
if (diffInSeconds < 31536000) return `${months} ${months === 1 ? 'month' : 'months'} ago`
|
||||
|
||||
const years = Math.floor(diffInSeconds / 31536000)
|
||||
return `${years} ${years === 1 ? 'year' : 'years'} ago`
|
||||
}
|
||||
|
||||
function handleProjectClick() {
|
||||
|
|
@ -79,13 +92,15 @@
|
|||
|
||||
<div class="project-info">
|
||||
<h3 class="project-title">{project.title}</h3>
|
||||
<div class="project-metadata">
|
||||
<span class="status" class:published={project.status === 'published'}>
|
||||
{project.status === 'published' ? 'Published' : 'Not published'}
|
||||
</span>
|
||||
<span class="separator">·</span>
|
||||
<span class="updated">Last updated {formatRelativeTime(project.updatedAt)}</span>
|
||||
</div>
|
||||
<AdminByline
|
||||
sections={[
|
||||
project.projectType === 'work' ? 'Work' : 'Labs',
|
||||
project.status === 'published' ? 'Published' : 'Draft',
|
||||
project.status === 'published' && project.publishedAt
|
||||
? `Published ${formatRelativeTime(project.publishedAt)}`
|
||||
: `Created ${formatRelativeTime(project.createdAt)}`
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-container">
|
||||
|
|
@ -171,23 +186,6 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.project-metadata {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-40;
|
||||
|
||||
.status {
|
||||
&.published {
|
||||
color: #22c55e; // Green color for published status
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: $grey-60;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -82,22 +82,22 @@
|
|||
}
|
||||
|
||||
&:disabled {
|
||||
background: $grey-95;
|
||||
background: $grey-90;
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal variant
|
||||
&.select-minimal {
|
||||
border: none;
|
||||
background: transparent;
|
||||
background: $grey-90;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: $grey-95;
|
||||
background: $grey-80;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: $grey-95;
|
||||
background: $grey-80;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
|
||||
import PostListItem from '$lib/components/admin/PostListItem.svelte'
|
||||
import PostDropdown from '$lib/components/admin/PostDropdown.svelte'
|
||||
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
|
||||
import Select from '$lib/components/admin/Select.svelte'
|
||||
|
|
@ -139,94 +140,6 @@
|
|||
applyFilter()
|
||||
}
|
||||
|
||||
function handlePostClick(post: Post) {
|
||||
goto(`/admin/posts/${post.id}/edit`)
|
||||
}
|
||||
|
||||
function getPostSnippet(post: Post): string {
|
||||
// Try excerpt first
|
||||
if (post.excerpt) {
|
||||
return post.excerpt.length > 150 ? post.excerpt.substring(0, 150) + '...' : post.excerpt
|
||||
}
|
||||
|
||||
// Try to extract text from content JSON
|
||||
if (post.content) {
|
||||
let textContent = ''
|
||||
|
||||
if (typeof post.content === 'object' && post.content.content) {
|
||||
// BlockNote/TipTap format
|
||||
function extractText(node: any): string {
|
||||
if (node.text) return node.text
|
||||
if (node.content && Array.isArray(node.content)) {
|
||||
return node.content.map(extractText).join(' ')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
textContent = extractText(post.content)
|
||||
} else if (typeof post.content === 'string') {
|
||||
textContent = post.content
|
||||
}
|
||||
|
||||
if (textContent) {
|
||||
return textContent.length > 150 ? textContent.substring(0, 150) + '...' : textContent
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to link description for link posts
|
||||
if (post.linkDescription) {
|
||||
return post.linkDescription.length > 150
|
||||
? post.linkDescription.substring(0, 150) + '...'
|
||||
: post.linkDescription
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return `${postTypeLabels[post.postType] || post.postType} without content`
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
const diffTime = now.getTime() - date.getTime()
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) {
|
||||
return 'Today'
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday'
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} days ago`
|
||||
} else {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayTitle(post: Post): string {
|
||||
if (post.title) return post.title
|
||||
|
||||
// For posts without titles, create a meaningful display title
|
||||
if (post.linkUrl) {
|
||||
try {
|
||||
const domain = new URL(post.linkUrl).hostname.replace('www.', '')
|
||||
return `Link to ${domain}`
|
||||
} catch {
|
||||
return 'Link post'
|
||||
}
|
||||
}
|
||||
|
||||
const snippet = getPostSnippet(post)
|
||||
if (
|
||||
snippet &&
|
||||
snippet !== `${postTypeLabels[post.postType] || post.postType} without content`
|
||||
) {
|
||||
return snippet.length > 50 ? snippet.substring(0, 50) + '...' : snippet
|
||||
}
|
||||
|
||||
return `${postTypeLabels[post.postType] || post.postType}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<AdminPage>
|
||||
|
|
@ -272,92 +185,7 @@
|
|||
{:else}
|
||||
<div class="posts-list">
|
||||
{#each filteredPosts as post}
|
||||
<article class="post-item" onclick={() => handlePostClick(post)}>
|
||||
<div class="post-header">
|
||||
<div class="post-meta">
|
||||
<span class="post-type">
|
||||
{postTypeIcons[post.postType] || '📄'}
|
||||
{postTypeLabels[post.postType] || post.postType}
|
||||
</span>
|
||||
<span class="post-date">{formatDate(post.updatedAt)}</span>
|
||||
</div>
|
||||
<div class="post-status">
|
||||
{#if post.status === 'published'}
|
||||
<span class="status-badge published">Published</span>
|
||||
{:else}
|
||||
<span class="status-badge draft">Draft</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-content">
|
||||
<h3 class="post-title">{getDisplayTitle(post)}</h3>
|
||||
|
||||
{#if post.linkUrl}
|
||||
<div class="post-link">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="link-url">{post.linkUrl}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p class="post-snippet">{getPostSnippet(post)}</p>
|
||||
|
||||
{#if post.tags && post.tags.length > 0}
|
||||
<div class="post-tags">
|
||||
{#each post.tags.slice(0, 3) as tag}
|
||||
<span class="tag">#{tag}</span>
|
||||
{/each}
|
||||
{#if post.tags.length > 3}
|
||||
<span class="tag-more">+{post.tags.length - 3} more</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="post-footer">
|
||||
<div class="post-actions">
|
||||
<span class="edit-hint">Click to edit</span>
|
||||
</div>
|
||||
<div class="post-indicator">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9 18l6-6-6-6"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<PostListItem {post} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -367,7 +195,6 @@
|
|||
<style lang="scss">
|
||||
@import '$styles/variables.scss';
|
||||
|
||||
|
||||
.error-message {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
|
|
@ -411,202 +238,8 @@
|
|||
.posts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-3x;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
background: white;
|
||||
border: 1px solid $grey-85;
|
||||
border-radius: 12px;
|
||||
padding: $unit-4x;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-3x;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-70;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.post-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: $unit-2x;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit-2x;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
|
||||
.post-type {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $unit-half;
|
||||
padding: $unit-half $unit;
|
||||
background: $grey-95;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: $grey-30;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.post-date {
|
||||
font-size: 0.875rem;
|
||||
color: $grey-50;
|
||||
}
|
||||
|
||||
.post-status {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: $unit-half $unit-2x;
|
||||
border-radius: 50px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&.published {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #16a34a;
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
&.draft {
|
||||
background: rgba(156, 163, 175, 0.1);
|
||||
color: #6b7280;
|
||||
border: 1px solid rgba(156, 163, 175, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.post-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
font-size: 0.875rem;
|
||||
color: $blue-60;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.link-url {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.post-snippet {
|
||||
margin: 0;
|
||||
font-size: 0.925rem;
|
||||
line-height: 1.5;
|
||||
color: $grey-30;
|
||||
}
|
||||
|
||||
.post-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.tag {
|
||||
font-size: 0.75rem;
|
||||
color: $grey-50;
|
||||
background: $grey-95;
|
||||
padding: $unit-half $unit;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tag-more {
|
||||
font-size: 0.75rem;
|
||||
color: $grey-50;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.post-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.post-actions {
|
||||
.edit-hint {
|
||||
font-size: 0.75rem;
|
||||
color: $grey-50;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.post-item:hover .edit-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.post-indicator {
|
||||
color: $grey-60;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.post-item:hover .post-indicator {
|
||||
color: $grey-30;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.post-item {
|
||||
padding: $unit-3x;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.post-item {
|
||||
padding: $unit-3x $unit-2x;
|
||||
}
|
||||
|
||||
.post-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.post-status {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
client: string | null
|
||||
status: string
|
||||
projectType: string
|
||||
logoUrl: string | null
|
||||
backgroundColor: string | null
|
||||
highlightColor: string | null
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue