Prettier + build errors
This commit is contained in:
parent
2a6291a547
commit
78443e2bdd
127 changed files with 2873 additions and 2292 deletions
|
|
@ -1,30 +1,23 @@
|
||||||
import type { StorybookConfig } from '@storybook/sveltekit';
|
import type { StorybookConfig } from '@storybook/sveltekit'
|
||||||
import { mergeConfig } from 'vite';
|
import { mergeConfig } from 'vite'
|
||||||
import path from 'path';
|
import path from 'path'
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: [
|
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
|
||||||
"../src/**/*.mdx",
|
addons: ['@storybook/addon-svelte-csf', '@storybook/addon-docs', '@storybook/addon-a11y'],
|
||||||
"../src/**/*.stories.@(js|ts|svelte)"
|
|
||||||
],
|
|
||||||
addons: [
|
|
||||||
"@storybook/addon-svelte-csf",
|
|
||||||
"@storybook/addon-docs",
|
|
||||||
"@storybook/addon-a11y"
|
|
||||||
],
|
|
||||||
framework: {
|
framework: {
|
||||||
name: "@storybook/sveltekit",
|
name: '@storybook/sveltekit',
|
||||||
options: {}
|
options: {}
|
||||||
},
|
},
|
||||||
viteFinal: async (config) => {
|
viteFinal: async (config) => {
|
||||||
return mergeConfig(config, {
|
return mergeConfig(config, {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'$lib': path.resolve('./src/lib'),
|
$lib: path.resolve('./src/lib'),
|
||||||
'$components': path.resolve('./src/lib/components'),
|
$components: path.resolve('./src/lib/components'),
|
||||||
'$icons': path.resolve('./src/assets/icons'),
|
$icons: path.resolve('./src/assets/icons'),
|
||||||
'$illos': path.resolve('./src/assets/illos'),
|
$illos: path.resolve('./src/assets/illos'),
|
||||||
'$styles': path.resolve('./src/assets/styles')
|
$styles: path.resolve('./src/assets/styles')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
|
|
@ -39,8 +32,8 @@ const config: StorybookConfig = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
export default config
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Preview } from '@storybook/sveltekit';
|
import type { Preview } from '@storybook/sveltekit'
|
||||||
import '../src/assets/styles/reset.css';
|
import '../src/assets/styles/reset.css'
|
||||||
import '../src/assets/styles/globals.scss';
|
import '../src/assets/styles/globals.scss'
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|
@ -8,8 +8,8 @@ const preview: Preview = {
|
||||||
controls: {
|
controls: {
|
||||||
matchers: {
|
matchers: {
|
||||||
color: /(background|color)$/i,
|
color: /(background|color)$/i,
|
||||||
date: /Date$/,
|
date: /Date$/
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
backgrounds: {
|
backgrounds: {
|
||||||
default: 'light',
|
default: 'light',
|
||||||
|
|
@ -17,8 +17,8 @@ const preview: Preview = {
|
||||||
{ name: 'light', value: '#ffffff' },
|
{ name: 'light', value: '#ffffff' },
|
||||||
{ name: 'dark', value: '#333333' },
|
{ name: 'dark', value: '#333333' },
|
||||||
{ name: 'admin', value: '#f5f5f5' },
|
{ name: 'admin', value: '#f5f5f5' },
|
||||||
{ name: 'grey-95', value: '#f8f9fa' },
|
{ name: 'grey-95', value: '#f8f9fa' }
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
viewport: {
|
viewport: {
|
||||||
viewports: {
|
viewports: {
|
||||||
|
|
@ -33,10 +33,10 @@ const preview: Preview = {
|
||||||
desktop: {
|
desktop: {
|
||||||
name: 'Desktop',
|
name: 'Desktop',
|
||||||
styles: { width: '1440px', height: '900px' }
|
styles: { width: '1440px', height: '900px' }
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default preview;
|
export default preview
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ We are using Svelte 5 in Runes mode, so make sure to only write solutions that w
|
||||||
|
|
||||||
Make sure to use the CSS variables that are defined across the various files in `src/assets/styles`. When making new colors or defining new variables, check that it doesn't exist first, then define it.
|
Make sure to use the CSS variables that are defined across the various files in `src/assets/styles`. When making new colors or defining new variables, check that it doesn't exist first, then define it.
|
||||||
|
|
||||||
|
|
||||||
### Key Architecture Components
|
### Key Architecture Components
|
||||||
|
|
||||||
**API Integration Layer** (`src/routes/api/`)
|
**API Integration Layer** (`src/routes/api/`)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||||
import storybook from "eslint-plugin-storybook";
|
import storybook from 'eslint-plugin-storybook'
|
||||||
|
|
||||||
import js from '@eslint/js'
|
import js from '@eslint/js'
|
||||||
import ts from 'typescript-eslint'
|
import ts from 'typescript-eslint'
|
||||||
|
|
@ -33,5 +33,5 @@ export default [
|
||||||
{
|
{
|
||||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||||
},
|
},
|
||||||
...storybook.configs["flat/recommended"]
|
...storybook.configs['flat/recommended']
|
||||||
];
|
]
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,8 @@ async function main() {
|
||||||
slug: 'granblue-team',
|
slug: 'granblue-team',
|
||||||
title: 'granblue.team',
|
title: 'granblue.team',
|
||||||
subtitle: 'Comprehensive web app for Granblue Fantasy players',
|
subtitle: 'Comprehensive web app for Granblue Fantasy players',
|
||||||
description: 'A comprehensive web application for Granblue Fantasy players to track raids, manage crews, and optimize team compositions. Features real-time raid tracking, character databases, and community tools.',
|
description:
|
||||||
|
'A comprehensive web application for Granblue Fantasy players to track raids, manage crews, and optimize team compositions. Features real-time raid tracking, character databases, and community tools.',
|
||||||
year: 2022,
|
year: 2022,
|
||||||
client: 'Personal Project',
|
client: 'Personal Project',
|
||||||
role: 'Full-Stack Developer',
|
role: 'Full-Stack Developer',
|
||||||
|
|
@ -119,7 +120,8 @@ async function main() {
|
||||||
slug: 'subway-board',
|
slug: 'subway-board',
|
||||||
title: 'Subway Board',
|
title: 'Subway Board',
|
||||||
subtitle: 'Beautiful, minimalist NYC subway dashboard',
|
subtitle: 'Beautiful, minimalist NYC subway dashboard',
|
||||||
description: 'A beautiful, minimalist dashboard displaying real-time NYC subway arrival times. Clean interface inspired by the classic subway map design with live MTA data integration.',
|
description:
|
||||||
|
'A beautiful, minimalist dashboard displaying real-time NYC subway arrival times. Clean interface inspired by the classic subway map design with live MTA data integration.',
|
||||||
year: 2023,
|
year: 2023,
|
||||||
client: 'Personal Project',
|
client: 'Personal Project',
|
||||||
role: 'Developer & Designer',
|
role: 'Developer & Designer',
|
||||||
|
|
@ -136,7 +138,8 @@ async function main() {
|
||||||
slug: 'siero-discord',
|
slug: 'siero-discord',
|
||||||
title: 'Siero for Discord',
|
title: 'Siero for Discord',
|
||||||
subtitle: 'Discord bot for Granblue Fantasy communities',
|
subtitle: 'Discord bot for Granblue Fantasy communities',
|
||||||
description: 'A Discord bot for Granblue Fantasy communities providing character lookups, raid notifications, and server management tools. Serves thousands of users across multiple servers.',
|
description:
|
||||||
|
'A Discord bot for Granblue Fantasy communities providing character lookups, raid notifications, and server management tools. Serves thousands of users across multiple servers.',
|
||||||
year: 2021,
|
year: 2021,
|
||||||
client: 'Personal Project',
|
client: 'Personal Project',
|
||||||
role: 'Bot Developer',
|
role: 'Bot Developer',
|
||||||
|
|
@ -153,7 +156,8 @@ async function main() {
|
||||||
slug: 'homelab',
|
slug: 'homelab',
|
||||||
title: 'Homelab',
|
title: 'Homelab',
|
||||||
subtitle: 'Self-hosted infrastructure on Kubernetes',
|
subtitle: 'Self-hosted infrastructure on Kubernetes',
|
||||||
description: 'Self-hosted infrastructure running on Kubernetes with monitoring, media servers, and development environments. Includes automated deployments and backup strategies.',
|
description:
|
||||||
|
'Self-hosted infrastructure running on Kubernetes with monitoring, media servers, and development environments. Includes automated deployments and backup strategies.',
|
||||||
year: 2023,
|
year: 2023,
|
||||||
client: 'Personal Project',
|
client: 'Personal Project',
|
||||||
role: 'DevOps Engineer',
|
role: 'DevOps Engineer',
|
||||||
|
|
@ -181,11 +185,13 @@ async function main() {
|
||||||
{ type: 'paragraph', content: 'This is my first essay on the new CMS!' },
|
{ type: 'paragraph', content: 'This is my first essay on the new CMS!' },
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: 'The system now uses a simplified post type system with just essays and posts.'
|
content:
|
||||||
|
'The system now uses a simplified post type system with just essays and posts.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: 'Essays are perfect for longer-form content with titles and excerpts, while posts are great for quick thoughts and updates.'
|
content:
|
||||||
|
'Essays are perfect for longer-form content with titles and excerpts, while posts are great for quick thoughts and updates.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -203,7 +209,8 @@ async function main() {
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: 'Just pushed a major update to the site. The new simplified post types are working great! 🎉'
|
content:
|
||||||
|
'Just pushed a major update to the site. The new simplified post types are working great! 🎉'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -221,7 +228,8 @@ async function main() {
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: 'Design systems have become essential for maintaining consistency across large products.'
|
content:
|
||||||
|
'Design systems have become essential for maintaining consistency across large products.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
|
|
@ -229,7 +237,8 @@ async function main() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: 'Too rigid, and designers feel boxed in. Too flexible, and you lose consistency.'
|
content:
|
||||||
|
'Too rigid, and designers feel boxed in. Too flexible, and you lose consistency.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -264,7 +273,8 @@ async function main() {
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: 'Built a small CLI tool over the weekend. Sometimes the best projects come from scratching your own itch.'
|
content:
|
||||||
|
'Built a small CLI tool over the weekend. Sometimes the best projects come from scratching your own itch.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export function getAuthHeaders(): HeadersInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'Authorization': `Basic ${adminCredentials}`
|
Authorization: `Basic ${adminCredentials}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,7 +32,10 @@ export function clearAuth() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make authenticated API request
|
// Make authenticated API request
|
||||||
export async function authenticatedFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
export async function authenticatedFetch(
|
||||||
|
url: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<Response> {
|
||||||
const headers = {
|
const headers = {
|
||||||
...getAuthHeaders(),
|
...getAuthHeaders(),
|
||||||
...options.headers
|
...options.headers
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,12 @@
|
||||||
|
|
||||||
const getPostTypeLabel = (postType: string) => {
|
const getPostTypeLabel = (postType: string) => {
|
||||||
switch (postType) {
|
switch (postType) {
|
||||||
case 'post': return 'Post'
|
case 'post':
|
||||||
case 'essay': return 'Essay'
|
return 'Post'
|
||||||
default: return 'Post'
|
case 'essay':
|
||||||
|
return 'Essay'
|
||||||
|
default:
|
||||||
|
return 'Post'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,18 +45,22 @@
|
||||||
|
|
||||||
case 'bulletList':
|
case 'bulletList':
|
||||||
case 'ul':
|
case 'ul':
|
||||||
const listItems = (block.content || []).map((item: any) => {
|
const listItems = (block.content || [])
|
||||||
|
.map((item: any) => {
|
||||||
const itemText = item.content || item.text || ''
|
const itemText = item.content || item.text || ''
|
||||||
return `<li>${itemText}</li>`
|
return `<li>${itemText}</li>`
|
||||||
}).join('')
|
})
|
||||||
|
.join('')
|
||||||
return `<ul>${listItems}</ul>`
|
return `<ul>${listItems}</ul>`
|
||||||
|
|
||||||
case 'orderedList':
|
case 'orderedList':
|
||||||
case 'ol':
|
case 'ol':
|
||||||
const orderedItems = (block.content || []).map((item: any) => {
|
const orderedItems = (block.content || [])
|
||||||
|
.map((item: any) => {
|
||||||
const itemText = item.content || item.text || ''
|
const itemText = item.content || item.text || ''
|
||||||
return `<li>${itemText}</li>`
|
return `<li>${itemText}</li>`
|
||||||
}).join('')
|
})
|
||||||
|
.join('')
|
||||||
return `<ol>${orderedItems}</ol>`
|
return `<ol>${orderedItems}</ol>`
|
||||||
|
|
||||||
case 'blockquote':
|
case 'blockquote':
|
||||||
|
|
@ -110,11 +117,13 @@
|
||||||
|
|
||||||
{#if post.linkUrl}
|
{#if post.linkUrl}
|
||||||
<div class="post-link-preview">
|
<div class="post-link-preview">
|
||||||
<LinkCard link={{
|
<LinkCard
|
||||||
|
link={{
|
||||||
url: post.linkUrl,
|
url: post.linkUrl,
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.linkDescription
|
description: post.linkDescription
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -316,7 +325,8 @@
|
||||||
background: $grey-95;
|
background: $grey-95;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: $grey-10;
|
color: $grey-10;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,16 @@
|
||||||
{#if project.status === 'password-protected'}
|
{#if project.status === 'password-protected'}
|
||||||
<div class="status-indicator password-protected">
|
<div class="status-indicator password-protected">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="11"
|
||||||
|
width="18"
|
||||||
|
height="11"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
<circle cx="12" cy="16" r="1" fill="currentColor" />
|
<circle cx="12" cy="16" r="1" fill="currentColor" />
|
||||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" stroke="currentColor" stroke-width="2" />
|
<path d="M7 11V7a5 5 0 0 1 10 0v4" stroke="currentColor" stroke-width="2" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -81,8 +90,20 @@
|
||||||
{#if project.status === 'list-only'}
|
{#if project.status === 'list-only'}
|
||||||
<div class="status-indicator list-only">
|
<div class="status-indicator list-only">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path
|
||||||
<path d="M1 1l22 22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M1 1l22 22"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>View Only</span>
|
<span>View Only</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
error = ''
|
error = ''
|
||||||
|
|
||||||
// Simulate a small delay for better UX
|
// Simulate a small delay for better UX
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
if (password === correctPassword) {
|
if (password === correctPassword) {
|
||||||
// Store in session storage
|
// Store in session storage
|
||||||
|
|
@ -63,7 +63,13 @@
|
||||||
{#snippet passwordHeader()}
|
{#snippet passwordHeader()}
|
||||||
<div class="password-header">
|
<div class="password-header">
|
||||||
<div class="lock-icon">
|
<div class="lock-icon">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18 11H6C5.45 11 5 11.45 5 12V19C5 19.55 5.45 20 6 20H18C18.55 20 19 19.55 19 19V12C19 11.45 18.55 11 18 11Z"
|
d="M18 11H6C5.45 11 5 11.45 5 12V19C5 19.55 5.45 20 6 20H18C18.55 20 19 19.55 19 19V12C19 11.45 18.55 11 18 11Z"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|
@ -191,7 +197,9 @@
|
||||||
border: 1px solid $grey-80;
|
border: 1px solid $grey-80;
|
||||||
border-radius: $unit;
|
border-radius: $unit;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,7 @@
|
||||||
<article class="universe-album-card">
|
<article class="universe-album-card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="album-type-badge">
|
<div class="album-type-badge">Album</div>
|
||||||
Album
|
|
||||||
</div>
|
|
||||||
<time class="album-date" datetime={album.publishedAt}>
|
<time class="album-date" datetime={album.publishedAt}>
|
||||||
{formatDate(album.publishedAt)}
|
{formatDate(album.publishedAt)}
|
||||||
</time>
|
</time>
|
||||||
|
|
@ -60,9 +58,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<a href="/photos/{album.slug}" class="view-album">
|
<a href="/photos/{album.slug}" class="view-album"> View album → </a>
|
||||||
View album →
|
|
||||||
</a>
|
|
||||||
<UniverseIcon class="universe-icon" />
|
<UniverseIcon class="universe-icon" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,12 @@
|
||||||
|
|
||||||
const getPostTypeLabel = (postType: string) => {
|
const getPostTypeLabel = (postType: string) => {
|
||||||
switch (postType) {
|
switch (postType) {
|
||||||
case 'post': return 'Post'
|
case 'post':
|
||||||
case 'essay': return 'Essay'
|
return 'Post'
|
||||||
default: return 'Post'
|
case 'essay':
|
||||||
|
return 'Essay'
|
||||||
|
default:
|
||||||
|
return 'Post'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,9 +88,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<a href="/universe/{post.slug}" class="read-more">
|
<a href="/universe/{post.slug}" class="read-more"> Read more → </a>
|
||||||
Read more →
|
|
||||||
</a>
|
|
||||||
<UniverseIcon class="universe-icon" />
|
<UniverseIcon class="universe-icon" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,11 @@
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (initialData && mode === 'edit') {
|
if (initialData && mode === 'edit') {
|
||||||
// Parse album content structure
|
// Parse album content structure
|
||||||
if (initialData.content && typeof initialData.content === 'object' && 'type' in initialData.content) {
|
if (
|
||||||
|
initialData.content &&
|
||||||
|
typeof initialData.content === 'object' &&
|
||||||
|
'type' in initialData.content
|
||||||
|
) {
|
||||||
const albumContent = initialData.content as any
|
const albumContent = initialData.content as any
|
||||||
if (albumContent.type === 'album') {
|
if (albumContent.type === 'album') {
|
||||||
// Album content structure: { type: 'album', gallery: [mediaIds], description: JSONContent }
|
// Album content structure: { type: 'album', gallery: [mediaIds], description: JSONContent }
|
||||||
|
|
@ -80,7 +84,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const mediaResults = await Promise.all(mediaPromises)
|
const mediaResults = await Promise.all(mediaPromises)
|
||||||
gallery = mediaResults.filter(media => media !== null)
|
gallery = mediaResults.filter((media) => media !== null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load gallery media:', error)
|
console.error('Failed to load gallery media:', error)
|
||||||
}
|
}
|
||||||
|
|
@ -114,9 +118,9 @@
|
||||||
postType: 'album',
|
postType: 'album',
|
||||||
status: newStatus,
|
status: newStatus,
|
||||||
content,
|
content,
|
||||||
gallery: gallery.map(media => media.id),
|
gallery: gallery.map((media) => media.id),
|
||||||
featuredImage: gallery.length > 0 ? gallery[0].id : undefined,
|
featuredImage: gallery.length > 0 ? gallery[0].id : undefined,
|
||||||
tags: tags.trim() ? tags.split(',').map(tag => tag.trim()) : []
|
tags: tags.trim() ? tags.split(',').map((tag) => tag.trim()) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = mode === 'edit' ? `/api/posts/${postId}` : '/api/posts'
|
const url = mode === 'edit' ? `/api/posts/${postId}` : '/api/posts'
|
||||||
|
|
@ -197,19 +201,23 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
{#if mode === 'create'}
|
{#if mode === 'create'}
|
||||||
<Button variant="secondary" onclick={handleCancel} disabled={isSaving}>
|
<Button variant="secondary" onclick={handleCancel} disabled={isSaving}>Cancel</Button>
|
||||||
Cancel
|
<Button
|
||||||
</Button>
|
variant="secondary"
|
||||||
<Button variant="secondary" onclick={() => handleSave('draft')} disabled={!isValid || isSaving}>
|
onclick={() => handleSave('draft')}
|
||||||
|
disabled={!isValid || isSaving}
|
||||||
|
>
|
||||||
{isSaving ? 'Saving...' : 'Save Draft'}
|
{isSaving ? 'Saving...' : 'Save Draft'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onclick={() => handleSave('published')} disabled={!isValid || isSaving}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onclick={() => handleSave('published')}
|
||||||
|
disabled={!isValid || isSaving}
|
||||||
|
>
|
||||||
{isSaving ? 'Publishing...' : 'Publish Album'}
|
{isSaving ? 'Publishing...' : 'Publish Album'}
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button variant="secondary" onclick={handleCancel} disabled={isSaving}>
|
<Button variant="secondary" onclick={handleCancel} disabled={isSaving}>Cancel</Button>
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" onclick={() => handleSave()} disabled={!isValid || isSaving}>
|
<Button variant="primary" onclick={() => handleSave()} disabled={!isValid || isSaving}>
|
||||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
// Get thumbnail - try cover photo first, then first photo
|
// Get thumbnail - try cover photo first, then first photo
|
||||||
function getThumbnailUrl(): string | null {
|
function getThumbnailUrl(): string | null {
|
||||||
if (album.coverPhotoId && album.photos.length > 0) {
|
if (album.coverPhotoId && album.photos.length > 0) {
|
||||||
const coverPhoto = album.photos.find(p => p.id === album.coverPhotoId)
|
const coverPhoto = album.photos.find((p) => p.id === album.coverPhotoId)
|
||||||
if (coverPhoto) {
|
if (coverPhoto) {
|
||||||
return coverPhoto.thumbnailUrl || coverPhoto.url
|
return coverPhoto.thumbnailUrl || coverPhoto.url
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,7 @@
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { onclick, variant = 'default', disabled = false, children }: Props = $props()
|
||||||
onclick,
|
|
||||||
variant = 'default',
|
|
||||||
disabled = false,
|
|
||||||
children
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
function handleClick(event: MouseEvent) {
|
function handleClick(event: MouseEvent) {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,7 @@
|
||||||
divider?: boolean
|
divider?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { isOpen = $bindable(), triggerElement, items, onClose }: Props = $props()
|
||||||
isOpen = $bindable(),
|
|
||||||
triggerElement,
|
|
||||||
items,
|
|
||||||
onClose
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
let dropdownElement: HTMLDivElement
|
let dropdownElement: HTMLDivElement
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,9 @@
|
||||||
if (!clipboardData) return false
|
if (!clipboardData) return false
|
||||||
|
|
||||||
// Check for images first
|
// Check for images first
|
||||||
const imageItem = Array.from(clipboardData.items).find(item => item.type.indexOf('image') === 0)
|
const imageItem = Array.from(clipboardData.items).find(
|
||||||
|
(item) => item.type.indexOf('image') === 0
|
||||||
|
)
|
||||||
if (imageItem) {
|
if (imageItem) {
|
||||||
const file = imageItem.getAsFile()
|
const file = imageItem.getAsFile()
|
||||||
if (!file) return false
|
if (!file) return false
|
||||||
|
|
|
||||||
|
|
@ -264,18 +264,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<Input
|
<Input label="Title" bind:value={title} required placeholder="Essay title" />
|
||||||
label="Title"
|
|
||||||
bind:value={title}
|
|
||||||
required
|
|
||||||
placeholder="Essay title"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input label="Slug" bind:value={slug} placeholder="essay-url-slug" />
|
||||||
label="Slug"
|
|
||||||
bind:value={slug}
|
|
||||||
placeholder="essay-url-slug"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
|
@ -366,7 +357,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.admin-container {
|
.admin-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
@ -383,7 +373,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Custom styles for save/publish buttons to maintain grey color scheme
|
// Custom styles for save/publish buttons to maintain grey color scheme
|
||||||
:global(.save-button.btn-primary) {
|
:global(.save-button.btn-primary) {
|
||||||
background-color: $grey-10;
|
background-color: $grey-10;
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@
|
||||||
|
|
||||||
function handleImagesSelect(media: Media[]) {
|
function handleImagesSelect(media: Media[]) {
|
||||||
// Add new images to existing ones, avoiding duplicates
|
// Add new images to existing ones, avoiding duplicates
|
||||||
const existingIds = new Set(value.map(item => item.id))
|
const existingIds = new Set(value.map((item) => item.id))
|
||||||
const newImages = media.filter(item => !existingIds.has(item.id))
|
const newImages = media.filter((item) => !existingIds.has(item.id))
|
||||||
|
|
||||||
if (maxItems) {
|
if (maxItems) {
|
||||||
const availableSlots = maxItems - value.length
|
const availableSlots = maxItems - value.length
|
||||||
|
|
@ -117,10 +117,8 @@
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const hasImages = $derived(value.length > 0)
|
const hasImages = $derived(value.length > 0)
|
||||||
const canAddMore = $derived(!maxItems || value.length < maxItems)
|
const canAddMore = $derived(!maxItems || value.length < maxItems)
|
||||||
const selectedIds = $derived(value.map(item => item.id))
|
const selectedIds = $derived(value.map((item) => item.id))
|
||||||
const itemsText = $derived(
|
const itemsText = $derived(value.length === 1 ? '1 image' : `${value.length} images`)
|
||||||
value.length === 1 ? '1 image' : `${value.length} images`
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="gallery-manager">
|
<div class="gallery-manager">
|
||||||
|
|
@ -158,7 +156,13 @@
|
||||||
>
|
>
|
||||||
<!-- Drag Handle -->
|
<!-- Drag Handle -->
|
||||||
<div class="drag-handle">
|
<div class="drag-handle">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<circle cx="9" cy="12" r="1" fill="currentColor" />
|
<circle cx="9" cy="12" r="1" fill="currentColor" />
|
||||||
<circle cx="9" cy="5" r="1" fill="currentColor" />
|
<circle cx="9" cy="5" r="1" fill="currentColor" />
|
||||||
<circle cx="9" cy="19" r="1" fill="currentColor" />
|
<circle cx="9" cy="19" r="1" fill="currentColor" />
|
||||||
|
|
@ -174,10 +178,29 @@
|
||||||
<img src={item.thumbnailUrl} alt={item.filename} />
|
<img src={item.thumbnailUrl} alt={item.filename} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="image-placeholder">
|
<div class="image-placeholder">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="2"/>
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="5"
|
||||||
|
width="18"
|
||||||
|
height="14"
|
||||||
|
rx="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
||||||
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="2" fill="none"/>
|
<path
|
||||||
|
d="M3 16l5-5 3 3 4-4 4 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -198,7 +221,13 @@
|
||||||
onclick={() => removeImage(index)}
|
onclick={() => removeImage(index)}
|
||||||
aria-label="Remove image"
|
aria-label="Remove image"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M6 6L18 18M6 18L18 6"
|
d="M6 6L18 18M6 18L18 6"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|
@ -217,13 +246,15 @@
|
||||||
|
|
||||||
<!-- Add More Button (if within grid) -->
|
<!-- Add More Button (if within grid) -->
|
||||||
{#if canAddMore}
|
{#if canAddMore}
|
||||||
<button
|
<button type="button" class="add-more-item" onclick={openModal}>
|
||||||
type="button"
|
|
||||||
class="add-more-item"
|
|
||||||
onclick={openModal}
|
|
||||||
>
|
|
||||||
<div class="add-icon">
|
<div class="add-icon">
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M12 5v14m-7-7h14"
|
d="M12 5v14m-7-7h14"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|
@ -241,8 +272,22 @@
|
||||||
<div class="empty-state" class:has-error={error}>
|
<div class="empty-state" class:has-error={error}>
|
||||||
<div class="empty-content">
|
<div class="empty-content">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="5"
|
||||||
|
width="18"
|
||||||
|
height="14"
|
||||||
|
rx="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
||||||
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="1.5" fill="none" />
|
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -135,10 +135,7 @@
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
const target = event.target as Node
|
const target = event.target as Node
|
||||||
// Don't close if clicking inside the trigger button or the popover itself
|
// Don't close if clicking inside the trigger button or the popover itself
|
||||||
if (
|
if (triggerElement?.contains(target) || popoverElement?.contains(target)) {
|
||||||
triggerElement?.contains(target) ||
|
|
||||||
popoverElement?.contains(target)
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onClose()
|
onClose()
|
||||||
|
|
@ -221,7 +218,7 @@
|
||||||
label={field.label}
|
label={field.label}
|
||||||
bind:value={data.tagInput}
|
bind:value={data.tagInput}
|
||||||
onkeydown={(e) => e.key === 'Enter' && (e.preventDefault(), onAddTag())}
|
onkeydown={(e) => e.key === 'Enter' && (e.preventDefault(), onAddTag())}
|
||||||
placeholder={field.placeholder || "Add tags..."}
|
placeholder={field.placeholder || 'Add tags...'}
|
||||||
/>
|
/>
|
||||||
<button type="button" onclick={onAddTag} class="add-tag-btn">Add</button>
|
<button type="button" onclick={onAddTag} class="add-tag-btn">Add</button>
|
||||||
|
|
||||||
|
|
@ -459,7 +456,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@include breakpoint('phone') {
|
@include breakpoint('phone') {
|
||||||
.metadata-popover {
|
.metadata-popover {
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,7 @@
|
||||||
? 'aspect-ratio: 16/9;'
|
? 'aspect-ratio: 16/9;'
|
||||||
: (() => {
|
: (() => {
|
||||||
const [width, height] = aspectRatio.split(':').map(Number)
|
const [width, height] = aspectRatio.split(':').map(Number)
|
||||||
return width && height
|
return width && height ? `aspect-ratio: ${width}/${height};` : 'aspect-ratio: 16/9;'
|
||||||
? `aspect-ratio: ${width}/${height};`
|
|
||||||
: 'aspect-ratio: 16/9;'
|
|
||||||
})()
|
})()
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -82,16 +80,12 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={openModal}
|
onclick={openModal}
|
||||||
onkeydown={(e) => e.key === 'Enter' && openModal()}
|
onkeydown={(e) => e.key === 'Enter' && openModal()}
|
||||||
onmouseenter={() => isHovering = true}
|
onmouseenter={() => (isHovering = true)}
|
||||||
onmouseleave={() => isHovering = false}
|
onmouseleave={() => (isHovering = false)}
|
||||||
>
|
>
|
||||||
{#if hasImage && value}
|
{#if hasImage && value}
|
||||||
<!-- Image Display -->
|
<!-- Image Display -->
|
||||||
<img
|
<img src={value.url} alt={value.filename} class="preview-image" />
|
||||||
src={value.url}
|
|
||||||
alt={value.filename}
|
|
||||||
class="preview-image"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Hover Overlay -->
|
<!-- Hover Overlay -->
|
||||||
{#if isHovering}
|
{#if isHovering}
|
||||||
|
|
@ -149,8 +143,22 @@
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="5"
|
||||||
|
width="18"
|
||||||
|
height="14"
|
||||||
|
rx="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
||||||
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="1.5" fill="none" />
|
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@
|
||||||
const selectedMedia = Array.isArray(media) ? media[0] : media
|
const selectedMedia = Array.isArray(media) ? media[0] : media
|
||||||
if (selectedMedia) {
|
if (selectedMedia) {
|
||||||
// Set a reasonable default width (max 600px)
|
// Set a reasonable default width (max 600px)
|
||||||
const displayWidth = selectedMedia.width && selectedMedia.width > 600 ? 600 : selectedMedia.width
|
const displayWidth =
|
||||||
|
selectedMedia.width && selectedMedia.width > 600 ? 600 : selectedMedia.width
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
|
|
@ -273,8 +274,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.edra-image-placeholder-icon) {
|
:global(.edra-image-placeholder-icon) {
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,6 @@
|
||||||
isUploading = false
|
isUploading = false
|
||||||
uploadProgress = 0
|
uploadProgress = 0
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
isUploading = false
|
isUploading = false
|
||||||
uploadProgress = 0
|
uploadProgress = 0
|
||||||
|
|
@ -276,7 +275,7 @@
|
||||||
alt={value?.altText || value?.filename || 'Uploaded image'}
|
alt={value?.altText || value?.filename || 'Uploaded image'}
|
||||||
containerWidth={100}
|
containerWidth={100}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
aspectRatio={aspectRatio}
|
{aspectRatio}
|
||||||
class="preview-image"
|
class="preview-image"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -288,9 +287,28 @@
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="overlay" buttonSize="small" onclick={handleRemove}>
|
<Button variant="overlay" buttonSize="small" onclick={handleRemove}>
|
||||||
<svg slot="icon" width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<polyline points="3,6 5,6 21,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
slot="icon"
|
||||||
<path d="M19 6V20A2 2 0 0 1 17 22H7A2 2 0 0 1 5 20V6M8 6V4A2 2 0 0 1 10 2H14A2 2 0 0 1 16 4V6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polyline
|
||||||
|
points="3,6 5,6 21,6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19 6V20A2 2 0 0 1 17 22H7A2 2 0 0 1 5 20V6M8 6V4A2 2 0 0 1 10 2H14A2 2 0 0 1 16 4V6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -331,7 +349,7 @@
|
||||||
alt={value?.altText || value?.filename || 'Uploaded image'}
|
alt={value?.altText || value?.filename || 'Uploaded image'}
|
||||||
containerWidth={800}
|
containerWidth={800}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
aspectRatio={aspectRatio}
|
{aspectRatio}
|
||||||
class="preview-image"
|
class="preview-image"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -344,9 +362,28 @@
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="overlay" buttonSize="small" onclick={handleRemove}>
|
<Button variant="overlay" buttonSize="small" onclick={handleRemove}>
|
||||||
<svg slot="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<polyline points="3,6 5,6 21,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
slot="icon"
|
||||||
<path d="M19 6V20A2 2 0 0 1 17 22H7A2 2 0 0 1 5 20V6M8 6V4A2 2 0 0 1 10 2H14A2 2 0 0 1 16 4V6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polyline
|
||||||
|
points="3,6 5,6 21,6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19 6V20A2 2 0 0 1 17 22H7A2 2 0 0 1 5 20V6M8 6V4A2 2 0 0 1 10 2H14A2 2 0 0 1 16 4V6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -365,7 +402,6 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Upload Drop Zone -->
|
<!-- Upload Drop Zone -->
|
||||||
<div
|
<div
|
||||||
|
|
@ -412,12 +448,53 @@
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Upload Prompt -->
|
<!-- Upload Prompt -->
|
||||||
<div class="upload-prompt">
|
<div class="upload-prompt">
|
||||||
<svg class="upload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M14 2H6A2 2 0 0 0 4 4V20A2 2 0 0 0 6 22H18A2 2 0 0 0 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
class="upload-icon"
|
||||||
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
width="48"
|
||||||
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
height="48"
|
||||||
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
viewBox="0 0 24 24"
|
||||||
<polyline points="10,9 9,9 8,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 2H6A2 2 0 0 0 4 4V20A2 2 0 0 0 6 22H18A2 2 0 0 0 20 20V8L14 2Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points="14,2 14,8 20,8"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="16"
|
||||||
|
y1="13"
|
||||||
|
x2="8"
|
||||||
|
y2="13"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="16"
|
||||||
|
y1="17"
|
||||||
|
x2="8"
|
||||||
|
y2="17"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points="10,9 9,9 8,9"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<p class="upload-main-text">{placeholder}</p>
|
<p class="upload-main-text">{placeholder}</p>
|
||||||
<p class="upload-sub-text">
|
<p class="upload-sub-text">
|
||||||
|
|
@ -432,14 +509,10 @@
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
{#if !hasValue && !isUploading}
|
{#if !hasValue && !isUploading}
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<Button variant="primary" onclick={handleBrowseClick}>
|
<Button variant="primary" onclick={handleBrowseClick}>Choose File</Button>
|
||||||
Choose File
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{#if showBrowseLibrary}
|
{#if showBrowseLibrary}
|
||||||
<Button variant="ghost" onclick={handleBrowseLibrary}>
|
<Button variant="ghost" onclick={handleBrowseLibrary}>Browse Library</Button>
|
||||||
Browse Library
|
|
||||||
</Button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,7 @@
|
||||||
onUpdate: (updatedMedia: Media) => void
|
onUpdate: (updatedMedia: Media) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { isOpen = $bindable(), media, onClose, onUpdate }: Props = $props()
|
||||||
isOpen = $bindable(),
|
|
||||||
media,
|
|
||||||
onClose,
|
|
||||||
onUpdate
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
let altText = $state('')
|
let altText = $state('')
|
||||||
|
|
@ -29,14 +24,16 @@
|
||||||
let successMessage = $state('')
|
let successMessage = $state('')
|
||||||
|
|
||||||
// Usage tracking state
|
// Usage tracking state
|
||||||
let usage = $state<Array<{
|
let usage = $state<
|
||||||
|
Array<{
|
||||||
contentType: string
|
contentType: string
|
||||||
contentId: number
|
contentId: number
|
||||||
contentTitle: string
|
contentTitle: string
|
||||||
fieldDisplayName: string
|
fieldDisplayName: string
|
||||||
contentUrl?: string
|
contentUrl?: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}>>([])
|
}>
|
||||||
|
>([])
|
||||||
let loadingUsage = $state(false)
|
let loadingUsage = $state(false)
|
||||||
|
|
||||||
// Initialize form when media changes
|
// Initialize form when media changes
|
||||||
|
|
@ -115,7 +112,6 @@
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleClose()
|
handleClose()
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Failed to update media. Please try again.'
|
error = 'Failed to update media. Please try again.'
|
||||||
console.error('Failed to update media:', err)
|
console.error('Failed to update media:', err)
|
||||||
|
|
@ -125,7 +121,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
if (!media || !confirm('Are you sure you want to delete this media file? This action cannot be undone.')) {
|
if (
|
||||||
|
!media ||
|
||||||
|
!confirm('Are you sure you want to delete this media file? This action cannot be undone.')
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +143,6 @@
|
||||||
// Close modal and let parent handle the deletion
|
// Close modal and let parent handle the deletion
|
||||||
handleClose()
|
handleClose()
|
||||||
// Note: Parent component should refresh the media list
|
// Note: Parent component should refresh the media list
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Failed to delete media. Please try again.'
|
error = 'Failed to delete media. Please try again.'
|
||||||
console.error('Failed to delete media:', err)
|
console.error('Failed to delete media:', err)
|
||||||
|
|
@ -155,12 +153,15 @@
|
||||||
|
|
||||||
function copyUrl() {
|
function copyUrl() {
|
||||||
if (media?.url) {
|
if (media?.url) {
|
||||||
navigator.clipboard.writeText(media.url).then(() => {
|
navigator.clipboard
|
||||||
|
.writeText(media.url)
|
||||||
|
.then(() => {
|
||||||
successMessage = 'URL copied to clipboard!'
|
successMessage = 'URL copied to clipboard!'
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
successMessage = ''
|
successMessage = ''
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
error = 'Failed to copy URL'
|
error = 'Failed to copy URL'
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
error = ''
|
error = ''
|
||||||
|
|
@ -187,7 +188,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if media}
|
{#if media}
|
||||||
<Modal bind:isOpen size="large" closeOnBackdrop={!isSaving} closeOnEscape={!isSaving} on:close={handleClose}>
|
<Modal
|
||||||
|
bind:isOpen
|
||||||
|
size="large"
|
||||||
|
closeOnBackdrop={!isSaving}
|
||||||
|
closeOnEscape={!isSaving}
|
||||||
|
on:close={handleClose}
|
||||||
|
>
|
||||||
<div class="media-details-modal">
|
<div class="media-details-modal">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|
@ -197,8 +204,20 @@
|
||||||
</div>
|
</div>
|
||||||
{#if !isSaving}
|
{#if !isSaving}
|
||||||
<Button variant="ghost" onclick={handleClose} iconOnly aria-label="Close modal">
|
<Button variant="ghost" onclick={handleClose} iconOnly aria-label="Close modal">
|
||||||
<svg slot="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
slot="icon"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 6L18 18M6 18L18 6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -213,9 +232,27 @@
|
||||||
<SmartImage {media} alt={media.altText || media.filename} />
|
<SmartImage {media} alt={media.altText || media.filename} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="file-placeholder">
|
<div class="file-placeholder">
|
||||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M14 2H6A2 2 0 0 0 4 4V20A2 2 0 0 0 6 22H18A2 2 0 0 0 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
width="64"
|
||||||
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
height="64"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 2H6A2 2 0 0 0 4 4V20A2 2 0 0 0 6 22H18A2 2 0 0 0 20 20V8L14 2Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points="14,2 14,8 20,8"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="file-type">{getFileType(media.mimeType)}</span>
|
<span class="file-type">{getFileType(media.mimeType)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -246,9 +283,7 @@
|
||||||
<span class="label">URL:</span>
|
<span class="label">URL:</span>
|
||||||
<div class="url-section">
|
<div class="url-section">
|
||||||
<span class="url-text">{media.url}</span>
|
<span class="url-text">{media.url}</span>
|
||||||
<Button variant="ghost" buttonSize="small" onclick={copyUrl}>
|
<Button variant="ghost" buttonSize="small" onclick={copyUrl}>Copy</Button>
|
||||||
Copy
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -291,7 +326,8 @@
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
<div class="toggle-content">
|
<div class="toggle-content">
|
||||||
<span class="toggle-title">Photography</span>
|
<span class="toggle-title">Photography</span>
|
||||||
<span class="toggle-description">Show this media in the photography experience</span>
|
<span class="toggle-description">Show this media in the photography experience</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -311,7 +347,12 @@
|
||||||
<div class="usage-content">
|
<div class="usage-content">
|
||||||
<div class="usage-header">
|
<div class="usage-header">
|
||||||
{#if usageItem.contentUrl}
|
{#if usageItem.contentUrl}
|
||||||
<a href={usageItem.contentUrl} class="usage-title" target="_blank" rel="noopener">
|
<a
|
||||||
|
href={usageItem.contentUrl}
|
||||||
|
class="usage-title"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
{usageItem.contentTitle}
|
{usageItem.contentTitle}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -321,7 +362,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="usage-details">
|
<div class="usage-details">
|
||||||
<span class="usage-field">{usageItem.fieldDisplayName}</span>
|
<span class="usage-field">{usageItem.fieldDisplayName}</span>
|
||||||
<span class="usage-date">Added {new Date(usageItem.createdAt).toLocaleDateString()}</span>
|
<span class="usage-date"
|
||||||
|
>Added {new Date(usageItem.createdAt).toLocaleDateString()}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -337,12 +380,7 @@
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="footer-left">
|
<div class="footer-left">
|
||||||
<Button
|
<Button variant="ghost" onclick={handleDelete} disabled={isSaving} class="delete-button">
|
||||||
variant="ghost"
|
|
||||||
onclick={handleDelete}
|
|
||||||
disabled={isSaving}
|
|
||||||
class="delete-button"
|
|
||||||
>
|
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -355,9 +393,7 @@
|
||||||
<span class="success-text">{successMessage}</span>
|
<span class="success-text">{successMessage}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Button variant="ghost" onclick={handleClose} disabled={isSaving}>
|
<Button variant="ghost" onclick={handleClose} disabled={isSaving}>Cancel</Button>
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" onclick={handleSave} disabled={isSaving}>
|
<Button variant="primary" onclick={handleSave} disabled={isSaving}>
|
||||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -711,8 +747,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive adjustments
|
// Responsive adjustments
|
||||||
|
|
|
||||||
|
|
@ -75,17 +75,17 @@
|
||||||
: mode === 'single' && value && !Array.isArray(value)
|
: mode === 'single' && value && !Array.isArray(value)
|
||||||
? [value.id]
|
? [value.id]
|
||||||
: mode === 'multiple' && Array.isArray(value)
|
: mode === 'multiple' && Array.isArray(value)
|
||||||
? value.map(item => item.id)
|
? value.map((item) => item.id)
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
|
|
||||||
const modalTitle = $derived(
|
const modalTitle = $derived(
|
||||||
mode === 'single' ? `Select ${fileType === 'image' ? 'Image' : 'Media'}` : `Select ${fileType === 'image' ? 'Images' : 'Media'}`
|
mode === 'single'
|
||||||
|
? `Select ${fileType === 'image' ? 'Image' : 'Media'}`
|
||||||
|
: `Select ${fileType === 'image' ? 'Images' : 'Media'}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const confirmText = $derived(
|
const confirmText = $derived(mode === 'single' ? 'Select' : 'Select Files')
|
||||||
mode === 'single' ? 'Select' : 'Select Files'
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="media-input">
|
<div class="media-input">
|
||||||
|
|
@ -106,10 +106,29 @@
|
||||||
<img src={value.thumbnailUrl} alt={value.filename} />
|
<img src={value.thumbnailUrl} alt={value.filename} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="media-placeholder">
|
<div class="media-placeholder">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="2"/>
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="5"
|
||||||
|
width="18"
|
||||||
|
height="14"
|
||||||
|
rx="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
||||||
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="2" fill="none"/>
|
<path
|
||||||
|
d="M3 16l5-5 3 3 4-4 4 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -133,10 +152,29 @@
|
||||||
<img src={item.thumbnailUrl} alt={item.filename} />
|
<img src={item.thumbnailUrl} alt={item.filename} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="media-placeholder">
|
<div class="media-placeholder">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="2"/>
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="5"
|
||||||
|
width="18"
|
||||||
|
height="14"
|
||||||
|
rx="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
||||||
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="2" fill="none"/>
|
<path
|
||||||
|
d="M3 16l5-5 3 3 4-4 4 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -168,9 +206,7 @@
|
||||||
class:placeholder={!hasValue}
|
class:placeholder={!hasValue}
|
||||||
/>
|
/>
|
||||||
<div class="input-actions">
|
<div class="input-actions">
|
||||||
<Button variant="ghost" onclick={openModal}>
|
<Button variant="ghost" onclick={openModal}>Browse</Button>
|
||||||
Browse
|
|
||||||
</Button>
|
|
||||||
{#if hasValue}
|
{#if hasValue}
|
||||||
<Button variant="ghost" onclick={handleClear} aria-label="Clear selection">
|
<Button variant="ghost" onclick={handleClear} aria-label="Clear selection">
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -205,7 +241,7 @@
|
||||||
{fileType}
|
{fileType}
|
||||||
{selectedIds}
|
{selectedIds}
|
||||||
title={modalTitle}
|
title={modalTitle}
|
||||||
confirmText={confirmText}
|
{confirmText}
|
||||||
onselect={handleMediaSelect}
|
onselect={handleMediaSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,9 @@
|
||||||
const selectionCount = $derived(selectedMedia.length)
|
const selectionCount = $derived(selectedMedia.length)
|
||||||
const footerText = $derived(
|
const footerText = $derived(
|
||||||
mode === 'single'
|
mode === 'single'
|
||||||
? canConfirm ? '1 item selected' : 'No item selected'
|
? canConfirm
|
||||||
|
? '1 item selected'
|
||||||
|
: 'No item selected'
|
||||||
: `${selectionCount} item${selectionCount !== 1 ? 's' : ''} selected`
|
: `${selectionCount} item${selectionCount !== 1 ? 's' : ''} selected`
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -117,14 +119,8 @@
|
||||||
<span class="selection-count">{footerText}</span>
|
<span class="selection-count">{footerText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
<Button variant="ghost" onclick={handleCancel} disabled={isLoading}>
|
<Button variant="ghost" onclick={handleCancel} disabled={isLoading}>Cancel</Button>
|
||||||
Cancel
|
<Button variant="primary" onclick={handleConfirm} disabled={!canConfirm || isLoading}>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onclick={handleConfirm}
|
|
||||||
disabled={!canConfirm || isLoading}
|
|
||||||
>
|
|
||||||
{confirmText}
|
{confirmText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,7 @@
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { mode, fileType = 'all', selectedIds = [], loading = $bindable(false) }: Props = $props()
|
||||||
mode,
|
|
||||||
fileType = 'all',
|
|
||||||
selectedIds = [],
|
|
||||||
loading = $bindable(false)
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
select: Media[]
|
select: Media[]
|
||||||
|
|
@ -37,7 +32,7 @@
|
||||||
// Initialize selected media from IDs
|
// Initialize selected media from IDs
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (selectedIds.length > 0 && media.length > 0) {
|
if (selectedIds.length > 0 && media.length > 0) {
|
||||||
selectedMedia = media.filter(item => selectedIds.includes(item.id))
|
selectedMedia = media.filter((item) => selectedIds.includes(item.id))
|
||||||
dispatch('select', selectedMedia)
|
dispatch('select', selectedMedia)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -112,7 +107,6 @@
|
||||||
currentPage = page
|
currentPage = page
|
||||||
totalPages = data.pagination.totalPages
|
totalPages = data.pagination.totalPages
|
||||||
total = data.pagination.total
|
total = data.pagination.total
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading media:', error)
|
console.error('Error loading media:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -125,10 +119,10 @@
|
||||||
selectedMedia = [item]
|
selectedMedia = [item]
|
||||||
dispatch('select', selectedMedia)
|
dispatch('select', selectedMedia)
|
||||||
} else {
|
} else {
|
||||||
const isSelected = selectedMedia.some(m => m.id === item.id)
|
const isSelected = selectedMedia.some((m) => m.id === item.id)
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selectedMedia = selectedMedia.filter(m => m.id !== item.id)
|
selectedMedia = selectedMedia.filter((m) => m.id !== item.id)
|
||||||
} else {
|
} else {
|
||||||
selectedMedia = [...selectedMedia, item]
|
selectedMedia = [...selectedMedia, item]
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +155,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSelected(item: Media): boolean {
|
function isSelected(item: Media): boolean {
|
||||||
return selectedMedia.some(m => m.id === item.id)
|
return selectedMedia.some((m) => m.id === item.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
|
|
@ -174,11 +168,7 @@
|
||||||
<!-- Search and Filter Controls -->
|
<!-- Search and Filter Controls -->
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="search-filters">
|
<div class="search-filters">
|
||||||
<Input
|
<Input type="search" placeholder="Search media files..." bind:value={searchQuery} />
|
||||||
type="search"
|
|
||||||
placeholder="Search media files..."
|
|
||||||
bind:value={searchQuery}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<select bind:value={filterType} class="filter-select">
|
<select bind:value={filterType} class="filter-select">
|
||||||
<option value="all">All Files</option>
|
<option value="all">All Files</option>
|
||||||
|
|
@ -216,7 +206,13 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if media.length === 0}
|
{:else if media.length === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="2" />
|
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="2" />
|
||||||
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
||||||
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="2" fill="none" />
|
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="2" fill="none" />
|
||||||
|
|
@ -237,16 +233,35 @@
|
||||||
<div class="media-thumbnail">
|
<div class="media-thumbnail">
|
||||||
{#if item.mimeType?.startsWith('image/')}
|
{#if item.mimeType?.startsWith('image/')}
|
||||||
<img
|
<img
|
||||||
src={item.mimeType === 'image/svg+xml' ? item.url : (item.thumbnailUrl || item.url)}
|
src={item.mimeType === 'image/svg+xml' ? item.url : item.thumbnailUrl || item.url}
|
||||||
alt={item.filename}
|
alt={item.filename}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="media-placeholder">
|
<div class="media-placeholder">
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="2"/>
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="5"
|
||||||
|
width="18"
|
||||||
|
height="14"
|
||||||
|
rx="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
<circle cx="8.5" cy="8.5" r=".5" fill="currentColor" />
|
||||||
<path d="M3 16l5-5 3 3 4-4 4 4" stroke="currentColor" stroke-width="2" fill="none"/>
|
<path
|
||||||
|
d="M3 16l5-5 3 3 4-4 4 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -254,19 +269,27 @@
|
||||||
<!-- Selection Indicator -->
|
<!-- Selection Indicator -->
|
||||||
{#if mode === 'multiple'}
|
{#if mode === 'multiple'}
|
||||||
<div class="selection-checkbox">
|
<div class="selection-checkbox">
|
||||||
<input
|
<input type="checkbox" checked={isSelected(item)} readonly />
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected(item)}
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Selected Overlay -->
|
<!-- Selected Overlay -->
|
||||||
{#if isSelected(item)}
|
{#if isSelected(item)}
|
||||||
<div class="selected-overlay">
|
<div class="selected-overlay">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9 12l2 2 4-4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -280,8 +303,17 @@
|
||||||
<div class="media-indicators">
|
<div class="media-indicators">
|
||||||
{#if item.isPhotography}
|
{#if item.isPhotography}
|
||||||
<span class="indicator-pill photography" title="Photography">
|
<span class="indicator-pill photography" title="Photography">
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<polygon points="12,2 15.09,8.26 22,9 17,14.74 18.18,21.02 12,17.77 5.82,21.02 7,14.74 2,9 8.91,8.26" fill="currentColor"/>
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="12,2 15.09,8.26 22,9 17,14.74 18.18,21.02 12,17.77 5.82,21.02 7,14.74 2,9 8.91,8.26"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Photo
|
Photo
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -291,9 +323,7 @@
|
||||||
Alt
|
Alt
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="indicator-pill no-alt-text" title="No alt text">
|
<span class="indicator-pill no-alt-text" title="No alt text"> No Alt </span>
|
||||||
No Alt
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="media-meta">
|
<div class="media-meta">
|
||||||
|
|
@ -310,12 +340,7 @@
|
||||||
<!-- Load More Button -->
|
<!-- Load More Button -->
|
||||||
{#if hasMore}
|
{#if hasMore}
|
||||||
<div class="load-more-container">
|
<div class="load-more-container">
|
||||||
<Button
|
<Button variant="ghost" onclick={loadMore} disabled={loading} class="load-more-button">
|
||||||
variant="ghost"
|
|
||||||
onclick={loadMore}
|
|
||||||
disabled={loading}
|
|
||||||
class="load-more-button"
|
|
||||||
>
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<LoadingSpinner buttonSize="small" />
|
<LoadingSpinner buttonSize="small" />
|
||||||
Loading...
|
Loading...
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,12 @@
|
||||||
<div class="file-list-header">
|
<div class="file-list-header">
|
||||||
<h3>Files to Upload</h3>
|
<h3>Files to Upload</h3>
|
||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
<Button variant="secondary" buttonSize="small" onclick={clearAll} disabled={isUploading}>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
buttonSize="small"
|
||||||
|
onclick={clearAll}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
Clear All
|
Clear All
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -132,11 +132,7 @@
|
||||||
<div class="popover-content">
|
<div class="popover-content">
|
||||||
<h3>Post Settings</h3>
|
<h3>Post Settings</h3>
|
||||||
|
|
||||||
<Input
|
<Input label="Slug" bind:value={slug} placeholder="post-slug" />
|
||||||
label="Slug"
|
|
||||||
bind:value={slug}
|
|
||||||
placeholder="post-slug"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if postType === 'essay'}
|
{#if postType === 'essay'}
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,12 @@
|
||||||
status,
|
status,
|
||||||
content: editorContent,
|
content: editorContent,
|
||||||
featuredImage: featuredImage.url,
|
featuredImage: featuredImage.url,
|
||||||
tags: tags ? tags.split(',').map(tag => tag.trim()).filter(Boolean) : [],
|
tags: tags
|
||||||
|
? tags
|
||||||
|
.split(',')
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [],
|
||||||
excerpt: generateExcerpt(editorContent)
|
excerpt: generateExcerpt(editorContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +152,6 @@
|
||||||
} else {
|
} else {
|
||||||
goto('/admin/posts')
|
goto('/admin/posts')
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = `Failed to ${mode === 'edit' ? 'update' : 'create'} photo post`
|
error = `Failed to ${mode === 'edit' ? 'update' : 'create'} photo post`
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
@ -193,13 +197,19 @@
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
{#if !isSaving}
|
{#if !isSaving}
|
||||||
<Button variant="ghost" onclick={() => goto('/admin/posts')}>
|
<Button variant="ghost" onclick={() => goto('/admin/posts')}>Cancel</Button>
|
||||||
Cancel
|
<Button
|
||||||
</Button>
|
variant="secondary"
|
||||||
<Button variant="secondary" onclick={handleDraft} disabled={!featuredImage || !title.trim()}>
|
onclick={handleDraft}
|
||||||
|
disabled={!featuredImage || !title.trim()}
|
||||||
|
>
|
||||||
Save Draft
|
Save Draft
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onclick={handlePublish} disabled={!featuredImage || !title.trim()}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onclick={handlePublish}
|
||||||
|
disabled={!featuredImage || !title.trim()}
|
||||||
|
>
|
||||||
{isSaving ? 'Publishing...' : 'Publish'}
|
{isSaving ? 'Publishing...' : 'Publish'}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,17 @@
|
||||||
label: 'Slug',
|
label: 'Slug',
|
||||||
placeholder: 'post-slug'
|
placeholder: 'post-slug'
|
||||||
},
|
},
|
||||||
...(postType === 'essay' ? [{
|
...(postType === 'essay'
|
||||||
|
? [
|
||||||
|
{
|
||||||
type: 'textarea' as const,
|
type: 'textarea' as const,
|
||||||
key: 'excerpt',
|
key: 'excerpt',
|
||||||
label: 'Excerpt',
|
label: 'Excerpt',
|
||||||
rows: 3,
|
rows: 3,
|
||||||
placeholder: 'Brief description...'
|
placeholder: 'Brief description...'
|
||||||
}] : []),
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
type: 'tags',
|
type: 'tags',
|
||||||
key: 'tags',
|
key: 'tags',
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,6 @@
|
||||||
formData.caseStudyContent = content
|
formData.caseStudyContent = content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
// Check if we're on the case study tab and should save editor content
|
// Check if we're on the case study tab and should save editor content
|
||||||
if (activeTab === 'case-study' && editorRef) {
|
if (activeTab === 'case-study' && editorRef) {
|
||||||
|
|
@ -134,7 +133,6 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
title: formData.title,
|
title: formData.title,
|
||||||
subtitle: formData.subtitle,
|
subtitle: formData.subtitle,
|
||||||
|
|
@ -236,7 +234,11 @@
|
||||||
dropdownActions={[
|
dropdownActions={[
|
||||||
{ label: 'Save as Draft', status: 'draft' },
|
{ label: 'Save as Draft', status: 'draft' },
|
||||||
{ label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' },
|
{ label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' },
|
||||||
{ label: 'Password Protected', status: 'password-protected', show: formData.status !== 'password-protected' }
|
{
|
||||||
|
label: 'Password Protected',
|
||||||
|
status: 'password-protected',
|
||||||
|
show: formData.status !== 'password-protected'
|
||||||
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -351,8 +353,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.tab-panels {
|
.tab-panels {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@
|
||||||
document.addEventListener('closeDropdowns', handleCloseDropdowns)
|
document.addEventListener('closeDropdowns', handleCloseDropdowns)
|
||||||
return () => document.removeEventListener('closeDropdowns', handleCloseDropdowns)
|
return () => document.removeEventListener('closeDropdowns', handleCloseDropdowns)
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -115,11 +114,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dropdown-container">
|
<div class="dropdown-container">
|
||||||
<button
|
<button class="action-button" onclick={handleToggleDropdown} aria-label="Project actions">
|
||||||
class="action-button"
|
|
||||||
onclick={handleToggleDropdown}
|
|
||||||
aria-label="Project actions"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
|
|
@ -201,7 +196,6 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dropdown-container {
|
.dropdown-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
@ -265,5 +259,4 @@
|
||||||
background-color: $grey-90;
|
background-color: $grey-90;
|
||||||
margin: $unit-half 0;
|
margin: $unit-half 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -44,25 +44,15 @@
|
||||||
onPublish={handlePublish}
|
onPublish={handlePublish}
|
||||||
onSaveDraft={handleSaveDraft}
|
onSaveDraft={handleSaveDraft}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isLoading={isLoading}
|
{isLoading}
|
||||||
/>
|
/>
|
||||||
{:else if status === 'published'}
|
{:else if status === 'published'}
|
||||||
<Button
|
<Button variant="primary" buttonSize="large" onclick={handleSave} disabled={isDisabled}>
|
||||||
variant="primary"
|
|
||||||
buttonSize="large"
|
|
||||||
onclick={handleSave}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Saving...' : 'Save'}
|
{isLoading ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- For other statuses like 'list-only', 'password-protected', etc. -->
|
<!-- For other statuses like 'list-only', 'password-protected', etc. -->
|
||||||
<Button
|
<Button variant="primary" buttonSize="large" onclick={handleSave} disabled={isDisabled}>
|
||||||
variant="primary"
|
|
||||||
buttonSize="large"
|
|
||||||
onclick={handleSave}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Saving...' : 'Save'}
|
{isLoading ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -229,7 +229,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.composer-container {
|
.composer-container {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const availableActions = $derived(
|
const availableActions = $derived(
|
||||||
dropdownActions.filter(action =>
|
dropdownActions.filter((action) => action.show !== false && action.status !== currentStatus)
|
||||||
action.show !== false && action.status !== currentStatus
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@
|
||||||
|
|
||||||
import { Extension, type Range, type Dispatch } from '@tiptap/core'
|
import { Extension, type Range, type Dispatch } from '@tiptap/core'
|
||||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||||
import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/pm/state'
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
import { Node as PMNode } from '@tiptap/pm/model'
|
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,16 @@ declare module '@tiptap/core' {
|
||||||
/**
|
/**
|
||||||
* Insert a gallery
|
* Insert a gallery
|
||||||
*/
|
*/
|
||||||
setGallery: (options: { images: Array<{ id: number; url: string; alt?: string; title?: string }> }) => ReturnType
|
setGallery: (options: {
|
||||||
|
images: Array<{ id: number; url: string; alt?: string; title?: string }>
|
||||||
|
}) => ReturnType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GalleryExtended = (component: Component<NodeViewProps>): Node<GalleryOptions, unknown> => {
|
export const GalleryExtended = (
|
||||||
|
component: Component<NodeViewProps>
|
||||||
|
): Node<GalleryOptions, unknown> => {
|
||||||
return Node.create<GalleryOptions>({
|
return Node.create<GalleryOptions>({
|
||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
|
|
||||||
|
|
@ -46,15 +50,16 @@ export const GalleryExtended = (component: Component<NodeViewProps>): Node<Galle
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [{ tag: `div[data-type="${this.name}"]` }]
|
||||||
{ tag: `div[data-type="${this.name}"]` }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
return [
|
||||||
|
'div',
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
'data-type': this.name
|
'data-type': this.name
|
||||||
})]
|
})
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
group: 'block',
|
group: 'block',
|
||||||
|
|
@ -67,7 +72,9 @@ export const GalleryExtended = (component: Component<NodeViewProps>): Node<Galle
|
||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
return {
|
return {
|
||||||
setGallery: (options) => ({ commands }) => {
|
setGallery:
|
||||||
|
(options) =>
|
||||||
|
({ commands }) => {
|
||||||
return commands.insertContent({
|
return commands.insertContent({
|
||||||
type: this.name,
|
type: this.name,
|
||||||
attrs: {
|
attrs: {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { Editor, findParentNode } from '@tiptap/core'
|
import { Editor, findParentNode } from '@tiptap/core'
|
||||||
import { EditorState, Selection, Transaction } from '@tiptap/pm/state'
|
|
||||||
import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'
|
import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'
|
||||||
import { Node, ResolvedPos } from '@tiptap/pm/model'
|
|
||||||
import type { EditorView } from '@tiptap/pm/view'
|
import type { EditorView } from '@tiptap/pm/view'
|
||||||
import Table from './table.js'
|
import Table from './table.js'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
function handleMediaSelect(media: Media | Media[]) {
|
function handleMediaSelect(media: Media | Media[]) {
|
||||||
const mediaArray = Array.isArray(media) ? media : [media]
|
const mediaArray = Array.isArray(media) ? media : [media]
|
||||||
const newImages = mediaArray.map(m => ({
|
const newImages = mediaArray.map((m) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
url: m.url,
|
url: m.url,
|
||||||
alt: m.altText || '',
|
alt: m.altText || '',
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
// Add to existing images
|
// Add to existing images
|
||||||
const existingImages = node.attrs.images || []
|
const existingImages = node.attrs.images || []
|
||||||
const currentIds = existingImages.map((img: any) => img.id)
|
const currentIds = existingImages.map((img: any) => img.id)
|
||||||
const uniqueNewImages = newImages.filter(img => !currentIds.includes(img.id))
|
const uniqueNewImages = newImages.filter((img) => !currentIds.includes(img.id))
|
||||||
updateAttributes({ images: [...existingImages, ...uniqueNewImages] })
|
updateAttributes({ images: [...existingImages, ...uniqueNewImages] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,12 +87,7 @@
|
||||||
<div class={`edra-gallery-grid ${layout === 'masonry' ? 'masonry' : 'grid'}`}>
|
<div class={`edra-gallery-grid ${layout === 'masonry' ? 'masonry' : 'grid'}`}>
|
||||||
{#each images as image}
|
{#each images as image}
|
||||||
<div class="edra-gallery-item">
|
<div class="edra-gallery-item">
|
||||||
<img
|
<img src={image.url} alt={image.alt} title={image.title} loading="lazy" />
|
||||||
src={image.url}
|
|
||||||
alt={image.alt}
|
|
||||||
title={image.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
{#if editor?.isEditable}
|
{#if editor?.isEditable}
|
||||||
<button
|
<button
|
||||||
class="edra-gallery-item-remove"
|
class="edra-gallery-item-remove"
|
||||||
|
|
@ -141,18 +136,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edra-gallery-toolbar-section">
|
<div class="edra-gallery-toolbar-section">
|
||||||
<button
|
<button class="edra-toolbar-button" onclick={handleAddImages} title="Add Images">
|
||||||
class="edra-toolbar-button"
|
|
||||||
onclick={handleAddImages}
|
|
||||||
title="Add Images"
|
|
||||||
>
|
|
||||||
<Plus />
|
<Plus />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="edra-toolbar-button" onclick={handleEditGallery} title="Edit Gallery">
|
||||||
class="edra-toolbar-button"
|
|
||||||
onclick={handleEditGallery}
|
|
||||||
title="Edit Gallery"
|
|
||||||
>
|
|
||||||
<Edit />
|
<Edit />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
function handleMediaSelect(media: Media | Media[]) {
|
function handleMediaSelect(media: Media | Media[]) {
|
||||||
const mediaArray = Array.isArray(media) ? media : [media]
|
const mediaArray = Array.isArray(media) ? media : [media]
|
||||||
if (mediaArray.length > 0) {
|
if (mediaArray.length > 0) {
|
||||||
const galleryImages = mediaArray.map(m => ({
|
const galleryImages = mediaArray.map((m) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
url: m.url,
|
url: m.url,
|
||||||
alt: m.altText || '',
|
alt: m.altText || '',
|
||||||
|
|
@ -225,8 +225,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.edra-gallery-placeholder-icon) {
|
:global(.edra-gallery-placeholder-icon) {
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,15 @@
|
||||||
function handleMediaSelect(media: Media | Media[]) {
|
function handleMediaSelect(media: Media | Media[]) {
|
||||||
const selectedMedia = Array.isArray(media) ? media[0] : media
|
const selectedMedia = Array.isArray(media) ? media[0] : media
|
||||||
if (selectedMedia) {
|
if (selectedMedia) {
|
||||||
editor.chain().focus().setImage({
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.setImage({
|
||||||
src: selectedMedia.url,
|
src: selectedMedia.url,
|
||||||
alt: selectedMedia.altText || '',
|
alt: selectedMedia.altText || '',
|
||||||
title: selectedMedia.description || ''
|
title: selectedMedia.description || ''
|
||||||
}).run()
|
})
|
||||||
|
.run()
|
||||||
}
|
}
|
||||||
isMediaLibraryOpen = false
|
isMediaLibraryOpen = false
|
||||||
}
|
}
|
||||||
|
|
@ -74,11 +78,15 @@
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const media = await response.json()
|
const media = await response.json()
|
||||||
editor.chain().focus().setImage({
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.setImage({
|
||||||
src: media.url,
|
src: media.url,
|
||||||
alt: media.altText || '',
|
alt: media.altText || '',
|
||||||
title: media.description || ''
|
title: media.description || ''
|
||||||
}).run()
|
})
|
||||||
|
.run()
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to upload image:', response.status)
|
console.error('Failed to upload image:', response.status)
|
||||||
alert('Failed to upload image. Please try again.')
|
alert('Failed to upload image. Please try again.')
|
||||||
|
|
@ -219,8 +227,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.edra-media-placeholder-icon) {
|
:global(.edra-media-placeholder-icon) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import type { Content, Editor } from '@tiptap/core'
|
import type { Content, Editor } from '@tiptap/core'
|
||||||
import { Node } from '@tiptap/pm/model'
|
|
||||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||||
import type { EditorState, Transaction } from '@tiptap/pm/state'
|
import type { EditorState, Transaction } from '@tiptap/pm/state'
|
||||||
import type { EditorView } from '@tiptap/pm/view'
|
import type { EditorView } from '@tiptap/pm/view'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const projectSchema = z.object({
|
export const projectSchema = z
|
||||||
|
.object({
|
||||||
title: z.string().min(1, 'Title is required'),
|
title: z.string().min(1, 'Title is required'),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
year: z
|
year: z
|
||||||
|
|
@ -21,7 +22,8 @@ export const projectSchema = z.object({
|
||||||
.or(z.literal('')),
|
.or(z.literal('')),
|
||||||
status: z.enum(['draft', 'published', 'list-only', 'password-protected']),
|
status: z.enum(['draft', 'published', 'list-only', 'password-protected']),
|
||||||
password: z.string().optional()
|
password: z.string().optional()
|
||||||
}).refine(
|
})
|
||||||
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.status === 'password-protected') {
|
if (data.status === 'password-protected') {
|
||||||
return data.password && data.password.trim().length > 0
|
return data.password && data.password.trim().length > 0
|
||||||
|
|
|
||||||
|
|
@ -216,11 +216,7 @@ export function getResponsiveUrls(publicId: string): Record<string, string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smart image size selection based on container width
|
// Smart image size selection based on container width
|
||||||
export function getSmartImageUrl(
|
export function getSmartImageUrl(publicId: string, containerWidth: number, retina = true): string {
|
||||||
publicId: string,
|
|
||||||
containerWidth: number,
|
|
||||||
retina = true
|
|
||||||
): string {
|
|
||||||
// Account for retina displays
|
// Account for retina displays
|
||||||
const targetWidth = retina ? containerWidth * 2 : containerWidth
|
const targetWidth = retina ? containerWidth * 2 : containerWidth
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export async function trackMediaUsage(references: MediaUsageReference[]) {
|
||||||
if (references.length === 0) return
|
if (references.length === 0) return
|
||||||
|
|
||||||
// Use upsert to handle duplicates gracefully
|
// Use upsert to handle duplicates gracefully
|
||||||
const operations = references.map(ref =>
|
const operations = references.map((ref) =>
|
||||||
prisma.mediaUsage.upsert({
|
prisma.mediaUsage.upsert({
|
||||||
where: {
|
where: {
|
||||||
mediaId_contentType_contentId_fieldName: {
|
mediaId_contentType_contentId_fieldName: {
|
||||||
|
|
@ -84,7 +84,7 @@ export async function updateMediaUsage(
|
||||||
// Add new usage references
|
// Add new usage references
|
||||||
if (mediaIds.length > 0) {
|
if (mediaIds.length > 0) {
|
||||||
await tx.mediaUsage.createMany({
|
await tx.mediaUsage.createMany({
|
||||||
data: mediaIds.map(mediaId => ({
|
data: mediaIds.map((mediaId) => ({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType,
|
contentType,
|
||||||
contentId,
|
contentId,
|
||||||
|
|
@ -170,13 +170,13 @@ export async function getMediaUsage(mediaId: number): Promise<MediaUsageDisplay[
|
||||||
*/
|
*/
|
||||||
function getFieldDisplayName(fieldName: string): string {
|
function getFieldDisplayName(fieldName: string): string {
|
||||||
const displayNames: Record<string, string> = {
|
const displayNames: Record<string, string> = {
|
||||||
'featuredImage': 'Featured Image',
|
featuredImage: 'Featured Image',
|
||||||
'logoUrl': 'Logo',
|
logoUrl: 'Logo',
|
||||||
'gallery': 'Gallery',
|
gallery: 'Gallery',
|
||||||
'content': 'Content',
|
content: 'Content',
|
||||||
'coverPhotoId': 'Cover Photo',
|
coverPhotoId: 'Cover Photo',
|
||||||
'photoId': 'Photo',
|
photoId: 'Photo',
|
||||||
'attachments': 'Attachments'
|
attachments: 'Attachments'
|
||||||
}
|
}
|
||||||
|
|
||||||
return displayNames[fieldName] || fieldName
|
return displayNames[fieldName] || fieldName
|
||||||
|
|
@ -195,8 +195,8 @@ export function extractMediaIds(data: any, fieldName: string): number[] {
|
||||||
// Gallery/attachments are arrays of media objects with id property
|
// Gallery/attachments are arrays of media objects with id property
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value
|
return value
|
||||||
.map(item => typeof item === 'object' ? item.id : parseInt(item))
|
.map((item) => (typeof item === 'object' ? item.id : parseInt(item)))
|
||||||
.filter(id => !isNaN(id))
|
.filter((id) => !isNaN(id))
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,9 @@ async function fetchRecentPSNGames(fetch: typeof window.fetch): Promise<Serializ
|
||||||
async function fetchProjects(
|
async function fetchProjects(
|
||||||
fetch: typeof window.fetch
|
fetch: typeof window.fetch
|
||||||
): Promise<{ projects: Project[]; pagination: any }> {
|
): Promise<{ projects: Project[]; pagination: any }> {
|
||||||
const response = await fetch('/api/projects?projectType=work&includeListOnly=true&includePasswordProtected=true')
|
const response = await fetch(
|
||||||
|
'/api/projects?projectType=work&includeListOnly=true&includePasswordProtected=true'
|
||||||
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch projects: ${response.status}`)
|
throw new Error(`Failed to fetch projects: ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,8 @@
|
||||||
{#if photographyFilter === 'all'}
|
{#if photographyFilter === 'all'}
|
||||||
No albums found. Create your first album!
|
No albums found. Create your first album!
|
||||||
{:else}
|
{:else}
|
||||||
No albums found matching the current filters. Try adjusting your filters or create a new album.
|
No albums found matching the current filters. Try adjusting your filters or create a new
|
||||||
|
album.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -577,7 +577,7 @@
|
||||||
triggerElement={metadataButtonElement}
|
triggerElement={metadataButtonElement}
|
||||||
onUpdate={handleMetadataUpdate}
|
onUpdate={handleMetadataUpdate}
|
||||||
onDelete={handleMetadataDelete}
|
onDelete={handleMetadataDelete}
|
||||||
onClose={() => isMetadataOpen = false}
|
onClose={() => (isMetadataOpen = false)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@
|
||||||
triggerElement={metadataButtonElement}
|
triggerElement={metadataButtonElement}
|
||||||
onUpdate={handleMetadataUpdate}
|
onUpdate={handleMetadataUpdate}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
onClose={() => isMetadataOpen = false}
|
onClose={() => (isMetadataOpen = false)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,22 +30,42 @@
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<Button buttonSize="small" iconOnly>
|
<Button buttonSize="small" iconOnly>
|
||||||
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<path d="M8 4v8m4-4H4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path
|
||||||
|
d="M8 4v8m4-4H4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<Button buttonSize="medium" iconOnly>
|
<Button buttonSize="medium" iconOnly>
|
||||||
<svg slot="icon" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
<svg slot="icon" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
<path d="M9 5v8m4-4H5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path
|
||||||
|
d="M9 5v8m4-4H5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<Button buttonSize="large" iconOnly>
|
<Button buttonSize="large" iconOnly>
|
||||||
<svg slot="icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
<svg slot="icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||||
<path d="M10 6v8m4-4H6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path
|
||||||
|
d="M10 6v8m4-4H6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<Button buttonSize="icon" iconOnly variant="ghost">
|
<Button buttonSize="icon" iconOnly variant="ghost">
|
||||||
<svg slot="icon" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
<svg slot="icon" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
<path d="M6 6l6 6m0-6l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path
|
||||||
|
d="M6 6l6 6m0-6l-6 6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -56,14 +76,25 @@
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<Button>
|
<Button>
|
||||||
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<path d="M8 4v8m4-4H4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path
|
||||||
|
d="M8 4v8m4-4H4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Add Item
|
Add Item
|
||||||
</Button>
|
</Button>
|
||||||
<Button iconPosition="right" variant="secondary">
|
<Button iconPosition="right" variant="secondary">
|
||||||
Next
|
Next
|
||||||
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path
|
||||||
|
d="M6 4l4 4-4 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -130,11 +130,7 @@
|
||||||
<p>Multiple image management with drag-and-drop reordering.</p>
|
<p>Multiple image management with drag-and-drop reordering.</p>
|
||||||
|
|
||||||
<div class="form-column">
|
<div class="form-column">
|
||||||
<GalleryManager
|
<GalleryManager label="Image Gallery" bind:value={galleryImages} showFileInfo={false} />
|
||||||
label="Image Gallery"
|
|
||||||
bind:value={galleryImages}
|
|
||||||
showFileInfo={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GalleryManager
|
<GalleryManager
|
||||||
label="Project Gallery (Max 6 images)"
|
label="Project Gallery (Max 6 images)"
|
||||||
|
|
@ -149,12 +145,8 @@
|
||||||
<section class="test-section">
|
<section class="test-section">
|
||||||
<h2>Form Actions</h2>
|
<h2>Form Actions</h2>
|
||||||
<div class="actions-grid">
|
<div class="actions-grid">
|
||||||
<Button variant="primary" onclick={logAllValues}>
|
<Button variant="primary" onclick={logAllValues}>Log All Values</Button>
|
||||||
Log All Values
|
<Button variant="ghost" onclick={clearAllValues}>Clear All</Button>
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" onclick={clearAllValues}>
|
|
||||||
Clear All
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -169,7 +161,11 @@
|
||||||
|
|
||||||
<div class="value-item">
|
<div class="value-item">
|
||||||
<h4>Multiple Media ({multipleMedia.length}):</h4>
|
<h4>Multiple Media ({multipleMedia.length}):</h4>
|
||||||
<pre>{JSON.stringify(multipleMedia.map(m => m.filename), null, 2)}</pre>
|
<pre>{JSON.stringify(
|
||||||
|
multipleMedia.map((m) => m.filename),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="value-item">
|
<div class="value-item">
|
||||||
|
|
@ -179,12 +175,20 @@
|
||||||
|
|
||||||
<div class="value-item">
|
<div class="value-item">
|
||||||
<h4>Gallery Images ({galleryImages.length}):</h4>
|
<h4>Gallery Images ({galleryImages.length}):</h4>
|
||||||
<pre>{JSON.stringify(galleryImages.map(m => m.filename), null, 2)}</pre>
|
<pre>{JSON.stringify(
|
||||||
|
galleryImages.map((m) => m.filename),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="value-item">
|
<div class="value-item">
|
||||||
<h4>Project Gallery ({projectGallery.length}):</h4>
|
<h4>Project Gallery ({projectGallery.length}):</h4>
|
||||||
<pre>{JSON.stringify(projectGallery.map(m => m.filename), null, 2)}</pre>
|
<pre>{JSON.stringify(
|
||||||
|
projectGallery.map((m) => m.filename),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@
|
||||||
|
|
||||||
<AdminPage title="ImageUploader Test" subtitle="Test the new direct upload functionality">
|
<AdminPage title="ImageUploader Test" subtitle="Test the new direct upload functionality">
|
||||||
<div class="test-container">
|
<div class="test-container">
|
||||||
|
|
||||||
<!-- Basic Image Upload -->
|
<!-- Basic Image Upload -->
|
||||||
<section class="test-section">
|
<section class="test-section">
|
||||||
<h2>Basic Image Upload</h2>
|
<h2>Basic Image Upload</h2>
|
||||||
|
|
@ -95,9 +94,7 @@
|
||||||
<button type="button" class="btn btn-primary" onclick={logAllValues}>
|
<button type="button" class="btn btn-primary" onclick={logAllValues}>
|
||||||
Log All Values
|
Log All Values
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-ghost" onclick={clearAll}>
|
<button type="button" class="btn btn-ghost" onclick={clearAll}> Clear All </button>
|
||||||
Clear All
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -107,36 +104,53 @@
|
||||||
<div class="values-display">
|
<div class="values-display">
|
||||||
<div class="value-item">
|
<div class="value-item">
|
||||||
<h4>Single Image:</h4>
|
<h4>Single Image:</h4>
|
||||||
<pre>{JSON.stringify(singleImage ? {
|
<pre>{JSON.stringify(
|
||||||
|
singleImage
|
||||||
|
? {
|
||||||
id: singleImage.id,
|
id: singleImage.id,
|
||||||
filename: singleImage.filename,
|
filename: singleImage.filename,
|
||||||
altText: singleImage.altText,
|
altText: singleImage.altText,
|
||||||
description: singleImage.description
|
description: singleImage.description
|
||||||
} : null, null, 2)}</pre>
|
}
|
||||||
|
: null,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="value-item">
|
<div class="value-item">
|
||||||
<h4>Logo Image:</h4>
|
<h4>Logo Image:</h4>
|
||||||
<pre>{JSON.stringify(logoImage ? {
|
<pre>{JSON.stringify(
|
||||||
|
logoImage
|
||||||
|
? {
|
||||||
id: logoImage.id,
|
id: logoImage.id,
|
||||||
filename: logoImage.filename,
|
filename: logoImage.filename,
|
||||||
altText: logoImage.altText,
|
altText: logoImage.altText,
|
||||||
description: logoImage.description
|
description: logoImage.description
|
||||||
} : null, null, 2)}</pre>
|
}
|
||||||
|
: null,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="value-item">
|
<div class="value-item">
|
||||||
<h4>Banner Image:</h4>
|
<h4>Banner Image:</h4>
|
||||||
<pre>{JSON.stringify(bannerImage ? {
|
<pre>{JSON.stringify(
|
||||||
|
bannerImage
|
||||||
|
? {
|
||||||
id: bannerImage.id,
|
id: bannerImage.id,
|
||||||
filename: bannerImage.filename,
|
filename: bannerImage.filename,
|
||||||
altText: bannerImage.altText,
|
altText: bannerImage.altText,
|
||||||
description: bannerImage.description
|
description: bannerImage.description
|
||||||
} : null, null, 2)}</pre>
|
}
|
||||||
|
: null,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</AdminPage>
|
</AdminPage>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,7 @@
|
||||||
step={5}
|
step={5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input type="color" label="Color Input" bind:value={colorValue} />
|
||||||
type="color"
|
|
||||||
label="Color Input"
|
|
||||||
bind:value={colorValue}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -102,23 +98,11 @@
|
||||||
<section>
|
<section>
|
||||||
<h2>Input Sizes</h2>
|
<h2>Input Sizes</h2>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<Input
|
<Input buttonSize="small" label="Small Input" placeholder="Small size" />
|
||||||
buttonSize="small"
|
|
||||||
label="Small Input"
|
|
||||||
placeholder="Small size"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input buttonSize="medium" label="Medium Input" placeholder="Medium size (default)" />
|
||||||
buttonSize="medium"
|
|
||||||
label="Medium Input"
|
|
||||||
placeholder="Medium size (default)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input buttonSize="large" label="Large Input" placeholder="Large size" />
|
||||||
buttonSize="large"
|
|
||||||
label="Large Input"
|
|
||||||
placeholder="Large size"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -129,46 +113,49 @@
|
||||||
label="Input with Error"
|
label="Input with Error"
|
||||||
placeholder="Try typing something"
|
placeholder="Try typing something"
|
||||||
bind:value={withErrorValue}
|
bind:value={withErrorValue}
|
||||||
error={withErrorValue.length > 0 && withErrorValue.length < 3 ? "Too short! Minimum 3 characters" : ""}
|
error={withErrorValue.length > 0 && withErrorValue.length < 3
|
||||||
|
? 'Too short! Minimum 3 characters'
|
||||||
|
: ''}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input label="Disabled Input" bind:value={disabledValue} disabled />
|
||||||
label="Disabled Input"
|
|
||||||
bind:value={disabledValue}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input label="Readonly Input" bind:value={readonlyValue} readonly />
|
||||||
label="Readonly Input"
|
|
||||||
bind:value={readonlyValue}
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Input with Icons</h2>
|
<h2>Input with Icons</h2>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<Input
|
<Input label="With Prefix Icon" placeholder="Username" prefixIcon>
|
||||||
label="With Prefix Icon"
|
|
||||||
placeholder="Username"
|
|
||||||
prefixIcon
|
|
||||||
>
|
|
||||||
<svg slot="prefix" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg slot="prefix" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.5" />
|
<circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.5" />
|
||||||
<path d="M4 14c0-2.21 1.79-4 4-4s4 1.79 4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path
|
||||||
|
d="M4 14c0-2.21 1.79-4 4-4s4 1.79 4 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Input>
|
</Input>
|
||||||
|
|
||||||
<Input
|
<Input label="With Suffix Icon" placeholder="Email" type="email" suffixIcon>
|
||||||
label="With Suffix Icon"
|
|
||||||
placeholder="Email"
|
|
||||||
type="email"
|
|
||||||
suffixIcon
|
|
||||||
>
|
|
||||||
<svg slot="suffix" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg slot="suffix" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<rect x="2" y="4" width="12" height="8" rx="1" stroke="currentColor" stroke-width="1.5"/>
|
<rect
|
||||||
<path d="M2 5l6 3 6-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
x="2"
|
||||||
|
y="4"
|
||||||
|
width="12"
|
||||||
|
height="8"
|
||||||
|
rx="1"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2 5l6 3 6-3"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Input>
|
</Input>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -198,11 +185,7 @@
|
||||||
<section>
|
<section>
|
||||||
<h2>Form Example</h2>
|
<h2>Form Example</h2>
|
||||||
<form class="demo-form" on:submit|preventDefault>
|
<form class="demo-form" on:submit|preventDefault>
|
||||||
<Input
|
<Input label="Project Name" placeholder="My Awesome Project" required />
|
||||||
label="Project Name"
|
|
||||||
placeholder="My Awesome Project"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="url"
|
type="url"
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,7 @@
|
||||||
<h2>Single Selection Mode</h2>
|
<h2>Single Selection Mode</h2>
|
||||||
<p>Test selecting a single media item.</p>
|
<p>Test selecting a single media item.</p>
|
||||||
|
|
||||||
<Button variant="primary" onclick={openSingleModal}>
|
<Button variant="primary" onclick={openSingleModal}>Open Single Selection Modal</Button>
|
||||||
Open Single Selection Modal
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{#if selectedSingleMedia}
|
{#if selectedSingleMedia}
|
||||||
<div class="selected-media">
|
<div class="selected-media">
|
||||||
|
|
@ -58,7 +56,10 @@
|
||||||
<p><strong>Size:</strong> {formatFileSize(selectedSingleMedia.size)}</p>
|
<p><strong>Size:</strong> {formatFileSize(selectedSingleMedia.size)}</p>
|
||||||
<p><strong>Type:</strong> {selectedSingleMedia.mimeType}</p>
|
<p><strong>Type:</strong> {selectedSingleMedia.mimeType}</p>
|
||||||
{#if selectedSingleMedia.width && selectedSingleMedia.height}
|
{#if selectedSingleMedia.width && selectedSingleMedia.height}
|
||||||
<p><strong>Dimensions:</strong> {selectedSingleMedia.width}×{selectedSingleMedia.height}</p>
|
<p>
|
||||||
|
<strong>Dimensions:</strong>
|
||||||
|
{selectedSingleMedia.width}×{selectedSingleMedia.height}
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -70,9 +71,7 @@
|
||||||
<h2>Multiple Selection Mode</h2>
|
<h2>Multiple Selection Mode</h2>
|
||||||
<p>Test selecting multiple media items.</p>
|
<p>Test selecting multiple media items.</p>
|
||||||
|
|
||||||
<Button variant="primary" onclick={openMultipleModal}>
|
<Button variant="primary" onclick={openMultipleModal}>Open Multiple Selection Modal</Button>
|
||||||
Open Multiple Selection Modal
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{#if selectedMultipleMedia.length > 0}
|
{#if selectedMultipleMedia.length > 0}
|
||||||
<div class="selected-media">
|
<div class="selected-media">
|
||||||
|
|
@ -98,10 +97,13 @@
|
||||||
<h2>Image Only Selection</h2>
|
<h2>Image Only Selection</h2>
|
||||||
<p>Test selecting only image files.</p>
|
<p>Test selecting only image files.</p>
|
||||||
|
|
||||||
<Button variant="secondary" onclick={() => {
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onclick={() => {
|
||||||
showSingleModal = true
|
showSingleModal = true
|
||||||
// This will be passed to the modal for image-only filtering
|
// This will be passed to the modal for image-only filtering
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Open Image Selection Modal
|
Open Image Selection Modal
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,13 @@
|
||||||
|
|
||||||
function addFiles(newFiles: File[]) {
|
function addFiles(newFiles: File[]) {
|
||||||
// Filter for image files
|
// Filter for image files
|
||||||
const imageFiles = newFiles.filter(file => file.type.startsWith('image/'))
|
const imageFiles = newFiles.filter((file) => file.type.startsWith('image/'))
|
||||||
|
|
||||||
if (imageFiles.length !== newFiles.length) {
|
if (imageFiles.length !== newFiles.length) {
|
||||||
uploadErrors = [...uploadErrors, `${newFiles.length - imageFiles.length} non-image files were skipped`]
|
uploadErrors = [
|
||||||
|
...uploadErrors,
|
||||||
|
`${newFiles.length - imageFiles.length} non-image files were skipped`
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
files = [...files, ...imageFiles]
|
files = [...files, ...imageFiles]
|
||||||
|
|
@ -98,7 +101,7 @@
|
||||||
const response = await fetch('/api/media/upload', {
|
const response = await fetch('/api/media/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Basic ${auth}`
|
Authorization: `Basic ${auth}`
|
||||||
},
|
},
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
|
|
@ -156,12 +159,54 @@
|
||||||
<div class="drop-zone-content">
|
<div class="drop-zone-content">
|
||||||
{#if files.length === 0}
|
{#if files.length === 0}
|
||||||
<div class="upload-icon">
|
<div class="upload-icon">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
width="48"
|
||||||
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
height="48"
|
||||||
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
viewBox="0 0 24 24"
|
||||||
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
fill="none"
|
||||||
<polyline points="10,9 9,9 8,9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points="14,2 14,8 20,8"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="16"
|
||||||
|
y1="13"
|
||||||
|
x2="8"
|
||||||
|
y2="13"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="16"
|
||||||
|
y1="17"
|
||||||
|
x2="8"
|
||||||
|
y2="17"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points="10,9 9,9 8,9"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3>Drop images here</h3>
|
<h3>Drop images here</h3>
|
||||||
|
|
@ -200,7 +245,12 @@
|
||||||
<div class="file-list-header">
|
<div class="file-list-header">
|
||||||
<h3>Files to Upload</h3>
|
<h3>Files to Upload</h3>
|
||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
<Button variant="secondary" buttonSize="small" onclick={clearAll} disabled={isUploading}>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
buttonSize="small"
|
||||||
|
onclick={clearAll}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
Clear All
|
Clear All
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -248,7 +298,14 @@
|
||||||
onclick={() => removeFile(index)}
|
onclick={() => removeFile(index)}
|
||||||
title="Remove file"
|
title="Remove file"
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -267,7 +324,7 @@
|
||||||
<div class="success-message">
|
<div class="success-message">
|
||||||
✅ Successfully uploaded {successCount} file{successCount !== 1 ? 's' : ''}
|
✅ Successfully uploaded {successCount} file{successCount !== 1 ? 's' : ''}
|
||||||
{#if successCount === files.length && uploadErrors.length === 0}
|
{#if successCount === files.length && uploadErrors.length === 0}
|
||||||
<br><small>Redirecting to media library...</small>
|
<br /><small>Redirecting to media library...</small>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleMetadataPopover(event: MouseEvent) {
|
function handleMetadataPopover(event: MouseEvent) {
|
||||||
const target = event.target as Node
|
const target = event.target as Node
|
||||||
// Don't close if clicking inside the metadata button or anywhere in a metadata popover
|
// Don't close if clicking inside the metadata button or anywhere in a metadata popover
|
||||||
|
|
@ -184,7 +183,6 @@
|
||||||
showMetadata = false
|
showMetadata = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (showMetadata) {
|
if (showMetadata) {
|
||||||
document.addEventListener('click', handleMetadataPopover)
|
document.addEventListener('click', handleMetadataPopover)
|
||||||
|
|
@ -240,7 +238,7 @@
|
||||||
onAddTag={addTag}
|
onAddTag={addTag}
|
||||||
onRemoveTag={removeTag}
|
onRemoveTag={removeTag}
|
||||||
onDelete={openDeleteConfirmation}
|
onDelete={openDeleteConfirmation}
|
||||||
onClose={() => showMetadata = false}
|
onClose={() => (showMetadata = false)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -373,7 +371,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: $unit-2x $unit-3x;
|
padding: $unit-2x $unit-3x;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
||||||
|
|
@ -114,8 +114,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Mock post object for metadata popover
|
// Mock post object for metadata popover
|
||||||
const mockPost = $derived({
|
const mockPost = $derived({
|
||||||
id: null,
|
id: null,
|
||||||
|
|
@ -171,7 +169,7 @@
|
||||||
onAddTag={addTag}
|
onAddTag={addTag}
|
||||||
onRemoveTag={removeTag}
|
onRemoveTag={removeTag}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
onClose={() => showMetadata = false}
|
onClose={() => (showMetadata = false)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -252,7 +250,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: $unit-2x $unit-3x;
|
padding: $unit-2x $unit-3x;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,8 @@
|
||||||
{#if selectedStatusFilter === 'all' && selectedTypeFilter === 'all'}
|
{#if selectedStatusFilter === 'all' && selectedTypeFilter === 'all'}
|
||||||
No projects found. Create your first project!
|
No projects found. Create your first project!
|
||||||
{:else}
|
{:else}
|
||||||
No projects found matching the current filters. Try adjusting your filters or create a new project.
|
No projects found matching the current filters. Try adjusting your filters or create a
|
||||||
|
new project.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -263,8 +264,6 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $unit-6x;
|
padding: $unit-6x;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
import { prisma } from '$lib/server/database'
|
import { prisma } from '$lib/server/database'
|
||||||
import { jsonResponse, errorResponse, checkAdminAuth, parseRequestBody } from '$lib/server/api-utils'
|
import {
|
||||||
|
jsonResponse,
|
||||||
|
errorResponse,
|
||||||
|
checkAdminAuth,
|
||||||
|
parseRequestBody
|
||||||
|
} from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
|
|
||||||
// GET /api/albums/[id] - Get a single album
|
// GET /api/albums/[id] - Get a single album
|
||||||
|
|
@ -41,16 +46,16 @@ export const GET: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Create a map of media by mediaId for efficient lookup
|
// Create a map of media by mediaId for efficient lookup
|
||||||
const mediaMap = new Map()
|
const mediaMap = new Map()
|
||||||
mediaUsages.forEach(usage => {
|
mediaUsages.forEach((usage) => {
|
||||||
if (usage.media) {
|
if (usage.media) {
|
||||||
mediaMap.set(usage.mediaId, usage.media)
|
mediaMap.set(usage.mediaId, usage.media)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Enrich photos with media information using proper media usage tracking
|
// Enrich photos with media information using proper media usage tracking
|
||||||
const photosWithMedia = album.photos.map(photo => {
|
const photosWithMedia = album.photos.map((photo) => {
|
||||||
// Find the corresponding media usage record for this photo
|
// Find the corresponding media usage record for this photo
|
||||||
const usage = mediaUsages.find(u => u.media && u.media.filename === photo.filename)
|
const usage = mediaUsages.find((u) => u.media && u.media.filename === photo.filename)
|
||||||
const media = usage?.media
|
const media = usage?.media
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
import { prisma } from '$lib/server/database'
|
import { prisma } from '$lib/server/database'
|
||||||
import { jsonResponse, errorResponse, checkAdminAuth, parseRequestBody } from '$lib/server/api-utils'
|
import {
|
||||||
|
jsonResponse,
|
||||||
|
errorResponse,
|
||||||
|
checkAdminAuth,
|
||||||
|
parseRequestBody
|
||||||
|
} from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
|
|
||||||
// POST /api/albums/[id]/photos - Add a photo to an album
|
// POST /api/albums/[id]/photos - Add a photo to an album
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
import { prisma } from '$lib/server/database'
|
import { prisma } from '$lib/server/database'
|
||||||
import { deleteFile, extractPublicId } from '$lib/server/cloudinary'
|
import { deleteFile, extractPublicId } from '$lib/server/cloudinary'
|
||||||
import { jsonResponse, errorResponse, checkAdminAuth, parseRequestBody } from '$lib/server/api-utils'
|
import {
|
||||||
|
jsonResponse,
|
||||||
|
errorResponse,
|
||||||
|
checkAdminAuth,
|
||||||
|
parseRequestBody
|
||||||
|
} from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
|
|
||||||
// GET /api/media/[id] - Get a single media item
|
// GET /api/media/[id] - Get a single media item
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ export const PATCH: RequestHandler = async (event) => {
|
||||||
description: updatedMedia.description,
|
description: updatedMedia.description,
|
||||||
updatedAt: updatedMedia.updatedAt
|
updatedAt: updatedMedia.updatedAt
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Media metadata update error', error as Error)
|
logger.error('Media metadata update error', error as Error)
|
||||||
return errorResponse('Failed to update media metadata', 500)
|
return errorResponse('Failed to update media metadata', 500)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@ import type { RequestHandler } from './$types'
|
||||||
import { prisma } from '$lib/server/database'
|
import { prisma } from '$lib/server/database'
|
||||||
import { jsonResponse, errorResponse, checkAdminAuth } from '$lib/server/api-utils'
|
import { jsonResponse, errorResponse, checkAdminAuth } from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
import { trackMediaUsage, extractMediaIds, removeMediaUsage, type MediaUsageReference } from '$lib/server/media-usage.js'
|
import {
|
||||||
|
trackMediaUsage,
|
||||||
|
extractMediaIds,
|
||||||
|
removeMediaUsage,
|
||||||
|
type MediaUsageReference
|
||||||
|
} from '$lib/server/media-usage.js'
|
||||||
|
|
||||||
// POST /api/media/backfill-usage - Backfill media usage tracking for all content
|
// POST /api/media/backfill-usage - Backfill media usage tracking for all content
|
||||||
export const POST: RequestHandler = async (event) => {
|
export const POST: RequestHandler = async (event) => {
|
||||||
|
|
@ -32,7 +37,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
// Track featured image
|
// Track featured image
|
||||||
const featuredImageIds = extractMediaIds(project, 'featuredImage')
|
const featuredImageIds = extractMediaIds(project, 'featuredImage')
|
||||||
featuredImageIds.forEach(mediaId => {
|
featuredImageIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -43,7 +48,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track logo
|
// Track logo
|
||||||
const logoIds = extractMediaIds(project, 'logoUrl')
|
const logoIds = extractMediaIds(project, 'logoUrl')
|
||||||
logoIds.forEach(mediaId => {
|
logoIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -54,7 +59,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track gallery images
|
// Track gallery images
|
||||||
const galleryIds = extractMediaIds(project, 'gallery')
|
const galleryIds = extractMediaIds(project, 'gallery')
|
||||||
galleryIds.forEach(mediaId => {
|
galleryIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -65,7 +70,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track media in case study content
|
// Track media in case study content
|
||||||
const contentIds = extractMediaIds(project, 'caseStudyContent')
|
const contentIds = extractMediaIds(project, 'caseStudyContent')
|
||||||
contentIds.forEach(mediaId => {
|
contentIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -88,7 +93,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
for (const post of posts) {
|
for (const post of posts) {
|
||||||
// Track featured image
|
// Track featured image
|
||||||
const featuredImageIds = extractMediaIds(post, 'featuredImage')
|
const featuredImageIds = extractMediaIds(post, 'featuredImage')
|
||||||
featuredImageIds.forEach(mediaId => {
|
featuredImageIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'post',
|
contentType: 'post',
|
||||||
|
|
@ -99,7 +104,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track attachments
|
// Track attachments
|
||||||
const attachmentIds = extractMediaIds(post, 'attachments')
|
const attachmentIds = extractMediaIds(post, 'attachments')
|
||||||
attachmentIds.forEach(mediaId => {
|
attachmentIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'post',
|
contentType: 'post',
|
||||||
|
|
@ -110,7 +115,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track media in post content
|
// Track media in post content
|
||||||
const contentIds = extractMediaIds(post, 'content')
|
const contentIds = extractMediaIds(post, 'content')
|
||||||
contentIds.forEach(mediaId => {
|
contentIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'post',
|
contentType: 'post',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
import { prisma } from '$lib/server/database'
|
import { prisma } from '$lib/server/database'
|
||||||
import { jsonResponse, errorResponse, checkAdminAuth, parseRequestBody } from '$lib/server/api-utils'
|
import {
|
||||||
|
jsonResponse,
|
||||||
|
errorResponse,
|
||||||
|
checkAdminAuth,
|
||||||
|
parseRequestBody
|
||||||
|
} from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
import { removeMediaUsage, extractMediaIds } from '$lib/server/media-usage.js'
|
import { removeMediaUsage, extractMediaIds } from '$lib/server/media-usage.js'
|
||||||
|
|
||||||
|
|
@ -17,7 +22,7 @@ export const DELETE: RequestHandler = async (event) => {
|
||||||
return errorResponse('Invalid request body. Expected array of media IDs.', 400)
|
return errorResponse('Invalid request body. Expected array of media IDs.', 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaIds = body.mediaIds.filter(id => typeof id === 'number' && !isNaN(id))
|
const mediaIds = body.mediaIds.filter((id) => typeof id === 'number' && !isNaN(id))
|
||||||
if (mediaIds.length === 0) {
|
if (mediaIds.length === 0) {
|
||||||
return errorResponse('No valid media IDs provided', 400)
|
return errorResponse('No valid media IDs provided', 400)
|
||||||
}
|
}
|
||||||
|
|
@ -50,16 +55,15 @@ export const DELETE: RequestHandler = async (event) => {
|
||||||
logger.info('Bulk media deletion completed', {
|
logger.info('Bulk media deletion completed', {
|
||||||
deletedCount: deleteResult.count,
|
deletedCount: deleteResult.count,
|
||||||
mediaIds,
|
mediaIds,
|
||||||
filenames: mediaRecords.map(m => m.filename)
|
filenames: mediaRecords.map((m) => m.filename)
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully deleted ${deleteResult.count} media file${deleteResult.count > 1 ? 's' : ''}`,
|
message: `Successfully deleted ${deleteResult.count} media file${deleteResult.count > 1 ? 's' : ''}`,
|
||||||
deletedCount: deleteResult.count,
|
deletedCount: deleteResult.count,
|
||||||
deletedFiles: mediaRecords.map(m => ({ id: m.id, filename: m.filename }))
|
deletedFiles: mediaRecords.map((m) => ({ id: m.id, filename: m.filename }))
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to bulk delete media files', error as Error)
|
logger.error('Failed to bulk delete media files', error as Error)
|
||||||
return errorResponse('Failed to delete media files', 500)
|
return errorResponse('Failed to delete media files', 500)
|
||||||
|
|
@ -74,7 +78,7 @@ async function cleanupMediaReferences(mediaIds: number[]) {
|
||||||
where: { id: { in: mediaIds } },
|
where: { id: { in: mediaIds } },
|
||||||
select: { url: true }
|
select: { url: true }
|
||||||
})
|
})
|
||||||
const urlsToRemove = mediaUrls.map(m => m.url)
|
const urlsToRemove = mediaUrls.map((m) => m.url)
|
||||||
|
|
||||||
// Clean up projects
|
// Clean up projects
|
||||||
const projects = await prisma.project.findMany({
|
const projects = await prisma.project.findMany({
|
||||||
|
|
@ -195,7 +199,7 @@ function cleanContentFromMedia(content: any, mediaIds: number[], urlsToRemove: s
|
||||||
|
|
||||||
// Remove image nodes that reference deleted media
|
// Remove image nodes that reference deleted media
|
||||||
if (node.type === 'image' && node.attrs?.src) {
|
if (node.type === 'image' && node.attrs?.src) {
|
||||||
const shouldRemove = urlsToRemove.some(url => node.attrs.src.includes(url))
|
const shouldRemove = urlsToRemove.some((url) => node.attrs.src.includes(url))
|
||||||
if (shouldRemove) {
|
if (shouldRemove) {
|
||||||
return null // Mark for removal
|
return null // Mark for removal
|
||||||
}
|
}
|
||||||
|
|
@ -203,9 +207,7 @@ function cleanContentFromMedia(content: any, mediaIds: number[], urlsToRemove: s
|
||||||
|
|
||||||
// Clean gallery nodes
|
// Clean gallery nodes
|
||||||
if (node.type === 'gallery' && node.attrs?.images) {
|
if (node.type === 'gallery' && node.attrs?.images) {
|
||||||
const filteredImages = node.attrs.images.filter((image: any) =>
|
const filteredImages = node.attrs.images.filter((image: any) => !mediaIds.includes(image.id))
|
||||||
!mediaIds.includes(image.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (filteredImages.length === 0) {
|
if (filteredImages.length === 0) {
|
||||||
return null // Remove empty gallery
|
return null // Remove empty gallery
|
||||||
|
|
@ -222,9 +224,7 @@ function cleanContentFromMedia(content: any, mediaIds: number[], urlsToRemove: s
|
||||||
|
|
||||||
// Recursively clean child nodes
|
// Recursively clean child nodes
|
||||||
if (node.content) {
|
if (node.content) {
|
||||||
const cleanedContent = node.content
|
const cleanedContent = node.content.map(cleanNode).filter((child: any) => child !== null)
|
||||||
.map(cleanNode)
|
|
||||||
.filter((child: any) => child !== null)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,21 @@ async function extractExifData(file: File): Promise<any> {
|
||||||
const buffer = await file.arrayBuffer()
|
const buffer = await file.arrayBuffer()
|
||||||
const exif = await exifr.parse(buffer, {
|
const exif = await exifr.parse(buffer, {
|
||||||
pick: [
|
pick: [
|
||||||
'Make', 'Model', 'LensModel', 'FocalLength', 'FNumber', 'ExposureTime',
|
'Make',
|
||||||
'ISO', 'DateTime', 'DateTimeOriginal', 'CreateDate', 'GPSLatitude',
|
'Model',
|
||||||
'GPSLongitude', 'GPSAltitude', 'Orientation', 'ColorSpace'
|
'LensModel',
|
||||||
|
'FocalLength',
|
||||||
|
'FNumber',
|
||||||
|
'ExposureTime',
|
||||||
|
'ISO',
|
||||||
|
'DateTime',
|
||||||
|
'DateTimeOriginal',
|
||||||
|
'CreateDate',
|
||||||
|
'GPSLatitude',
|
||||||
|
'GPSLongitude',
|
||||||
|
'GPSAltitude',
|
||||||
|
'Orientation',
|
||||||
|
'ColorSpace'
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -77,7 +89,9 @@ async function extractExifData(file: File): Promise<any> {
|
||||||
|
|
||||||
return Object.keys(formattedExif).length > 0 ? formattedExif : null
|
return Object.keys(formattedExif).length > 0 ? formattedExif : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('Failed to extract EXIF data', { error: error instanceof Error ? error.message : 'Unknown error' })
|
logger.warn('Failed to extract EXIF data', {
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
})
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +197,10 @@ export const POST: RequestHandler = async (event) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Media upload error', error as Error)
|
logger.error('Media upload error', error as Error)
|
||||||
console.error('Detailed upload error:', error)
|
console.error('Detailed upload error:', error)
|
||||||
return errorResponse(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 500)
|
return errorResponse(
|
||||||
|
`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
500
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@ export const GET: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Transform albums to PhotoAlbum format
|
// Transform albums to PhotoAlbum format
|
||||||
const photoAlbums: PhotoAlbum[] = albums
|
const photoAlbums: PhotoAlbum[] = albums
|
||||||
.filter(album => album.photos.length > 0) // Only include albums with published photos
|
.filter((album) => album.photos.length > 0) // Only include albums with published photos
|
||||||
.map(album => ({
|
.map((album) => ({
|
||||||
id: `album-${album.id}`,
|
id: `album-${album.id}`,
|
||||||
slug: album.slug, // Add slug for navigation
|
slug: album.slug, // Add slug for navigation
|
||||||
title: album.title,
|
title: album.title,
|
||||||
|
|
@ -80,7 +80,7 @@ export const GET: RequestHandler = async (event) => {
|
||||||
width: album.photos[0].width || 400,
|
width: album.photos[0].width || 400,
|
||||||
height: album.photos[0].height || 400
|
height: album.photos[0].height || 400
|
||||||
},
|
},
|
||||||
photos: album.photos.map(photo => ({
|
photos: album.photos.map((photo) => ({
|
||||||
id: `photo-${photo.id}`,
|
id: `photo-${photo.id}`,
|
||||||
src: photo.url,
|
src: photo.url,
|
||||||
alt: photo.caption || photo.filename,
|
alt: photo.caption || photo.filename,
|
||||||
|
|
@ -92,7 +92,7 @@ export const GET: RequestHandler = async (event) => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Transform individual photos to Photo format
|
// Transform individual photos to Photo format
|
||||||
const photos: Photo[] = individualPhotos.map(photo => ({
|
const photos: Photo[] = individualPhotos.map((photo) => ({
|
||||||
id: `photo-${photo.id}`,
|
id: `photo-${photo.id}`,
|
||||||
src: photo.url,
|
src: photo.url,
|
||||||
alt: photo.title || photo.caption || photo.filename,
|
alt: photo.title || photo.caption || photo.filename,
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,13 @@ export const GET: RequestHandler = async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the specific photo
|
// Find the specific photo
|
||||||
const photo = album.photos.find(p => p.id === photoId)
|
const photo = album.photos.find((p) => p.id === photoId)
|
||||||
if (!photo) {
|
if (!photo) {
|
||||||
return errorResponse('Photo not found in album', 404)
|
return errorResponse('Photo not found in album', 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get photo index for navigation
|
// Get photo index for navigation
|
||||||
const photoIndex = album.photos.findIndex(p => p.id === photoId)
|
const photoIndex = album.photos.findIndex((p) => p.id === photoId)
|
||||||
const prevPhoto = photoIndex > 0 ? album.photos[photoIndex - 1] : null
|
const prevPhoto = photoIndex > 0 ? album.photos[photoIndex - 1] : null
|
||||||
const nextPhoto = photoIndex < album.photos.length - 1 ? album.photos[photoIndex + 1] : null
|
const nextPhoto = photoIndex < album.photos.length - 1 ? album.photos[photoIndex + 1] : null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,11 @@ import {
|
||||||
checkAdminAuth
|
checkAdminAuth
|
||||||
} from '$lib/server/api-utils'
|
} from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
import { trackMediaUsage, extractMediaIds, type MediaUsageReference } from '$lib/server/media-usage.js'
|
import {
|
||||||
|
trackMediaUsage,
|
||||||
|
extractMediaIds,
|
||||||
|
type MediaUsageReference
|
||||||
|
} from '$lib/server/media-usage.js'
|
||||||
|
|
||||||
// GET /api/posts - List all posts
|
// GET /api/posts - List all posts
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
|
|
@ -126,7 +130,8 @@ export const POST: RequestHandler = async (event) => {
|
||||||
linkUrl: data.link_url,
|
linkUrl: data.link_url,
|
||||||
linkDescription: data.linkDescription,
|
linkDescription: data.linkDescription,
|
||||||
featuredImage: featuredImageId,
|
featuredImage: featuredImageId,
|
||||||
attachments: data.attachedPhotos && data.attachedPhotos.length > 0 ? data.attachedPhotos : null,
|
attachments:
|
||||||
|
data.attachedPhotos && data.attachedPhotos.length > 0 ? data.attachedPhotos : null,
|
||||||
tags: data.tags,
|
tags: data.tags,
|
||||||
publishedAt: data.publishedAt
|
publishedAt: data.publishedAt
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +143,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track featured image
|
// Track featured image
|
||||||
const featuredImageIds = extractMediaIds({ featuredImage: featuredImageId }, 'featuredImage')
|
const featuredImageIds = extractMediaIds({ featuredImage: featuredImageId }, 'featuredImage')
|
||||||
featuredImageIds.forEach(mediaId => {
|
featuredImageIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'post',
|
contentType: 'post',
|
||||||
|
|
@ -173,7 +178,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track media in post content
|
// Track media in post content
|
||||||
const contentIds = extractMediaIds({ content: postContent }, 'content')
|
const contentIds = extractMediaIds({ content: postContent }, 'content')
|
||||||
contentIds.forEach(mediaId => {
|
contentIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'post',
|
contentType: 'post',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@ import type { RequestHandler } from './$types'
|
||||||
import { prisma } from '$lib/server/database'
|
import { prisma } from '$lib/server/database'
|
||||||
import { jsonResponse, errorResponse, checkAdminAuth } from '$lib/server/api-utils'
|
import { jsonResponse, errorResponse, checkAdminAuth } from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
import { updateMediaUsage, removeMediaUsage, extractMediaIds, trackMediaUsage, type MediaUsageReference } from '$lib/server/media-usage.js'
|
import {
|
||||||
|
updateMediaUsage,
|
||||||
|
removeMediaUsage,
|
||||||
|
extractMediaIds,
|
||||||
|
trackMediaUsage,
|
||||||
|
type MediaUsageReference
|
||||||
|
} from '$lib/server/media-usage.js'
|
||||||
|
|
||||||
// GET /api/posts/[id] - Get a single post
|
// GET /api/posts/[id] - Get a single post
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
|
|
@ -93,7 +99,8 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
linkUrl: data.link_url,
|
linkUrl: data.link_url,
|
||||||
linkDescription: data.linkDescription,
|
linkDescription: data.linkDescription,
|
||||||
featuredImage: featuredImageId,
|
featuredImage: featuredImageId,
|
||||||
attachments: data.attachedPhotos && data.attachedPhotos.length > 0 ? data.attachedPhotos : null,
|
attachments:
|
||||||
|
data.attachedPhotos && data.attachedPhotos.length > 0 ? data.attachedPhotos : null,
|
||||||
tags: data.tags,
|
tags: data.tags,
|
||||||
publishedAt: data.publishedAt
|
publishedAt: data.publishedAt
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +116,7 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track featured image
|
// Track featured image
|
||||||
const featuredImageIds = extractMediaIds(post, 'featuredImage')
|
const featuredImageIds = extractMediaIds(post, 'featuredImage')
|
||||||
featuredImageIds.forEach(mediaId => {
|
featuredImageIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'post',
|
contentType: 'post',
|
||||||
|
|
@ -120,7 +127,7 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track attachments
|
// Track attachments
|
||||||
const attachmentIds = extractMediaIds(post, 'attachments')
|
const attachmentIds = extractMediaIds(post, 'attachments')
|
||||||
attachmentIds.forEach(mediaId => {
|
attachmentIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'post',
|
contentType: 'post',
|
||||||
|
|
@ -131,7 +138,7 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track media in post content
|
// Track media in post content
|
||||||
const contentIds = extractMediaIds(post, 'content')
|
const contentIds = extractMediaIds(post, 'content')
|
||||||
contentIds.forEach(mediaId => {
|
contentIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'post',
|
contentType: 'post',
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ import {
|
||||||
} from '$lib/server/api-utils'
|
} from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
import { createSlug, ensureUniqueSlug } from '$lib/server/database'
|
import { createSlug, ensureUniqueSlug } from '$lib/server/database'
|
||||||
import { trackMediaUsage, extractMediaIds, type MediaUsageReference } from '$lib/server/media-usage.js'
|
import {
|
||||||
|
trackMediaUsage,
|
||||||
|
extractMediaIds,
|
||||||
|
type MediaUsageReference
|
||||||
|
} from '$lib/server/media-usage.js'
|
||||||
|
|
||||||
// GET /api/projects - List all projects
|
// GET /api/projects - List all projects
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
|
|
@ -22,7 +26,8 @@ export const GET: RequestHandler = async (event) => {
|
||||||
const status = event.url.searchParams.get('status')
|
const status = event.url.searchParams.get('status')
|
||||||
const projectType = event.url.searchParams.get('projectType')
|
const projectType = event.url.searchParams.get('projectType')
|
||||||
const includeListOnly = event.url.searchParams.get('includeListOnly') === 'true'
|
const includeListOnly = event.url.searchParams.get('includeListOnly') === 'true'
|
||||||
const includePasswordProtected = event.url.searchParams.get('includePasswordProtected') === 'true'
|
const includePasswordProtected =
|
||||||
|
event.url.searchParams.get('includePasswordProtected') === 'true'
|
||||||
|
|
||||||
// Build where clause
|
// Build where clause
|
||||||
const where: any = {}
|
const where: any = {}
|
||||||
|
|
@ -126,7 +131,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track featured image
|
// Track featured image
|
||||||
const featuredImageIds = extractMediaIds(body, 'featuredImage')
|
const featuredImageIds = extractMediaIds(body, 'featuredImage')
|
||||||
featuredImageIds.forEach(mediaId => {
|
featuredImageIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -137,7 +142,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track logo
|
// Track logo
|
||||||
const logoIds = extractMediaIds(body, 'logoUrl')
|
const logoIds = extractMediaIds(body, 'logoUrl')
|
||||||
logoIds.forEach(mediaId => {
|
logoIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -148,7 +153,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track gallery images
|
// Track gallery images
|
||||||
const galleryIds = extractMediaIds(body, 'gallery')
|
const galleryIds = extractMediaIds(body, 'gallery')
|
||||||
galleryIds.forEach(mediaId => {
|
galleryIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -159,7 +164,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track media in case study content
|
// Track media in case study content
|
||||||
const contentIds = extractMediaIds(body, 'caseStudyContent')
|
const contentIds = extractMediaIds(body, 'caseStudyContent')
|
||||||
contentIds.forEach(mediaId => {
|
contentIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,12 @@ import {
|
||||||
} from '$lib/server/api-utils'
|
} from '$lib/server/api-utils'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
import { ensureUniqueSlug } from '$lib/server/database'
|
import { ensureUniqueSlug } from '$lib/server/database'
|
||||||
import { updateMediaUsage, removeMediaUsage, extractMediaIds, type MediaUsageReference } from '$lib/server/media-usage.js'
|
import {
|
||||||
|
updateMediaUsage,
|
||||||
|
removeMediaUsage,
|
||||||
|
extractMediaIds,
|
||||||
|
type MediaUsageReference
|
||||||
|
} from '$lib/server/media-usage.js'
|
||||||
|
|
||||||
// GET /api/projects/[id] - Get a single project
|
// GET /api/projects/[id] - Get a single project
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
|
|
@ -103,7 +108,7 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track featured image
|
// Track featured image
|
||||||
const featuredImageIds = extractMediaIds(project, 'featuredImage')
|
const featuredImageIds = extractMediaIds(project, 'featuredImage')
|
||||||
featuredImageIds.forEach(mediaId => {
|
featuredImageIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -114,7 +119,7 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track logo
|
// Track logo
|
||||||
const logoIds = extractMediaIds(project, 'logoUrl')
|
const logoIds = extractMediaIds(project, 'logoUrl')
|
||||||
logoIds.forEach(mediaId => {
|
logoIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -125,7 +130,7 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track gallery images
|
// Track gallery images
|
||||||
const galleryIds = extractMediaIds(project, 'gallery')
|
const galleryIds = extractMediaIds(project, 'gallery')
|
||||||
galleryIds.forEach(mediaId => {
|
galleryIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
@ -136,7 +141,7 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Track media in case study content
|
// Track media in case study content
|
||||||
const contentIds = extractMediaIds(project, 'caseStudyContent')
|
const contentIds = extractMediaIds(project, 'caseStudyContent')
|
||||||
contentIds.forEach(mediaId => {
|
contentIds.forEach((mediaId) => {
|
||||||
usageReferences.push({
|
usageReferences.push({
|
||||||
mediaId,
|
mediaId,
|
||||||
contentType: 'project',
|
contentType: 'project',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dotenv/config'
|
import 'dotenv/config'
|
||||||
import { error, json } from '@sveltejs/kit'
|
import { error, json } from '@sveltejs/kit'
|
||||||
import redis from '../redis-client'
|
import redis from '../redis-client'
|
||||||
import SteamAPI, { Game, GameInfo, GameInfoExtended, UserPlaytime } from 'steamapi'
|
import SteamAPI from 'steamapi'
|
||||||
|
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ export const GET: RequestHandler = async (event) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Transform posts to universe items
|
// Transform posts to universe items
|
||||||
const postItems: UniverseItem[] = posts.map(post => ({
|
const postItems: UniverseItem[] = posts.map((post) => ({
|
||||||
id: post.id,
|
id: post.id,
|
||||||
type: 'post' as const,
|
type: 'post' as const,
|
||||||
slug: post.slug,
|
slug: post.slug,
|
||||||
|
|
@ -104,7 +104,7 @@ export const GET: RequestHandler = async (event) => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Transform albums to universe items
|
// Transform albums to universe items
|
||||||
const albumItems: UniverseItem[] = albums.map(album => ({
|
const albumItems: UniverseItem[] = albums.map((album) => ({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
type: 'album' as const,
|
type: 'album' as const,
|
||||||
slug: album.slug,
|
slug: album.slug,
|
||||||
|
|
@ -120,8 +120,9 @@ export const GET: RequestHandler = async (event) => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Combine and sort by publishedAt
|
// Combine and sort by publishedAt
|
||||||
const allItems = [...postItems, ...albumItems]
|
const allItems = [...postItems, ...albumItems].sort(
|
||||||
.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())
|
(a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
|
||||||
|
)
|
||||||
|
|
||||||
// Apply pagination
|
// Apply pagination
|
||||||
const paginatedItems = allItems.slice(offset, offset + limit)
|
const paginatedItems = allItems.slice(offset, offset + limit)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ import type { PageLoad } from './$types'
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
export const load: PageLoad = async ({ fetch }) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/projects?projectType=labs&includeListOnly=true&includePasswordProtected=true')
|
const response = await fetch(
|
||||||
|
'/api/projects?projectType=labs&includeListOnly=true&includePasswordProtected=true'
|
||||||
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch labs projects')
|
throw new Error('Failed to fetch labs projects')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,18 @@
|
||||||
</Page>
|
</Page>
|
||||||
{:else if project.status === 'password-protected'}
|
{:else if project.status === 'password-protected'}
|
||||||
<Page>
|
<Page>
|
||||||
<ProjectPasswordProtection projectSlug={project.slug} correctPassword={project.password || ''} projectType="labs">
|
<ProjectPasswordProtection
|
||||||
|
projectSlug={project.slug}
|
||||||
|
correctPassword={project.password || ''}
|
||||||
|
projectType="labs"
|
||||||
|
>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<div slot="header" class="project-header">
|
<div slot="header" class="project-header">
|
||||||
{#if project.logoUrl}
|
{#if project.logoUrl}
|
||||||
<div class="project-logo" style="background-color: {project.backgroundColor || '#f5f5f5'}">
|
<div
|
||||||
|
class="project-logo"
|
||||||
|
style="background-color: {project.backgroundColor || '#f5f5f5'}"
|
||||||
|
>
|
||||||
<img src={project.logoUrl} alt="{project.title} logo" />
|
<img src={project.logoUrl} alt="{project.title} logo" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import type { Project } from '$lib/types/project'
|
||||||
export const load: PageLoad = async ({ params, fetch }) => {
|
export const load: PageLoad = async ({ params, fetch }) => {
|
||||||
try {
|
try {
|
||||||
// Find project by slug - we'll fetch all published, list-only, and password-protected projects
|
// Find project by slug - we'll fetch all published, list-only, and password-protected projects
|
||||||
const response = await fetch(`/api/projects?projectType=labs&includeListOnly=true&includePasswordProtected=true`)
|
const response = await fetch(
|
||||||
|
`/api/projects?projectType=labs&includeListOnly=true&includePasswordProtected=true`
|
||||||
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch projects')
|
throw new Error('Failed to fetch projects')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@
|
||||||
exifData.aperture,
|
exifData.aperture,
|
||||||
formatSpeed(exifData.shutterSpeed),
|
formatSpeed(exifData.shutterSpeed),
|
||||||
exifData.iso ? `ISO ${exifData.iso}` : null
|
exifData.iso ? `ISO ${exifData.iso}` : null
|
||||||
].filter(Boolean).join(' • '),
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' • '),
|
||||||
location: exifData.location,
|
location: exifData.location,
|
||||||
dateTaken: exifData.dateTaken
|
dateTaken: exifData.dateTaken
|
||||||
}
|
}
|
||||||
|
|
@ -45,12 +47,25 @@
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
{#if photo && album}
|
{#if photo && album}
|
||||||
<title>{photo.title || photo.caption || `Photo ${navigation?.currentIndex}`} - {album.title}</title>
|
<title
|
||||||
<meta name="description" content={photo.description || photo.caption || `Photo from ${album.title}`} />
|
>{photo.title || photo.caption || `Photo ${navigation?.currentIndex}`} - {album.title}</title
|
||||||
|
>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content={photo.description || photo.caption || `Photo from ${album.title}`}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Open Graph meta tags -->
|
<!-- Open Graph meta tags -->
|
||||||
<meta property="og:title" content="{photo.title || photo.caption || `Photo ${navigation?.currentIndex}`} - {album.title}" />
|
<meta
|
||||||
<meta property="og:description" content={photo.description || photo.caption || `Photo from ${album.title}`} />
|
property="og:title"
|
||||||
|
content="{photo.title ||
|
||||||
|
photo.caption ||
|
||||||
|
`Photo ${navigation?.currentIndex}`} - {album.title}"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content={photo.description || photo.caption || `Photo from ${album.title}`}
|
||||||
|
/>
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:image" content={photo.url} />
|
<meta property="og:image" content={photo.url} />
|
||||||
|
|
||||||
|
|
@ -68,7 +83,7 @@
|
||||||
<div class="error-container">
|
<div class="error-container">
|
||||||
<div class="error-content">
|
<div class="error-content">
|
||||||
<h1>Photo Not Found</h1>
|
<h1>Photo Not Found</h1>
|
||||||
<p>{error || 'The photo you\'re looking for doesn\'t exist.'}</p>
|
<p>{error || "The photo you're looking for doesn't exist."}</p>
|
||||||
<a href="/photos" class="back-link">← Back to Photos</a>
|
<a href="/photos" class="back-link">← Back to Photos</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,14 +103,26 @@
|
||||||
{#if navigation.prevPhoto}
|
{#if navigation.prevPhoto}
|
||||||
<a href="/photos/{album.slug}/{navigation.prevPhoto.id}" class="nav-btn prev">
|
<a href="/photos/{album.slug}/{navigation.prevPhoto.id}" class="nav-btn prev">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||||
<path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path
|
||||||
|
d="M12.5 15L7.5 10L12.5 5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Previous
|
Previous
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="nav-btn disabled">
|
<div class="nav-btn disabled">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||||
<path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path
|
||||||
|
d="M12.5 15L7.5 10L12.5 5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Previous
|
Previous
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,14 +132,26 @@
|
||||||
<a href="/photos/{album.slug}/{navigation.nextPhoto.id}" class="nav-btn next">
|
<a href="/photos/{album.slug}/{navigation.nextPhoto.id}" class="nav-btn next">
|
||||||
Next
|
Next
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||||
<path d="M7.5 5L12.5 10L7.5 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path
|
||||||
|
d="M7.5 5L12.5 10L7.5 15"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="nav-btn disabled">
|
<div class="nav-btn disabled">
|
||||||
Next
|
Next
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||||
<path d="M7.5 5L12.5 10L7.5 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path
|
||||||
|
d="M7.5 5L12.5 10L7.5 15"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -241,16 +280,16 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"header header"
|
'header header'
|
||||||
"main details";
|
'main details';
|
||||||
grid-template-columns: 1fr 400px;
|
grid-template-columns: 1fr 400px;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
|
|
||||||
@include breakpoint('tablet') {
|
@include breakpoint('tablet') {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"header"
|
'header'
|
||||||
"main"
|
'main'
|
||||||
"details";
|
'details';
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: auto 1fr auto;
|
grid-template-rows: auto 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,16 @@
|
||||||
const error = $derived(data.error)
|
const error = $derived(data.error)
|
||||||
|
|
||||||
// Transform album data to PhotoItem format for PhotoGrid
|
// Transform album data to PhotoItem format for PhotoGrid
|
||||||
const photoItems = $derived(album?.photos?.map((photo: any) => ({
|
const photoItems = $derived(
|
||||||
|
album?.photos?.map((photo: any) => ({
|
||||||
id: `photo-${photo.id}`,
|
id: `photo-${photo.id}`,
|
||||||
src: photo.url,
|
src: photo.url,
|
||||||
alt: photo.caption || photo.filename,
|
alt: photo.caption || photo.filename,
|
||||||
caption: photo.caption,
|
caption: photo.caption,
|
||||||
width: photo.width || 400,
|
width: photo.width || 400,
|
||||||
height: photo.height || 400
|
height: photo.height || 400
|
||||||
})) ?? [])
|
})) ?? []
|
||||||
|
)
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
|
|
@ -68,13 +70,15 @@
|
||||||
{#if album.location}
|
{#if album.location}
|
||||||
<span class="meta-item">📍 {album.location}</span>
|
<span class="meta-item">📍 {album.location}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="meta-item">📷 {album.photos?.length || 0} photo{(album.photos?.length || 0) !== 1 ? 's' : ''}</span>
|
<span class="meta-item"
|
||||||
|
>📷 {album.photos?.length || 0} photo{(album.photos?.length || 0) !== 1 ? 's' : ''}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Photo Grid -->
|
<!-- Photo Grid -->
|
||||||
{#if photoItems.length > 0}
|
{#if photoItems.length > 0}
|
||||||
<PhotoGrid photoItems={photoItems} albumSlug={album.slug} />
|
<PhotoGrid {photoItems} albumSlug={album.slug} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty-album">
|
<div class="empty-album">
|
||||||
<p>This album doesn't contain any photos yet.</p>
|
<p>This album doesn't contain any photos yet.</p>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ function convertContentToHTML(content: any): string {
|
||||||
const level = block.level || 2
|
const level = block.level || 2
|
||||||
return `<h${level}>${escapeXML(block.content || '')}</h${level}>`
|
return `<h${level}>${escapeXML(block.content || '')}</h${level}>`
|
||||||
case 'list':
|
case 'list':
|
||||||
const items = (block.content || []).map((item: any) => `<li>${escapeXML(item)}</li>`).join('')
|
const items = (block.content || [])
|
||||||
|
.map((item: any) => `<li>${escapeXML(item)}</li>`)
|
||||||
|
.join('')
|
||||||
return block.listType === 'ordered' ? `<ol>${items}</ol>` : `<ul>${items}</ul>`
|
return block.listType === 'ordered' ? `<ol>${items}</ol>` : `<ul>${items}</ul>`
|
||||||
default:
|
default:
|
||||||
return `<p>${escapeXML(block.content || '')}</p>`
|
return `<p>${escapeXML(block.content || '')}</p>`
|
||||||
|
|
@ -119,11 +121,12 @@ export const GET: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Combine all content types
|
// Combine all content types
|
||||||
const items = [
|
const items = [
|
||||||
...posts.map(post => ({
|
...posts.map((post) => ({
|
||||||
type: 'post',
|
type: 'post',
|
||||||
section: 'universe',
|
section: 'universe',
|
||||||
id: post.id.toString(),
|
id: post.id.toString(),
|
||||||
title: post.title || `${post.postType.charAt(0).toUpperCase() + post.postType.slice(1)} Post`,
|
title:
|
||||||
|
post.title || `${post.postType.charAt(0).toUpperCase() + post.postType.slice(1)} Post`,
|
||||||
description: post.excerpt || extractTextSummary(post.content) || '',
|
description: post.excerpt || extractTextSummary(post.content) || '',
|
||||||
content: convertContentToHTML(post.content),
|
content: convertContentToHTML(post.content),
|
||||||
link: `${event.url.origin}/universe/${post.slug}`,
|
link: `${event.url.origin}/universe/${post.slug}`,
|
||||||
|
|
@ -133,12 +136,14 @@ export const GET: RequestHandler = async (event) => {
|
||||||
postType: post.postType,
|
postType: post.postType,
|
||||||
linkUrl: post.linkUrl || null
|
linkUrl: post.linkUrl || null
|
||||||
})),
|
})),
|
||||||
...universeAlbums.map(album => ({
|
...universeAlbums.map((album) => ({
|
||||||
type: 'album',
|
type: 'album',
|
||||||
section: 'universe',
|
section: 'universe',
|
||||||
id: album.id.toString(),
|
id: album.id.toString(),
|
||||||
title: album.title,
|
title: album.title,
|
||||||
description: album.description || `Photo album with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`,
|
description:
|
||||||
|
album.description ||
|
||||||
|
`Photo album with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`,
|
||||||
content: album.description ? `<p>${escapeXML(album.description)}</p>` : '',
|
content: album.description ? `<p>${escapeXML(album.description)}</p>` : '',
|
||||||
link: `${event.url.origin}/photos/${album.slug}`,
|
link: `${event.url.origin}/photos/${album.slug}`,
|
||||||
guid: `${event.url.origin}/photos/${album.slug}`,
|
guid: `${event.url.origin}/photos/${album.slug}`,
|
||||||
|
|
@ -149,13 +154,15 @@ export const GET: RequestHandler = async (event) => {
|
||||||
location: album.location
|
location: album.location
|
||||||
})),
|
})),
|
||||||
...photoAlbums
|
...photoAlbums
|
||||||
.filter(album => !universeAlbums.some(ua => ua.id === album.id)) // Avoid duplicates
|
.filter((album) => !universeAlbums.some((ua) => ua.id === album.id)) // Avoid duplicates
|
||||||
.map(album => ({
|
.map((album) => ({
|
||||||
type: 'album',
|
type: 'album',
|
||||||
section: 'photos',
|
section: 'photos',
|
||||||
id: album.id.toString(),
|
id: album.id.toString(),
|
||||||
title: album.title,
|
title: album.title,
|
||||||
description: album.description || `Photography album${album.location ? ` from ${album.location}` : ''} with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`,
|
description:
|
||||||
|
album.description ||
|
||||||
|
`Photography album${album.location ? ` from ${album.location}` : ''} with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`,
|
||||||
content: album.description ? `<p>${escapeXML(album.description)}</p>` : '',
|
content: album.description ? `<p>${escapeXML(album.description)}</p>` : '',
|
||||||
link: `${event.url.origin}/photos/${album.slug}`,
|
link: `${event.url.origin}/photos/${album.slug}`,
|
||||||
guid: `${event.url.origin}/photos/${album.slug}`,
|
guid: `${event.url.origin}/photos/${album.slug}`,
|
||||||
|
|
@ -186,7 +193,9 @@ export const GET: RequestHandler = async (event) => {
|
||||||
<generator>SvelteKit RSS Generator</generator>
|
<generator>SvelteKit RSS Generator</generator>
|
||||||
<docs>https://cyber.harvard.edu/rss/rss.html</docs>
|
<docs>https://cyber.harvard.edu/rss/rss.html</docs>
|
||||||
<ttl>60</ttl>
|
<ttl>60</ttl>
|
||||||
${items.map(item => `
|
${items
|
||||||
|
.map(
|
||||||
|
(item) => `
|
||||||
<item>
|
<item>
|
||||||
<title>${escapeXML(item.title)}</title>
|
<title>${escapeXML(item.title)}</title>
|
||||||
<description><![CDATA[${item.description}]]></description>
|
<description><![CDATA[${item.description}]]></description>
|
||||||
|
|
@ -198,13 +207,19 @@ ${item.updatedDate ? `<atom:updated>${new Date(item.updatedDate).toISOString()}<
|
||||||
<category>${item.section}</category>
|
<category>${item.section}</category>
|
||||||
<category>${item.type === 'post' ? item.postType : 'album'}</category>
|
<category>${item.type === 'post' ? item.postType : 'album'}</category>
|
||||||
${item.type === 'post' && item.linkUrl ? `<comments>${item.linkUrl}</comments>` : ''}
|
${item.type === 'post' && item.linkUrl ? `<comments>${item.linkUrl}</comments>` : ''}
|
||||||
${item.type === 'album' && item.coverPhoto ? `
|
${
|
||||||
|
item.type === 'album' && item.coverPhoto
|
||||||
|
? `
|
||||||
<enclosure url="${event.url.origin}${item.coverPhoto.url}" type="image/jpeg" length="0"/>
|
<enclosure url="${event.url.origin}${item.coverPhoto.url}" type="image/jpeg" length="0"/>
|
||||||
<media:thumbnail url="${event.url.origin}${item.coverPhoto.thumbnailUrl || item.coverPhoto.url}"/>
|
<media:thumbnail url="${event.url.origin}${item.coverPhoto.thumbnailUrl || item.coverPhoto.url}"/>
|
||||||
<media:content url="${event.url.origin}${item.coverPhoto.url}" type="image/jpeg"/>` : ''}
|
<media:content url="${event.url.origin}${item.coverPhoto.url}" type="image/jpeg"/>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
${item.location ? `<category domain="location">${escapeXML(item.location)}</category>` : ''}
|
${item.location ? `<category domain="location">${escapeXML(item.location)}</category>` : ''}
|
||||||
<author>noreply@jedmund.com (Justin Edmund)</author>
|
<author>noreply@jedmund.com (Justin Edmund)</author>
|
||||||
</item>`).join('')}
|
</item>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
</channel>
|
</channel>
|
||||||
</rss>`
|
</rss>`
|
||||||
|
|
||||||
|
|
@ -215,9 +230,9 @@ ${item.location ? `<category domain="location">${escapeXML(item.location)}</cate
|
||||||
'Content-Type': 'application/rss+xml; charset=utf-8',
|
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||||
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
||||||
'Last-Modified': lastBuildDate,
|
'Last-Modified': lastBuildDate,
|
||||||
'ETag': `"${Buffer.from(rssXml).toString('base64').slice(0, 16)}"`,
|
ETag: `"${Buffer.from(rssXml).toString('base64').slice(0, 16)}"`,
|
||||||
'X-Content-Type-Options': 'nosniff',
|
'X-Content-Type-Options': 'nosniff',
|
||||||
'Vary': 'Accept-Encoding'
|
Vary: 'Accept-Encoding'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,13 @@ export const GET: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Combine albums and standalone photos
|
// Combine albums and standalone photos
|
||||||
const items = [
|
const items = [
|
||||||
...albums.map(album => ({
|
...albums.map((album) => ({
|
||||||
type: 'album',
|
type: 'album',
|
||||||
id: album.id.toString(),
|
id: album.id.toString(),
|
||||||
title: album.title,
|
title: album.title,
|
||||||
description: album.description || `Photography album${album.location ? ` from ${album.location}` : ''} with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`,
|
description:
|
||||||
|
album.description ||
|
||||||
|
`Photography album${album.location ? ` from ${album.location}` : ''} with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`,
|
||||||
content: album.description ? `<p>${escapeXML(album.description)}</p>` : '',
|
content: album.description ? `<p>${escapeXML(album.description)}</p>` : '',
|
||||||
link: `${event.url.origin}/photos/${album.slug}`,
|
link: `${event.url.origin}/photos/${album.slug}`,
|
||||||
pubDate: album.createdAt,
|
pubDate: album.createdAt,
|
||||||
|
|
@ -78,12 +80,16 @@ export const GET: RequestHandler = async (event) => {
|
||||||
location: album.location,
|
location: album.location,
|
||||||
date: album.date
|
date: album.date
|
||||||
})),
|
})),
|
||||||
...standalonePhotos.map(photo => ({
|
...standalonePhotos.map((photo) => ({
|
||||||
type: 'photo',
|
type: 'photo',
|
||||||
id: photo.id.toString(),
|
id: photo.id.toString(),
|
||||||
title: photo.title || photo.filename,
|
title: photo.title || photo.filename,
|
||||||
description: photo.description || photo.caption || `Photo: ${photo.filename}`,
|
description: photo.description || photo.caption || `Photo: ${photo.filename}`,
|
||||||
content: photo.description ? `<p>${escapeXML(photo.description)}</p>` : (photo.caption ? `<p>${escapeXML(photo.caption)}</p>` : ''),
|
content: photo.description
|
||||||
|
? `<p>${escapeXML(photo.description)}</p>`
|
||||||
|
: photo.caption
|
||||||
|
? `<p>${escapeXML(photo.caption)}</p>`
|
||||||
|
: '',
|
||||||
link: `${event.url.origin}/photos/photo/${photo.slug || photo.id}`,
|
link: `${event.url.origin}/photos/photo/${photo.slug || photo.id}`,
|
||||||
pubDate: photo.publishedAt || photo.createdAt,
|
pubDate: photo.publishedAt || photo.createdAt,
|
||||||
updatedDate: photo.updatedAt,
|
updatedDate: photo.updatedAt,
|
||||||
|
|
@ -111,7 +117,9 @@ export const GET: RequestHandler = async (event) => {
|
||||||
<generator>SvelteKit RSS Generator</generator>
|
<generator>SvelteKit RSS Generator</generator>
|
||||||
<docs>https://cyber.harvard.edu/rss/rss.html</docs>
|
<docs>https://cyber.harvard.edu/rss/rss.html</docs>
|
||||||
<ttl>60</ttl>
|
<ttl>60</ttl>
|
||||||
${items.map(item => `
|
${items
|
||||||
|
.map(
|
||||||
|
(item) => `
|
||||||
<item>
|
<item>
|
||||||
<title>${escapeXML(item.title)}</title>
|
<title>${escapeXML(item.title)}</title>
|
||||||
<description><![CDATA[${item.description}]]></description>
|
<description><![CDATA[${item.description}]]></description>
|
||||||
|
|
@ -121,17 +129,27 @@ ${item.content ? `<content:encoded><![CDATA[${item.content}]]></content:encoded>
|
||||||
<pubDate>${formatRFC822Date(new Date(item.pubDate))}</pubDate>
|
<pubDate>${formatRFC822Date(new Date(item.pubDate))}</pubDate>
|
||||||
${item.updatedDate ? `<atom:updated>${new Date(item.updatedDate).toISOString()}</atom:updated>` : ''}
|
${item.updatedDate ? `<atom:updated>${new Date(item.updatedDate).toISOString()}</atom:updated>` : ''}
|
||||||
<category>${item.type}</category>
|
<category>${item.type}</category>
|
||||||
${item.type === 'album' && item.coverPhoto ? `
|
${
|
||||||
|
item.type === 'album' && item.coverPhoto
|
||||||
|
? `
|
||||||
<enclosure url="${event.url.origin}${item.coverPhoto.url}" type="image/jpeg" length="0"/>
|
<enclosure url="${event.url.origin}${item.coverPhoto.url}" type="image/jpeg" length="0"/>
|
||||||
<media:thumbnail url="${event.url.origin}${item.coverPhoto.thumbnailUrl || item.coverPhoto.url}"/>
|
<media:thumbnail url="${event.url.origin}${item.coverPhoto.thumbnailUrl || item.coverPhoto.url}"/>
|
||||||
<media:content url="${event.url.origin}${item.coverPhoto.url}" type="image/jpeg"/>` : ''}
|
<media:content url="${event.url.origin}${item.coverPhoto.url}" type="image/jpeg"/>`
|
||||||
${item.type === 'photo' ? `
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
item.type === 'photo'
|
||||||
|
? `
|
||||||
<enclosure url="${event.url.origin}${item.url}" type="image/jpeg" length="0"/>
|
<enclosure url="${event.url.origin}${item.url}" type="image/jpeg" length="0"/>
|
||||||
<media:thumbnail url="${event.url.origin}${item.thumbnailUrl || item.url}"/>
|
<media:thumbnail url="${event.url.origin}${item.thumbnailUrl || item.url}"/>
|
||||||
<media:content url="${event.url.origin}${item.url}" type="image/jpeg"/>` : ''}
|
<media:content url="${event.url.origin}${item.url}" type="image/jpeg"/>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
${item.location ? `<category domain="location">${escapeXML(item.location)}</category>` : ''}
|
${item.location ? `<category domain="location">${escapeXML(item.location)}</category>` : ''}
|
||||||
<author>noreply@jedmund.com (Justin Edmund)</author>
|
<author>noreply@jedmund.com (Justin Edmund)</author>
|
||||||
</item>`).join('')}
|
</item>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
</channel>
|
</channel>
|
||||||
</rss>`
|
</rss>`
|
||||||
|
|
||||||
|
|
@ -142,9 +160,9 @@ ${item.location ? `<category domain="location">${escapeXML(item.location)}</cate
|
||||||
'Content-Type': 'application/rss+xml; charset=utf-8',
|
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||||
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
||||||
'Last-Modified': lastBuildDate,
|
'Last-Modified': lastBuildDate,
|
||||||
'ETag': `"${Buffer.from(rssXml).toString('base64').slice(0, 16)}"`,
|
ETag: `"${Buffer.from(rssXml).toString('base64').slice(0, 16)}"`,
|
||||||
'X-Content-Type-Options': 'nosniff',
|
'X-Content-Type-Options': 'nosniff',
|
||||||
'Vary': 'Accept-Encoding'
|
Vary: 'Accept-Encoding'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ function convertContentToHTML(content: any): string {
|
||||||
const level = block.level || 2
|
const level = block.level || 2
|
||||||
return `<h${level}>${escapeXML(block.content || '')}</h${level}>`
|
return `<h${level}>${escapeXML(block.content || '')}</h${level}>`
|
||||||
case 'list':
|
case 'list':
|
||||||
const items = (block.content || []).map((item: any) => `<li>${escapeXML(item)}</li>`).join('')
|
const items = (block.content || [])
|
||||||
|
.map((item: any) => `<li>${escapeXML(item)}</li>`)
|
||||||
|
.join('')
|
||||||
return block.listType === 'ordered' ? `<ol>${items}</ol>` : `<ul>${items}</ul>`
|
return block.listType === 'ordered' ? `<ol>${items}</ol>` : `<ul>${items}</ul>`
|
||||||
default:
|
default:
|
||||||
return `<p>${escapeXML(block.content || '')}</p>`
|
return `<p>${escapeXML(block.content || '')}</p>`
|
||||||
|
|
@ -81,10 +83,11 @@ export const GET: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Combine and sort by date
|
// Combine and sort by date
|
||||||
const items = [
|
const items = [
|
||||||
...posts.map(post => ({
|
...posts.map((post) => ({
|
||||||
type: 'post',
|
type: 'post',
|
||||||
id: post.id.toString(),
|
id: post.id.toString(),
|
||||||
title: post.title || `${post.postType.charAt(0).toUpperCase() + post.postType.slice(1)} Post`,
|
title:
|
||||||
|
post.title || `${post.postType.charAt(0).toUpperCase() + post.postType.slice(1)} Post`,
|
||||||
description: post.excerpt || extractTextSummary(post.content) || '',
|
description: post.excerpt || extractTextSummary(post.content) || '',
|
||||||
content: convertContentToHTML(post.content),
|
content: convertContentToHTML(post.content),
|
||||||
link: `${event.url.origin}/universe/${post.slug}`,
|
link: `${event.url.origin}/universe/${post.slug}`,
|
||||||
|
|
@ -94,11 +97,13 @@ export const GET: RequestHandler = async (event) => {
|
||||||
postType: post.postType,
|
postType: post.postType,
|
||||||
linkUrl: post.linkUrl || null
|
linkUrl: post.linkUrl || null
|
||||||
})),
|
})),
|
||||||
...albums.map(album => ({
|
...albums.map((album) => ({
|
||||||
type: 'album',
|
type: 'album',
|
||||||
id: album.id.toString(),
|
id: album.id.toString(),
|
||||||
title: album.title,
|
title: album.title,
|
||||||
description: album.description || `Photo album with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`,
|
description:
|
||||||
|
album.description ||
|
||||||
|
`Photo album with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`,
|
||||||
content: album.description ? `<p>${escapeXML(album.description)}</p>` : '',
|
content: album.description ? `<p>${escapeXML(album.description)}</p>` : '',
|
||||||
link: `${event.url.origin}/photos/${album.slug}`,
|
link: `${event.url.origin}/photos/${album.slug}`,
|
||||||
guid: `${event.url.origin}/photos/${album.slug}`,
|
guid: `${event.url.origin}/photos/${album.slug}`,
|
||||||
|
|
@ -126,7 +131,9 @@ export const GET: RequestHandler = async (event) => {
|
||||||
<generator>SvelteKit RSS Generator</generator>
|
<generator>SvelteKit RSS Generator</generator>
|
||||||
<docs>https://cyber.harvard.edu/rss/rss.html</docs>
|
<docs>https://cyber.harvard.edu/rss/rss.html</docs>
|
||||||
<ttl>60</ttl>
|
<ttl>60</ttl>
|
||||||
${items.map(item => `
|
${items
|
||||||
|
.map(
|
||||||
|
(item) => `
|
||||||
<item>
|
<item>
|
||||||
<title>${escapeXML(item.title)}</title>
|
<title>${escapeXML(item.title)}</title>
|
||||||
<description><![CDATA[${item.description}]]></description>
|
<description><![CDATA[${item.description}]]></description>
|
||||||
|
|
@ -138,7 +145,9 @@ ${item.updatedDate ? `<atom:updated>${new Date(item.updatedDate).toISOString()}<
|
||||||
<category>${item.type === 'post' ? item.postType : 'album'}</category>
|
<category>${item.type === 'post' ? item.postType : 'album'}</category>
|
||||||
${item.type === 'post' && item.linkUrl ? `<comments>${item.linkUrl}</comments>` : ''}
|
${item.type === 'post' && item.linkUrl ? `<comments>${item.linkUrl}</comments>` : ''}
|
||||||
<author>noreply@jedmund.com (Justin Edmund)</author>
|
<author>noreply@jedmund.com (Justin Edmund)</author>
|
||||||
</item>`).join('')}
|
</item>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
</channel>
|
</channel>
|
||||||
</rss>`
|
</rss>`
|
||||||
|
|
||||||
|
|
@ -149,9 +158,9 @@ ${item.type === 'post' && item.linkUrl ? `<comments>${item.linkUrl}</comments>`
|
||||||
'Content-Type': 'application/rss+xml; charset=utf-8',
|
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||||
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
||||||
'Last-Modified': lastBuildDate,
|
'Last-Modified': lastBuildDate,
|
||||||
'ETag': `"${Buffer.from(rssXml).toString('base64').slice(0, 16)}"`,
|
ETag: `"${Buffer.from(rssXml).toString('base64').slice(0, 16)}"`,
|
||||||
'X-Content-Type-Options': 'nosniff',
|
'X-Content-Type-Options': 'nosniff',
|
||||||
'Vary': 'Accept-Encoding'
|
Vary: 'Accept-Encoding'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,17 @@
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
{#if post}
|
{#if post}
|
||||||
<title>{pageTitle} - jedmund</title>
|
<title>{pageTitle} - jedmund</title>
|
||||||
<meta name="description" content={post.excerpt || `${post.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`} />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content={post.excerpt || `${post.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Open Graph meta tags -->
|
<!-- Open Graph meta tags -->
|
||||||
<meta property="og:title" content={pageTitle} />
|
<meta property="og:title" content={pageTitle} />
|
||||||
<meta property="og:description" content={post.excerpt || `${post.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`} />
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content={post.excerpt || `${post.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`}
|
||||||
|
/>
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
{#if post.attachments && post.attachments.length > 0}
|
{#if post.attachments && post.attachments.length > 0}
|
||||||
<meta property="og:image" content={post.attachments[0].url} />
|
<meta property="og:image" content={post.attachments[0].url} />
|
||||||
|
|
@ -36,7 +42,7 @@
|
||||||
<div class="error-container">
|
<div class="error-container">
|
||||||
<div class="error-content">
|
<div class="error-content">
|
||||||
<h1>Post Not Found</h1>
|
<h1>Post Not Found</h1>
|
||||||
<p>{error || 'The post you\'re looking for doesn\'t exist.'}</p>
|
<p>{error || "The post you're looking for doesn't exist."}</p>
|
||||||
<a href="/universe" class="back-link">← Back to Universe</a>
|
<a href="/universe" class="back-link">← Back to Universe</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,18 @@
|
||||||
</Page>
|
</Page>
|
||||||
{:else if project.status === 'password-protected'}
|
{:else if project.status === 'password-protected'}
|
||||||
<Page>
|
<Page>
|
||||||
<ProjectPasswordProtection projectSlug={project.slug} correctPassword={project.password || ''} projectType="work">
|
<ProjectPasswordProtection
|
||||||
|
projectSlug={project.slug}
|
||||||
|
correctPassword={project.password || ''}
|
||||||
|
projectType="work"
|
||||||
|
>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<div slot="header" class="project-header">
|
<div slot="header" class="project-header">
|
||||||
{#if project.logoUrl}
|
{#if project.logoUrl}
|
||||||
<div class="project-logo" style="background-color: {project.backgroundColor || '#f5f5f5'}">
|
<div
|
||||||
|
class="project-logo"
|
||||||
|
style="background-color: {project.backgroundColor || '#f5f5f5'}"
|
||||||
|
>
|
||||||
<img src={project.logoUrl} alt="{project.title} logo" />
|
<img src={project.logoUrl} alt="{project.title} logo" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script module>
|
<script module>
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
import { defineMeta } from '@storybook/addon-svelte-csf'
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte'
|
||||||
import { fn } from 'storybook/test';
|
import { fn } from 'storybook/test'
|
||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
|
|
@ -12,13 +12,13 @@
|
||||||
backgroundColor: { control: 'color' },
|
backgroundColor: { control: 'color' },
|
||||||
size: {
|
size: {
|
||||||
control: { type: 'select' },
|
control: { type: 'select' },
|
||||||
options: ['small', 'medium', 'large'],
|
options: ['small', 'medium', 'large']
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
onclick: fn(),
|
onclick: fn()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -->
|
<!-- More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -->
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './button.css';
|
import './button.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Is this the principal call to action on the page? */
|
/** Is this the principal call to action on the page? */
|
||||||
primary?: boolean;
|
primary?: boolean
|
||||||
/** What background color to use */
|
/** What background color to use */
|
||||||
backgroundColor?: string;
|
backgroundColor?: string
|
||||||
/** How large should the button be? */
|
/** How large should the button be? */
|
||||||
size?: 'small' | 'medium' | 'large';
|
size?: 'small' | 'medium' | 'large'
|
||||||
/** Button contents */
|
/** Button contents */
|
||||||
label: string;
|
label: string
|
||||||
/** The onclick event handler */
|
/** The onclick event handler */
|
||||||
onclick?: () => void;
|
onclick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const { primary = false, backgroundColor, size = 'medium', label, ...props }: Props = $props();
|
const { primary = false, backgroundColor, size = 'medium', label, ...props }: Props = $props()
|
||||||
|
|
||||||
let mode = $derived(primary ? 'storybook-button--primary' : 'storybook-button--secondary');
|
let mode = $derived(primary ? 'storybook-button--primary' : 'storybook-button--secondary')
|
||||||
let style = $derived(backgroundColor ? `background-color: ${backgroundColor}` : '');
|
let style = $derived(backgroundColor ? `background-color: ${backgroundColor}` : '')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
import { Meta } from "@storybook/addon-docs/blocks";
|
import { Meta } from '@storybook/addon-docs/blocks'
|
||||||
|
|
||||||
import Github from "./assets/github.svg";
|
import Github from './assets/github.svg'
|
||||||
import Discord from "./assets/discord.svg";
|
import Discord from './assets/discord.svg'
|
||||||
import Youtube from "./assets/youtube.svg";
|
import Youtube from './assets/youtube.svg'
|
||||||
import Tutorials from "./assets/tutorials.svg";
|
import Tutorials from './assets/tutorials.svg'
|
||||||
import Styling from "./assets/styling.png";
|
import Styling from './assets/styling.png'
|
||||||
import Context from "./assets/context.png";
|
import Context from './assets/context.png'
|
||||||
import Assets from "./assets/assets.png";
|
import Assets from './assets/assets.png'
|
||||||
import Docs from "./assets/docs.png";
|
import Docs from './assets/docs.png'
|
||||||
import Share from "./assets/share.png";
|
import Share from './assets/share.png'
|
||||||
import FigmaPlugin from "./assets/figma-plugin.png";
|
import FigmaPlugin from './assets/figma-plugin.png'
|
||||||
import Testing from "./assets/testing.png";
|
import Testing from './assets/testing.png'
|
||||||
import Accessibility from "./assets/accessibility.png";
|
import Accessibility from './assets/accessibility.png'
|
||||||
import Theming from "./assets/theming.png";
|
import Theming from './assets/theming.png'
|
||||||
import AddonLibrary from "./assets/addon-library.png";
|
import AddonLibrary from './assets/addon-library.png'
|
||||||
|
|
||||||
export const RightArrow = () => <svg
|
export const RightArrow = () => (
|
||||||
|
<svg
|
||||||
viewBox="0 0 14 14"
|
viewBox="0 0 14 14"
|
||||||
width="8px"
|
width="8px"
|
||||||
height="14px"
|
height="14px"
|
||||||
|
|
@ -30,6 +31,7 @@ export const RightArrow = () => <svg
|
||||||
>
|
>
|
||||||
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
|
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
<Meta title="Configure your project" />
|
<Meta title="Configure your project" />
|
||||||
|
|
||||||
|
|
@ -38,6 +40,7 @@ export const RightArrow = () => <svg
|
||||||
# Configure your project
|
# Configure your project
|
||||||
|
|
||||||
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
|
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="sb-section">
|
<div className="sb-section">
|
||||||
<div className="sb-section-item">
|
<div className="sb-section-item">
|
||||||
|
|
@ -84,6 +87,7 @@ export const RightArrow = () => <svg
|
||||||
# Do more with Storybook
|
# Do more with Storybook
|
||||||
|
|
||||||
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
|
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sb-section">
|
<div className="sb-section">
|
||||||
|
|
@ -203,6 +207,7 @@ export const RightArrow = () => <svg
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>Discover tutorials<RightArrow /></a>
|
>Discover tutorials<RightArrow /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script module>
|
<script module>
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
import { defineMeta } from '@storybook/addon-svelte-csf'
|
||||||
import Header from './Header.svelte';
|
import Header from './Header.svelte'
|
||||||
import { fn } from 'storybook/test';
|
import { fn } from 'storybook/test'
|
||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
|
|
@ -11,14 +11,14 @@
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
parameters: {
|
parameters: {
|
||||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||||
layout: 'fullscreen',
|
layout: 'fullscreen'
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
onLogin: fn(),
|
onLogin: fn(),
|
||||||
onLogout: fn(),
|
onLogout: fn(),
|
||||||
onCreateAccount: fn(),
|
onCreateAccount: fn()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Story name="Logged In" args={{ user: { name: 'Jane Doe' } }} />
|
<Story name="Logged In" args={{ user: { name: 'Jane Doe' } }} />
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './header.css';
|
import './header.css'
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user?: { name: string };
|
user?: { name: string }
|
||||||
onLogin?: () => void;
|
onLogin?: () => void
|
||||||
onLogout?: () => void;
|
onLogout?: () => void
|
||||||
onCreateAccount?: () => void;
|
onCreateAccount?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user, onLogin, onLogout, onCreateAccount }: Props = $props();
|
const { user, onLogin, onLogout, onCreateAccount }: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script module>
|
<script module>
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
import { defineMeta } from '@storybook/addon-svelte-csf'
|
||||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
import { expect, userEvent, waitFor, within } from 'storybook/test'
|
||||||
import Page from './Page.svelte';
|
import Page from './Page.svelte'
|
||||||
import { fn } from 'storybook/test';
|
import { fn } from 'storybook/test'
|
||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
|
|
@ -10,20 +10,22 @@
|
||||||
component: Page,
|
component: Page,
|
||||||
parameters: {
|
parameters: {
|
||||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||||
layout: 'fullscreen',
|
layout: 'fullscreen'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Story name="Logged In" play={async ({ canvasElement }) => {
|
<Story
|
||||||
const canvas = within(canvasElement);
|
name="Logged In"
|
||||||
const loginButton = canvas.getByRole('button', { name: /Log in/i });
|
play={async ({ canvasElement }) => {
|
||||||
await expect(loginButton).toBeInTheDocument();
|
const canvas = within(canvasElement)
|
||||||
await userEvent.click(loginButton);
|
const loginButton = canvas.getByRole('button', { name: /Log in/i })
|
||||||
await waitFor(() => expect(loginButton).not.toBeInTheDocument());
|
await expect(loginButton).toBeInTheDocument()
|
||||||
|
await userEvent.click(loginButton)
|
||||||
|
await waitFor(() => expect(loginButton).not.toBeInTheDocument())
|
||||||
|
|
||||||
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
|
const logoutButton = canvas.getByRole('button', { name: /Log out/i })
|
||||||
await expect(logoutButton).toBeInTheDocument();
|
await expect(logoutButton).toBeInTheDocument()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './page.css';
|
import './page.css'
|
||||||
import Header from './Header.svelte';
|
import Header from './Header.svelte'
|
||||||
|
|
||||||
let user = $state<{ name: string }>();
|
let user = $state<{ name: string }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Button from '$lib/components/admin/Button.svelte';
|
import Button from '$lib/components/admin/Button.svelte'
|
||||||
import ButtonShowcase from './ButtonShowcase.svelte';
|
import ButtonShowcase from './ButtonShowcase.svelte'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Admin/Button',
|
title: 'Admin/Button',
|
||||||
|
|
@ -38,7 +38,7 @@ export default {
|
||||||
},
|
},
|
||||||
onclick: { action: 'clicked' }
|
onclick: { action: 'clicked' }
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Interactive Playground (this will be the default story for the controls)
|
// Interactive Playground (this will be the default story for the controls)
|
||||||
export const Playground = {
|
export const Playground = {
|
||||||
|
|
@ -52,49 +52,49 @@ export const Playground = {
|
||||||
active: false,
|
active: false,
|
||||||
disabled: false
|
disabled: false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Primary story
|
// Primary story
|
||||||
export const Primary = {
|
export const Primary = {
|
||||||
args: {
|
args: {
|
||||||
variant: 'primary'
|
variant: 'primary'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Secondary story
|
// Secondary story
|
||||||
export const Secondary = {
|
export const Secondary = {
|
||||||
args: {
|
args: {
|
||||||
variant: 'secondary'
|
variant: 'secondary'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Danger story
|
// Danger story
|
||||||
export const Danger = {
|
export const Danger = {
|
||||||
args: {
|
args: {
|
||||||
variant: 'danger'
|
variant: 'danger'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Ghost story
|
// Ghost story
|
||||||
export const Ghost = {
|
export const Ghost = {
|
||||||
args: {
|
args: {
|
||||||
variant: 'ghost'
|
variant: 'ghost'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Text story
|
// Text story
|
||||||
export const Text = {
|
export const Text = {
|
||||||
args: {
|
args: {
|
||||||
variant: 'text'
|
variant: 'text'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Overlay story
|
// Overlay story
|
||||||
export const Overlay = {
|
export const Overlay = {
|
||||||
args: {
|
args: {
|
||||||
variant: 'overlay'
|
variant: 'overlay'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Loading State
|
// Loading State
|
||||||
export const Loading = {
|
export const Loading = {
|
||||||
|
|
@ -102,7 +102,7 @@ export const Loading = {
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
loading: true
|
loading: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Disabled State
|
// Disabled State
|
||||||
export const Disabled = {
|
export const Disabled = {
|
||||||
|
|
@ -110,7 +110,7 @@ export const Disabled = {
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
disabled: true
|
disabled: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Full Width
|
// Full Width
|
||||||
export const FullWidth = {
|
export const FullWidth = {
|
||||||
|
|
@ -118,11 +118,11 @@ export const FullWidth = {
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
fullWidth: true
|
fullWidth: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// All variants showcase
|
// All variants showcase
|
||||||
export const AllVariants = {
|
export const AllVariants = {
|
||||||
render: () => ({
|
render: () => ({
|
||||||
Component: ButtonShowcase
|
Component: ButtonShowcase
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import Button from '$lib/components/admin/Button.svelte';
|
import Button from '$lib/components/admin/Button.svelte'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="button-showcase">
|
<div class="button-showcase">
|
||||||
|
|
@ -30,24 +30,77 @@
|
||||||
<h4>With Icons</h4>
|
<h4>With Icons</h4>
|
||||||
<div class="button-grid">
|
<div class="button-grid">
|
||||||
<Button variant="primary" iconPosition="left">
|
<Button variant="primary" iconPosition="left">
|
||||||
<svg slot="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M12 5V19M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
slot="icon"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 5V19M5 12H19"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Add Item
|
Add Item
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="secondary" iconPosition="right">
|
<Button variant="secondary" iconPosition="right">
|
||||||
Download
|
Download
|
||||||
<svg slot="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M21 15V19A2 2 0 0119 21H5A2 2 0 013 19V15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
slot="icon"
|
||||||
<polyline points="7,10 12,15 17,10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
width="16"
|
||||||
<line x1="12" y1="15" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21 15V19A2 2 0 0119 21H5A2 2 0 013 19V15"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points="7,10 12,15 17,10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
y1="15"
|
||||||
|
x2="12"
|
||||||
|
y2="3"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="ghost" iconOnly={true}>
|
<Button variant="ghost" iconOnly={true}>
|
||||||
<svg slot="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
slot="icon"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 6L18 18M6 18L18 6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import ImageUploader from '$lib/components/admin/ImageUploader.svelte';
|
import ImageUploader from '$lib/components/admin/ImageUploader.svelte'
|
||||||
|
|
||||||
// Mock Media object for testing
|
// Mock Media object for testing
|
||||||
const mockMedia = {
|
const mockMedia = {
|
||||||
|
|
@ -15,7 +15,7 @@ const mockMedia = {
|
||||||
description: 'This is a sample image for testing purposes',
|
description: 'This is a sample image for testing purposes',
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Admin/Form Components/ImageUploader',
|
title: 'Admin/Form Components/ImageUploader',
|
||||||
|
|
@ -51,7 +51,7 @@ export default {
|
||||||
control: 'text'
|
control: 'text'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Empty uploader
|
// Empty uploader
|
||||||
export const Empty = {
|
export const Empty = {
|
||||||
|
|
@ -62,7 +62,7 @@ export const Empty = {
|
||||||
required: false,
|
required: false,
|
||||||
maxFileSize: 10
|
maxFileSize: 10
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// With uploaded image
|
// With uploaded image
|
||||||
export const WithImage = {
|
export const WithImage = {
|
||||||
|
|
@ -72,7 +72,7 @@ export const WithImage = {
|
||||||
allowAltText: true,
|
allowAltText: true,
|
||||||
aspectRatio: '1:1'
|
aspectRatio: '1:1'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Square aspect ratio
|
// Square aspect ratio
|
||||||
export const SquareAspectRatio = {
|
export const SquareAspectRatio = {
|
||||||
|
|
@ -83,7 +83,7 @@ export const SquareAspectRatio = {
|
||||||
allowAltText: true,
|
allowAltText: true,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Wide aspect ratio
|
// Wide aspect ratio
|
||||||
export const WideAspectRatio = {
|
export const WideAspectRatio = {
|
||||||
|
|
@ -94,7 +94,7 @@ export const WideAspectRatio = {
|
||||||
allowAltText: true,
|
allowAltText: true,
|
||||||
helpText: 'Recommended size: 1920x1080 pixels'
|
helpText: 'Recommended size: 1920x1080 pixels'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Required field
|
// Required field
|
||||||
export const Required = {
|
export const Required = {
|
||||||
|
|
@ -104,7 +104,7 @@ export const Required = {
|
||||||
allowAltText: true,
|
allowAltText: true,
|
||||||
placeholder: 'This field is required'
|
placeholder: 'This field is required'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// With error
|
// With error
|
||||||
export const WithError = {
|
export const WithError = {
|
||||||
|
|
@ -113,7 +113,7 @@ export const WithError = {
|
||||||
error: 'Please select a valid image file',
|
error: 'Please select a valid image file',
|
||||||
allowAltText: true
|
allowAltText: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// With help text
|
// With help text
|
||||||
export const WithHelpText = {
|
export const WithHelpText = {
|
||||||
|
|
@ -124,7 +124,7 @@ export const WithHelpText = {
|
||||||
aspectRatio: '1:1',
|
aspectRatio: '1:1',
|
||||||
maxFileSize: 5
|
maxFileSize: 5
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Without alt text
|
// Without alt text
|
||||||
export const WithoutAltText = {
|
export const WithoutAltText = {
|
||||||
|
|
@ -134,7 +134,7 @@ export const WithoutAltText = {
|
||||||
placeholder: 'Upload a decorative image',
|
placeholder: 'Upload a decorative image',
|
||||||
helpText: 'This image is purely decorative and does not need alt text.'
|
helpText: 'This image is purely decorative and does not need alt text.'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// With browse library option
|
// With browse library option
|
||||||
export const WithBrowseLibrary = {
|
export const WithBrowseLibrary = {
|
||||||
|
|
@ -144,7 +144,7 @@ export const WithBrowseLibrary = {
|
||||||
showBrowseLibrary: true,
|
showBrowseLibrary: true,
|
||||||
placeholder: 'Upload a new image or browse existing ones'
|
placeholder: 'Upload a new image or browse existing ones'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Small file size limit
|
// Small file size limit
|
||||||
export const SmallFileLimit = {
|
export const SmallFileLimit = {
|
||||||
|
|
@ -154,7 +154,7 @@ export const SmallFileLimit = {
|
||||||
allowAltText: true,
|
allowAltText: true,
|
||||||
helpText: 'Maximum file size: 1MB'
|
helpText: 'Maximum file size: 1MB'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Interactive playground
|
// Interactive playground
|
||||||
export const Playground = {
|
export const Playground = {
|
||||||
|
|
@ -166,4 +166,4 @@ export const Playground = {
|
||||||
maxFileSize: 10,
|
maxFileSize: 10,
|
||||||
showBrowseLibrary: false
|
showBrowseLibrary: false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Input from '$lib/components/admin/Input.svelte';
|
import Input from '$lib/components/admin/Input.svelte'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Admin/Input',
|
title: 'Admin/Input',
|
||||||
|
|
@ -7,7 +7,19 @@ export default {
|
||||||
argTypes: {
|
argTypes: {
|
||||||
type: {
|
type: {
|
||||||
control: { type: 'select' },
|
control: { type: 'select' },
|
||||||
options: ['text', 'email', 'password', 'url', 'search', 'number', 'tel', 'date', 'time', 'color', 'textarea']
|
options: [
|
||||||
|
'text',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'url',
|
||||||
|
'search',
|
||||||
|
'number',
|
||||||
|
'tel',
|
||||||
|
'date',
|
||||||
|
'time',
|
||||||
|
'color',
|
||||||
|
'textarea'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
control: { type: 'select' },
|
control: { type: 'select' },
|
||||||
|
|
@ -50,7 +62,7 @@ export default {
|
||||||
control: 'number'
|
control: 'number'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Interactive Playground
|
// Interactive Playground
|
||||||
export const Playground = {
|
export const Playground = {
|
||||||
|
|
@ -64,7 +76,7 @@ export const Playground = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
readonly: false
|
readonly: false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Basic text input
|
// Basic text input
|
||||||
export const Basic = {
|
export const Basic = {
|
||||||
|
|
@ -73,7 +85,7 @@ export const Basic = {
|
||||||
label: 'Basic Input',
|
label: 'Basic Input',
|
||||||
placeholder: 'Type something...'
|
placeholder: 'Type something...'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Email input
|
// Email input
|
||||||
export const Email = {
|
export const Email = {
|
||||||
|
|
@ -83,7 +95,7 @@ export const Email = {
|
||||||
placeholder: 'you@example.com',
|
placeholder: 'you@example.com',
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Password input
|
// Password input
|
||||||
export const Password = {
|
export const Password = {
|
||||||
|
|
@ -93,7 +105,7 @@ export const Password = {
|
||||||
placeholder: 'Enter your password',
|
placeholder: 'Enter your password',
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Search input
|
// Search input
|
||||||
export const Search = {
|
export const Search = {
|
||||||
|
|
@ -102,7 +114,7 @@ export const Search = {
|
||||||
label: 'Search',
|
label: 'Search',
|
||||||
placeholder: 'Search for something...'
|
placeholder: 'Search for something...'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Number input
|
// Number input
|
||||||
export const Number = {
|
export const Number = {
|
||||||
|
|
@ -113,7 +125,7 @@ export const Number = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 120
|
max: 120
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Textarea
|
// Textarea
|
||||||
export const Textarea = {
|
export const Textarea = {
|
||||||
|
|
@ -123,7 +135,7 @@ export const Textarea = {
|
||||||
placeholder: 'Tell us about yourself...',
|
placeholder: 'Tell us about yourself...',
|
||||||
rows: 4
|
rows: 4
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Auto-resizing textarea
|
// Auto-resizing textarea
|
||||||
export const AutoResizeTextarea = {
|
export const AutoResizeTextarea = {
|
||||||
|
|
@ -134,7 +146,7 @@ export const AutoResizeTextarea = {
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
rows: 2
|
rows: 2
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// With help text
|
// With help text
|
||||||
export const WithHelpText = {
|
export const WithHelpText = {
|
||||||
|
|
@ -144,7 +156,7 @@ export const WithHelpText = {
|
||||||
placeholder: 'you@example.com',
|
placeholder: 'you@example.com',
|
||||||
helpText: 'We will never share your email with anyone else.'
|
helpText: 'We will never share your email with anyone else.'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// With error
|
// With error
|
||||||
export const WithError = {
|
export const WithError = {
|
||||||
|
|
@ -155,7 +167,7 @@ export const WithError = {
|
||||||
error: 'Please enter a valid email address.',
|
error: 'Please enter a valid email address.',
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Character count
|
// Character count
|
||||||
export const CharacterCount = {
|
export const CharacterCount = {
|
||||||
|
|
@ -167,7 +179,7 @@ export const CharacterCount = {
|
||||||
showCharCount: true,
|
showCharCount: true,
|
||||||
rows: 3
|
rows: 3
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Different sizes
|
// Different sizes
|
||||||
export const Sizes = {
|
export const Sizes = {
|
||||||
|
|
@ -181,7 +193,7 @@ export const Sizes = {
|
||||||
`,
|
`,
|
||||||
components: { Input }
|
components: { Input }
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// Disabled state
|
// Disabled state
|
||||||
export const Disabled = {
|
export const Disabled = {
|
||||||
|
|
@ -191,7 +203,7 @@ export const Disabled = {
|
||||||
value: 'This input is disabled',
|
value: 'This input is disabled',
|
||||||
disabled: true
|
disabled: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Readonly state
|
// Readonly state
|
||||||
export const Readonly = {
|
export const Readonly = {
|
||||||
|
|
@ -201,7 +213,7 @@ export const Readonly = {
|
||||||
value: 'This input is readonly',
|
value: 'This input is readonly',
|
||||||
readonly: true
|
readonly: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// With prefix icon
|
// With prefix icon
|
||||||
export const WithPrefixIcon = {
|
export const WithPrefixIcon = {
|
||||||
|
|
@ -223,7 +235,7 @@ export const WithPrefixIcon = {
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// With suffix icon
|
// With suffix icon
|
||||||
export const WithSuffixIcon = {
|
export const WithSuffixIcon = {
|
||||||
|
|
@ -245,7 +257,7 @@ export const WithSuffixIcon = {
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// Color input
|
// Color input
|
||||||
export const ColorInput = {
|
export const ColorInput = {
|
||||||
|
|
@ -254,7 +266,7 @@ export const ColorInput = {
|
||||||
label: 'Pick a Color',
|
label: 'Pick a Color',
|
||||||
value: '#ff6b6b'
|
value: '#ff6b6b'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Date input
|
// Date input
|
||||||
export const DateInput = {
|
export const DateInput = {
|
||||||
|
|
@ -263,7 +275,7 @@ export const DateInput = {
|
||||||
label: 'Select Date',
|
label: 'Select Date',
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Time input
|
// Time input
|
||||||
export const TimeInput = {
|
export const TimeInput = {
|
||||||
|
|
@ -271,4 +283,4 @@ export const TimeInput = {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
label: 'Select Time'
|
label: 'Select Time'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import MediaInput from '$lib/components/admin/MediaInput.svelte';
|
import MediaInput from '$lib/components/admin/MediaInput.svelte'
|
||||||
|
|
||||||
// Mock Media objects for testing
|
// Mock Media objects for testing
|
||||||
const mockMedia = {
|
const mockMedia = {
|
||||||
|
|
@ -13,7 +13,7 @@ const mockMedia = {
|
||||||
thumbnailUrl: 'https://via.placeholder.com/300x200/0066cc/ffffff?text=Sample+Image',
|
thumbnailUrl: 'https://via.placeholder.com/300x200/0066cc/ffffff?text=Sample+Image',
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
}
|
||||||
|
|
||||||
const mockMediaList = [
|
const mockMediaList = [
|
||||||
mockMedia,
|
mockMedia,
|
||||||
|
|
@ -43,7 +43,7 @@ const mockMediaList = [
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
}
|
}
|
||||||
];
|
]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Admin/Form Components/MediaInput',
|
title: 'Admin/Form Components/MediaInput',
|
||||||
|
|
@ -71,7 +71,7 @@ export default {
|
||||||
control: 'text'
|
control: 'text'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Single media input (empty)
|
// Single media input (empty)
|
||||||
export const SingleEmpty = {
|
export const SingleEmpty = {
|
||||||
|
|
@ -82,7 +82,7 @@ export const SingleEmpty = {
|
||||||
placeholder: 'No image selected',
|
placeholder: 'No image selected',
|
||||||
value: null
|
value: null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Single media input (with value)
|
// Single media input (with value)
|
||||||
export const SingleWithValue = {
|
export const SingleWithValue = {
|
||||||
|
|
@ -92,7 +92,7 @@ export const SingleWithValue = {
|
||||||
fileType: 'image',
|
fileType: 'image',
|
||||||
value: mockMedia
|
value: mockMedia
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Multiple media input (empty)
|
// Multiple media input (empty)
|
||||||
export const MultipleEmpty = {
|
export const MultipleEmpty = {
|
||||||
|
|
@ -103,7 +103,7 @@ export const MultipleEmpty = {
|
||||||
placeholder: 'No images selected',
|
placeholder: 'No images selected',
|
||||||
value: []
|
value: []
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Multiple media input (with values)
|
// Multiple media input (with values)
|
||||||
export const MultipleWithValues = {
|
export const MultipleWithValues = {
|
||||||
|
|
@ -113,7 +113,7 @@ export const MultipleWithValues = {
|
||||||
fileType: 'image',
|
fileType: 'image',
|
||||||
value: mockMediaList
|
value: mockMediaList
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Required field
|
// Required field
|
||||||
export const Required = {
|
export const Required = {
|
||||||
|
|
@ -124,7 +124,7 @@ export const Required = {
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'Choose a logo image'
|
placeholder: 'Choose a logo image'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// With error state
|
// With error state
|
||||||
export const WithError = {
|
export const WithError = {
|
||||||
|
|
@ -135,7 +135,7 @@ export const WithError = {
|
||||||
required: true,
|
required: true,
|
||||||
error: 'Please select a profile picture'
|
error: 'Please select a profile picture'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// All file types
|
// All file types
|
||||||
export const AllFileTypes = {
|
export const AllFileTypes = {
|
||||||
|
|
@ -145,7 +145,7 @@ export const AllFileTypes = {
|
||||||
fileType: 'all',
|
fileType: 'all',
|
||||||
placeholder: 'Choose any media file'
|
placeholder: 'Choose any media file'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Video only
|
// Video only
|
||||||
export const VideoOnly = {
|
export const VideoOnly = {
|
||||||
|
|
@ -155,7 +155,7 @@ export const VideoOnly = {
|
||||||
fileType: 'video',
|
fileType: 'video',
|
||||||
placeholder: 'Choose a video file'
|
placeholder: 'Choose a video file'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Multiple with many files
|
// Multiple with many files
|
||||||
export const MultipleWithManyFiles = {
|
export const MultipleWithManyFiles = {
|
||||||
|
|
@ -193,7 +193,7 @@ export const MultipleWithManyFiles = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Interactive playground
|
// Interactive playground
|
||||||
export const Playground = {
|
export const Playground = {
|
||||||
|
|
@ -204,4 +204,4 @@ export const Playground = {
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: 'Select a file'
|
placeholder: 'Select a file'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,35 @@
|
||||||
// Simple test to check if project edit page loads correctly
|
// Simple test to check if project edit page loads correctly
|
||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer')
|
||||||
|
|
||||||
(async () => {
|
;(async () => {
|
||||||
const browser = await puppeteer.launch({ headless: false });
|
const browser = await puppeteer.launch({ headless: false })
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Go to admin login first (might be needed)
|
// Go to admin login first (might be needed)
|
||||||
await page.goto('http://localhost:5173/admin/login');
|
await page.goto('http://localhost:5173/admin/login')
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
// Try to go directly to edit page
|
// Try to go directly to edit page
|
||||||
await page.goto('http://localhost:5173/admin/projects/8/edit');
|
await page.goto('http://localhost:5173/admin/projects/8/edit')
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000)
|
||||||
|
|
||||||
// Check if title field is populated
|
// Check if title field is populated
|
||||||
const titleValue = await page.$eval('input[placeholder*="title" i], input[name*="title" i], #title',
|
const titleValue = await page.$eval(
|
||||||
el => el.value);
|
'input[placeholder*="title" i], input[name*="title" i], #title',
|
||||||
|
(el) => el.value
|
||||||
|
)
|
||||||
|
|
||||||
console.log('Title field value:', titleValue);
|
console.log('Title field value:', titleValue)
|
||||||
|
|
||||||
if (titleValue === 'Maitsu') {
|
if (titleValue === 'Maitsu') {
|
||||||
console.log('✅ Form loading works correctly!');
|
console.log('✅ Form loading works correctly!')
|
||||||
} else {
|
} else {
|
||||||
console.log('❌ Form loading failed - title not populated');
|
console.log('❌ Form loading failed - title not populated')
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Test failed:', error.message);
|
console.error('Test failed:', error.message)
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close()
|
||||||
}
|
}
|
||||||
})();
|
})()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue