chore: remove dead code and unused files

Delete completely unused files:
- album-stream.ts store (127 lines, never imported)
- AdminSegmentedController + BaseSegmentedController (546 lines, superseded by AdminSegmentedControl)
- AlbumMetadataPopover.svelte (never imported)
- 5 test/demo pages in admin routes (buttons, inputs, *-test routes)

Total cleanup: ~1,200+ lines of dead code removed
This commit is contained in:
Justin Edmund 2025-11-04 19:03:50 -08:00
parent bc102fba0a
commit d964bf05cd
9 changed files with 0 additions and 2010 deletions

View file

@ -1,205 +0,0 @@
<script lang="ts">
import { page } from '$app/stores'
import BaseSegmentedController from './BaseSegmentedController.svelte'
import { clickOutside } from '$lib/actions/clickOutside'
const currentPath = $derived($page.url.pathname)
interface NavItem {
value: string
label: string
href: string
icon: string
}
const navItems: NavItem[] = [
{ value: 'dashboard', label: 'Dashboard', href: '/admin', icon: '📊' },
{ value: 'projects', label: 'Projects', href: '/admin/projects', icon: '💼' },
{ value: 'universe', label: 'Universe', href: '/admin/universe', icon: '🌟' },
{ value: 'media', label: 'Media', href: '/admin/media', icon: '🖼️' }
]
// Track dropdown state
let showDropdown = $state(false)
// Calculate active value based on current path
const activeValue = $derived(
currentPath === '/admin'
? 'dashboard'
: currentPath.startsWith('/admin/projects')
? 'projects'
: currentPath.startsWith('/admin/posts') || currentPath.startsWith('/admin/universe')
? 'universe'
: currentPath.startsWith('/admin/media')
? 'media'
: ''
)
function handleClickOutside() {
showDropdown = false
}
</script>
<nav class="admin-segmented-controller">
<BaseSegmentedController
items={navItems}
value={activeValue}
variant="navigation"
pillColor="#e5e5e5"
gap={4}
containerPadding={0}
class="admin-nav-pills"
>
{#snippet children({ item, isActive })}
<span class="icon">{item.icon}</span>
<span>{item.label}</span>
{/snippet}
</BaseSegmentedController>
<div
class="dropdown-container"
use:clickOutside={{ enabled: showDropdown }}
onclickoutside={handleClickOutside}
>
<button
class="dropdown-trigger"
onclick={() => (showDropdown = !showDropdown)}
aria-label="Menu"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" class:rotate={showDropdown}>
<path
d="M3 5L6 8L9 5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
{#if showDropdown}
<div class="dropdown-menu">
<form method="POST" action="/admin/logout">
<button class="dropdown-item" type="submit">
<span>Log out</span>
</button>
</form>
</div>
{/if}
</div>
</nav>
<style lang="scss">
.admin-segmented-controller {
display: flex;
align-items: center;
gap: $unit;
background: $gray-100;
padding: $unit;
border-radius: 100px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
position: relative;
overflow: visible;
}
:global(.admin-nav-pills) {
flex: 1;
background: transparent !important;
padding: 0 !important;
box-shadow: none !important;
:global(.segmented-pill) {
background-color: $gray-85 !important;
}
:global(.segmented-item) {
gap: 6px;
padding: 10px 16px;
font-size: 1rem;
color: $gray-20;
&:global(.active) {
color: $gray-10;
}
}
}
.icon {
font-size: 1.1rem;
line-height: 1;
width: 20px;
text-align: center;
}
.dropdown-container {
position: relative;
}
.dropdown-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background: transparent;
border: none;
color: $gray-40;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
color: $gray-20;
}
svg {
transition: transform 0.2s ease;
&.rotate {
transform: rotate(180deg);
}
}
}
.dropdown-menu {
position: absolute;
top: calc(100% + $unit);
right: 0;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 150px;
z-index: $z-index-modal;
overflow: hidden;
animation: slideDown 0.2s ease;
}
.dropdown-item {
display: block;
width: 100%;
padding: $unit-2x $unit-3x;
background: none;
border: none;
text-align: left;
font-size: 0.925rem;
color: $gray-20;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background-color: $gray-95;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View file

@ -1,104 +0,0 @@
<script lang="ts">
import GenericMetadataPopover, { type MetadataConfig } from './GenericMetadataPopover.svelte'
type Props = {
album: any
triggerElement: HTMLElement
onUpdate: (key: string, value: any) => void
onDelete: () => void
onClose?: () => void
}
let {
album = $bindable(),
triggerElement,
onUpdate,
onDelete,
onClose = () => {}
}: Props = $props()
// Convert album date to YYYY-MM-DD format for date input
const albumDate = $derived(album.date ? new Date(album.date).toISOString().split('T')[0] : '')
// Handle date changes - convert back to ISO string
function handleDateChange(key: string, value: string) {
if (key === 'date') {
const isoDate = value ? new Date(value).toISOString() : null
onUpdate(key, isoDate)
} else {
onUpdate(key, value)
}
}
const config: MetadataConfig = {
title: 'Album Settings',
fields: [
{
type: 'input',
key: 'slug',
label: 'Slug',
placeholder: 'album-url-slug',
helpText: 'Used in the album URL.'
},
{
type: 'date',
key: 'date',
label: 'Date',
helpText: 'When was this album created or photos taken?'
},
{
type: 'input',
key: 'location',
label: 'Location',
placeholder: 'Location where photos were taken'
},
{
type: 'section',
key: 'display-options',
label: 'Display Options'
},
{
type: 'toggle',
key: 'isPhotography',
label: 'Show in Photos',
helpText: 'Show this album in the photography experience'
},
{
type: 'toggle',
key: 'showInUniverse',
label: 'Show in Universe',
helpText: 'Display this album in the Universe feed'
},
{
type: 'metadata',
key: 'metadata'
}
],
deleteButton: {
label: 'Delete Album',
action: onDelete
}
}
// Create a reactive data object that includes the formatted date
let popoverData = $state({
...album,
date: albumDate
})
// Sync changes back to album
$effect(() => {
popoverData = {
...album,
date: albumDate
}
})
</script>
<GenericMetadataPopover
{config}
bind:data={popoverData}
{triggerElement}
onUpdate={handleDateChange}
{onClose}
/>

View file

@ -1,340 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte'
import type { Snippet } from 'svelte'
interface BaseItem {
value: string | number
label: string
href?: string
[key: string]: any
}
interface Props<T extends BaseItem = BaseItem> {
items: T[]
value?: string | number
defaultValue?: string | number
onChange?: (value: string | number, item: T) => void
variant?: 'navigation' | 'selection'
size?: 'small' | 'medium' | 'large'
fullWidth?: boolean
pillColor?: string | ((item: T) => string)
showPill?: boolean
gap?: number
containerPadding?: number
class?: string
children?: Snippet<[{ item: T; index: number; isActive: boolean; isHovered: boolean }]>
}
let {
items = [],
value = $bindable(),
defaultValue,
onChange,
variant = 'selection',
size = 'medium',
fullWidth = false,
pillColor = 'white',
showPill = true,
gap = 4,
containerPadding = 4,
class: className = '',
children
}: Props = $props()
// State
let containerElement: HTMLElement
let itemElements: HTMLElement[] = []
let pillStyle = ''
let hoveredIndex = $state(-1)
let internalValue = $state(defaultValue ?? value ?? items[0]?.value ?? '')
// Derived state
const currentValue = $derived(value ?? internalValue)
const activeIndex = $derived(items.findIndex((item) => item.value === currentValue))
// Effects
$effect(() => {
if (value !== undefined) {
internalValue = value
}
})
$effect(() => {
updatePillPosition()
})
// Functions
function updatePillPosition() {
if (!showPill) return
if (activeIndex >= 0 && itemElements[activeIndex] && containerElement) {
const activeElement = itemElements[activeIndex]
const containerRect = containerElement.getBoundingClientRect()
const activeRect = activeElement.getBoundingClientRect()
const left = activeRect.left - containerRect.left - containerPadding
const width = activeRect.width
pillStyle = `transform: translateX(${left}px); width: ${width}px;`
} else {
pillStyle = 'opacity: 0;'
}
}
function handleItemClick(item: BaseItem, index: number) {
if (variant === 'selection') {
const newValue = item.value
internalValue = newValue
if (value === undefined) {
// Uncontrolled mode
value = newValue
}
onChange?.(newValue, item)
}
// Navigation variant handles clicks via href
}
function handleKeyDown(event: KeyboardEvent) {
const currentIndex = activeIndex >= 0 ? activeIndex : 0
let newIndex = currentIndex
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
event.preventDefault()
newIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1
break
case 'ArrowRight':
case 'ArrowDown':
event.preventDefault()
newIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0
break
case 'Home':
event.preventDefault()
newIndex = 0
break
case 'End':
event.preventDefault()
newIndex = items.length - 1
break
case 'Enter':
case ' ':
if (variant === 'navigation' && items[currentIndex]?.href) {
// Let the link handle navigation
return
}
event.preventDefault()
if (items[currentIndex]) {
handleItemClick(items[currentIndex], currentIndex)
}
return
}
if (newIndex !== currentIndex && items[newIndex]) {
if (variant === 'navigation' && items[newIndex].href) {
// Focus the link
itemElements[newIndex]?.focus()
} else {
handleItemClick(items[newIndex], newIndex)
}
}
}
function getPillColor(item: BaseItem): string {
if (typeof pillColor === 'function') {
return pillColor(item)
}
return pillColor
}
// Lifecycle
onMount(() => {
const handleResize = () => updatePillPosition()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
})
// Size classes
const sizeClasses = {
small: 'segmented-controller-small',
medium: 'segmented-controller-medium',
large: 'segmented-controller-large'
}
</script>
<div
bind:this={containerElement}
class="base-segmented-controller {sizeClasses[size]} {className}"
class:full-width={fullWidth}
role="tablist"
style="--gap: {gap}px; --container-padding: {containerPadding}px;"
onkeydown={handleKeyDown}
>
{#if showPill && activeIndex >= 0}
<div
class="segmented-pill"
style="{pillStyle}; background-color: {getPillColor(items[activeIndex])};"
aria-hidden="true"
></div>
{/if}
{#each items as item, index}
{@const isActive = index === activeIndex}
{@const isHovered = index === hoveredIndex}
{#if variant === 'navigation' && item.href}
<a
bind:this={itemElements[index]}
href={item.href}
class="segmented-item"
class:active={isActive}
role="tab"
aria-selected={isActive}
tabindex={isActive ? 0 : -1}
onmouseenter={() => (hoveredIndex = index)}
onmouseleave={() => (hoveredIndex = -1)}
>
{#if children}
{@render children({ item, index, isActive, isHovered })}
{:else}
<span class="item-label">{item.label}</span>
{/if}
</a>
{:else}
<button
bind:this={itemElements[index]}
type="button"
class="segmented-item"
class:active={isActive}
role="tab"
aria-selected={isActive}
tabindex={isActive ? 0 : -1}
onclick={() => handleItemClick(item, index)}
onmouseenter={() => (hoveredIndex = index)}
onmouseleave={() => (hoveredIndex = -1)}
>
{#if children}
{@render children({ item, index, isActive, isHovered })}
{:else}
<span class="item-label">{item.label}</span>
{/if}
</button>
{/if}
{/each}
</div>
<style lang="scss">
.base-segmented-controller {
display: inline-flex;
align-items: center;
gap: var(--gap);
padding: var(--container-padding);
background-color: $gray-90;
border-radius: $corner-radius-xl;
position: relative;
box-sizing: border-box;
&.full-width {
width: 100%;
.segmented-item {
flex: 1;
}
}
}
.segmented-pill {
position: absolute;
top: var(--container-padding);
bottom: var(--container-padding);
background-color: white;
border-radius: $corner-radius-lg;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: $shadow-sm;
z-index: $z-index-base;
pointer-events: none;
}
.segmented-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
cursor: pointer;
text-decoration: none;
border-radius: $corner-radius-lg;
transition: all 0.2s ease;
z-index: $z-index-above;
font-family: inherit;
outline: none;
&:not(.active):hover {
background-color: rgba(0, 0, 0, 0.05);
}
&:focus-visible {
box-shadow: 0 0 0 2px $blue-50;
}
&.active {
color: $gray-10;
.item-label {
font-weight: 600;
}
}
&:not(.active) {
color: $gray-50;
&:hover {
color: $gray-30;
}
}
}
.item-label {
position: relative;
transition: all 0.2s ease;
white-space: nowrap;
user-select: none;
}
// Size variants
.segmented-controller-small {
.segmented-item {
padding: $unit $unit-2x;
font-size: 0.875rem;
min-height: 32px;
}
}
.segmented-controller-medium {
.segmented-item {
padding: calc($unit + $unit-half) $unit-3x;
font-size: 0.9375rem;
min-height: 40px;
}
}
.segmented-controller-large {
.segmented-item {
padding: $unit-2x $unit-4x;
font-size: 1rem;
min-height: 48px;
}
}
// Animation states
@media (prefers-reduced-motion: reduce) {
.segmented-pill,
.segmented-item {
transition: none;
}
}
</style>

View file

@ -1,126 +0,0 @@
import { writable, derived, get, type Readable } from 'svelte/store'
import { browser } from '$app/environment'
import type { Album } from '$lib/types/lastfm'
interface AlbumStreamState {
connected: boolean
albums: Album[]
lastUpdate: Date | null
}
function createAlbumStream() {
const { subscribe, set, update } = writable<AlbumStreamState>({
connected: false,
albums: [],
lastUpdate: null
})
let eventSource: EventSource | null = null
let reconnectTimeout: NodeJS.Timeout | null = null
let reconnectAttempts = 0
function connect() {
if (!browser || eventSource?.readyState === EventSource.OPEN) return
// Don't connect in Storybook
if (typeof window !== 'undefined' && window.parent !== window) {
// We're in an iframe, likely Storybook
console.log('Album stream disabled in Storybook')
return
}
// Clean up existing connection
disconnect()
eventSource = new EventSource('/api/lastfm/stream')
eventSource.addEventListener('connected', () => {
console.log('Album stream connected')
reconnectAttempts = 0
update((state) => ({ ...state, connected: true }))
})
eventSource.addEventListener('albums', (event) => {
try {
const albums: Album[] = JSON.parse(event.data)
const nowPlayingAlbum = albums.find((a) => a.isNowPlaying)
console.log('Album stream received albums:', {
totalAlbums: albums.length,
nowPlayingAlbum: nowPlayingAlbum
? `${nowPlayingAlbum.artist.name} - ${nowPlayingAlbum.name}`
: 'none'
})
update((state) => ({
...state,
albums,
lastUpdate: new Date()
}))
} catch (error) {
console.error('Error parsing albums update:', error)
}
})
eventSource.addEventListener('heartbeat', () => {
// Heartbeat received, connection is healthy
})
eventSource.addEventListener('error', (error) => {
console.error('Album stream error:', error)
update((state) => ({ ...state, connected: false }))
// Attempt to reconnect with exponential backoff
if (reconnectAttempts < 5) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
reconnectTimeout = setTimeout(() => {
reconnectAttempts++
connect()
}, delay)
}
})
eventSource.addEventListener('open', () => {
update((state) => ({ ...state, connected: true }))
})
}
function disconnect() {
if (eventSource) {
eventSource.close()
eventSource = null
}
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
reconnectTimeout = null
}
update((state) => ({ ...state, connected: false }))
}
// Auto-connect in browser (but not in admin)
if (browser && !window.location.pathname.startsWith('/admin')) {
connect()
// Reconnect on visibility change
document.addEventListener('visibilitychange', () => {
const currentState = get({ subscribe })
if (
document.visibilityState === 'visible' &&
!currentState.connected &&
!window.location.pathname.startsWith('/admin')
) {
connect()
}
})
}
return {
subscribe,
connect,
disconnect,
// Derived store for just the albums
albums: derived({ subscribe }, ($state) => $state.albums) as Readable<Album[]>
}
}
export const albumStream = createAlbumStream()

View file

@ -1,155 +0,0 @@
<script lang="ts">
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import Button from '$lib/components/admin/Button.svelte'
</script>
<AdminPage title="Button Components Demo">
<div class="button-demo">
<section>
<h2>Variants</h2>
<div class="button-group">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="text">Text</Button>
</div>
</section>
<section>
<h2>Sizes</h2>
<div class="button-group">
<Button buttonSize="small">Small</Button>
<Button buttonSize="medium">Medium</Button>
<Button buttonSize="large">Large</Button>
</div>
</section>
<section>
<h2>Icon Buttons</h2>
<div class="button-group">
<Button buttonSize="small" iconOnly>
<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"
/>
</svg>
</Button>
<Button buttonSize="medium" iconOnly>
<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"
/>
</svg>
</Button>
<Button buttonSize="large" iconOnly>
<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"
/>
</svg>
</Button>
<Button buttonSize="icon" iconOnly variant="ghost">
<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"
/>
</svg>
</Button>
</div>
</section>
<section>
<h2>Buttons with Icons</h2>
<div class="button-group">
<Button>
<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"
/>
</svg>
Add Item
</Button>
<Button iconPosition="right" variant="secondary">
Next
<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"
/>
</svg>
</Button>
</div>
</section>
<section>
<h2>States</h2>
<div class="button-group">
<Button disabled>Disabled</Button>
<Button loading>Loading</Button>
<Button active variant="ghost">Active</Button>
</div>
</section>
<section>
<h2>Square Buttons</h2>
<div class="button-group">
<Button pill={false} buttonSize="small">Small Square</Button>
<Button pill={false}>Medium Square</Button>
<Button pill={false} buttonSize="large">Large Square</Button>
</div>
</section>
<section>
<h2>Full Width</h2>
<Button fullWidth>Full Width Button</Button>
</section>
</div>
</AdminPage>
<style lang="scss">
.button-demo {
display: flex;
flex-direction: column;
gap: $unit-4x;
max-width: 800px;
}
section {
display: flex;
flex-direction: column;
gap: $unit-2x;
h2 {
font-size: 18px;
font-weight: 600;
color: $gray-20;
margin: 0;
}
}
.button-group {
display: flex;
align-items: center;
gap: $unit-2x;
flex-wrap: wrap;
}
</style>

View file

@ -1,293 +0,0 @@
<script lang="ts">
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import MediaInput from '$lib/components/admin/MediaInput.svelte'
import ImagePicker from '$lib/components/admin/ImagePicker.svelte'
import GalleryManager from '$lib/components/admin/GalleryManager.svelte'
import Button from '$lib/components/admin/Button.svelte'
import type { Media } from '@prisma/client'
// State for different components
let singleMedia = $state<Media | null>(null)
let multipleMedia = $state<Media[]>([])
let logoImage = $state<Media | null>(null)
let featuredImage = $state<Media | null>(null)
let galleryImages = $state<Media[]>([])
let projectGallery = $state<Media[]>([])
function handleSingleMediaSelect(media: Media | null) {
singleMedia = media
console.log('Single media selected:', media)
}
function handleMultipleMediaSelect(media: Media[]) {
multipleMedia = media
console.log('Multiple media selected:', media)
}
function handleLogoSelect(media: Media | null) {
logoImage = media
console.log('Logo selected:', media)
}
function handleFeaturedImageSelect(media: Media | null) {
featuredImage = media
console.log('Featured image selected:', media)
}
function handleGallerySelect(media: Media[]) {
galleryImages = media
console.log('Gallery images selected:', media)
}
function handleProjectGallerySelect(media: Media[]) {
projectGallery = media
console.log('Project gallery selected:', media)
}
function logAllValues() {
console.log('All form values:', {
singleMedia,
multipleMedia,
logoImage,
featuredImage,
galleryImages,
projectGallery
})
}
function clearAllValues() {
singleMedia = null
multipleMedia = []
logoImage = null
featuredImage = null
galleryImages = []
projectGallery = []
}
</script>
<AdminPage title="Form Components Test" subtitle="Test all media form integration components">
<div class="test-container">
<!-- MediaInput Tests -->
<section class="test-section">
<h2>MediaInput Component</h2>
<p>Generic input component for media selection with preview.</p>
<div class="form-grid">
<MediaInput
label="Single Media File"
bind:value={singleMedia}
mode="single"
fileType="all"
placeholder="Choose any media file"
/>
<MediaInput
label="Multiple Media Files"
bind:value={multipleMedia}
mode="multiple"
fileType="all"
placeholder="Choose multiple files"
/>
<MediaInput
label="Single Image Only"
bind:value={logoImage}
mode="single"
fileType="image"
placeholder="Choose an image"
required={true}
/>
</div>
</section>
<!-- ImagePicker Tests -->
<section class="test-section">
<h2>ImagePicker Component</h2>
<p>Specialized image picker with enhanced preview and aspect ratio support.</p>
<div class="form-grid">
<ImagePicker
label="Featured Image"
bind:value={featuredImage}
aspectRatio="16:9"
placeholder="Select a featured image"
showDimensions={true}
/>
<ImagePicker
label="Square Logo"
bind:value={logoImage}
aspectRatio="1:1"
placeholder="Select a square logo"
required={true}
/>
</div>
</section>
<!-- GalleryManager Tests -->
<section class="test-section">
<h2>GalleryManager Component</h2>
<p>Multiple image management with drag-and-drop reordering.</p>
<div class="form-column">
<GalleryManager label="Image Gallery" bind:value={galleryImages} showFileInfo={false} />
<GalleryManager
label="Project Gallery (Max 6 images)"
bind:value={projectGallery}
maxItems={6}
showFileInfo={true}
/>
</div>
</section>
<!-- Form Actions -->
<section class="test-section">
<h2>Form Actions</h2>
<div class="actions-grid">
<Button variant="primary" onclick={logAllValues}>Log All Values</Button>
<Button variant="ghost" onclick={clearAllValues}>Clear All</Button>
</div>
</section>
<!-- Values Display -->
<section class="test-section">
<h2>Current Values</h2>
<div class="values-display">
<div class="value-item">
<h4>Single Media:</h4>
<pre>{JSON.stringify(singleMedia?.filename || null, null, 2)}</pre>
</div>
<div class="value-item">
<h4>Multiple Media ({multipleMedia.length}):</h4>
<pre>{JSON.stringify(
multipleMedia.map((m) => m.filename),
null,
2
)}</pre>
</div>
<div class="value-item">
<h4>Featured Image:</h4>
<pre>{JSON.stringify(featuredImage?.filename || null, null, 2)}</pre>
</div>
<div class="value-item">
<h4>Gallery Images ({galleryImages.length}):</h4>
<pre>{JSON.stringify(
galleryImages.map((m) => m.filename),
null,
2
)}</pre>
</div>
<div class="value-item">
<h4>Project Gallery ({projectGallery.length}):</h4>
<pre>{JSON.stringify(
projectGallery.map((m) => m.filename),
null,
2
)}</pre>
</div>
</div>
</section>
</div>
</AdminPage>
<style lang="scss">
.test-container {
max-width: 1200px;
margin: 0 auto;
padding: $unit-4x;
}
.test-section {
margin-bottom: $unit-6x;
padding: $unit-4x;
background-color: white;
border-radius: $card-corner-radius;
border: 1px solid $gray-90;
h2 {
margin: 0 0 $unit 0;
font-size: 1.25rem;
font-weight: 600;
color: $gray-10;
}
p {
margin: 0 0 $unit-3x 0;
color: $gray-30;
font-size: 0.875rem;
}
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: $unit-4x;
}
.form-column {
display: flex;
flex-direction: column;
gap: $unit-4x;
}
.actions-grid {
display: flex;
gap: $unit-2x;
justify-content: center;
}
.values-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: $unit-3x;
}
.value-item {
h4 {
margin: 0 0 $unit 0;
font-size: 0.875rem;
font-weight: 600;
color: $gray-20;
}
pre {
background-color: $gray-95;
padding: $unit-2x;
border-radius: $card-corner-radius;
font-size: 0.75rem;
color: $gray-10;
overflow-x: auto;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
}
@media (max-width: 768px) {
.test-container {
padding: $unit-2x;
}
.test-section {
padding: $unit-3x;
}
.form-grid {
grid-template-columns: 1fr;
gap: $unit-3x;
}
.actions-grid {
flex-direction: column;
}
.values-display {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -1,272 +0,0 @@
<script lang="ts">
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import ImageUploader from '$lib/components/admin/ImageUploader.svelte'
import type { Media } from '@prisma/client'
let singleImage = $state<Media | null>(null)
let logoImage = $state<Media | null>(null)
let bannerImage = $state<Media | null>(null)
function handleSingleImageUpload(media: Media) {
singleImage = media
console.log('Single image uploaded:', media)
}
function handleLogoUpload(media: Media) {
logoImage = media
console.log('Logo uploaded:', media)
}
function handleBannerUpload(media: Media) {
bannerImage = media
console.log('Banner uploaded:', media)
}
function logAllValues() {
console.log('All uploaded images:', {
singleImage,
logoImage,
bannerImage
})
}
function clearAll() {
singleImage = null
logoImage = null
bannerImage = null
}
</script>
<AdminPage title="ImageUploader Test" subtitle="Test the new direct upload functionality">
<div class="test-container">
<!-- Basic Image Upload -->
<section class="test-section">
<h2>Basic Image Upload</h2>
<p>Standard image upload with alt text support.</p>
<ImageUploader
label="Featured Image"
bind:value={singleImage}
onUpload={handleSingleImageUpload}
allowAltText={true}
helpText="Upload any image to test the basic functionality."
/>
</section>
<!-- Square Logo Upload -->
<section class="test-section">
<h2>Square Logo Upload</h2>
<p>Image upload with 1:1 aspect ratio constraint.</p>
<ImageUploader
label="Company Logo"
bind:value={logoImage}
onUpload={handleLogoUpload}
aspectRatio="1:1"
allowAltText={true}
required={true}
maxFileSize={2}
helpText="Upload a square logo (1:1 aspect ratio). Max 2MB."
/>
</section>
<!-- Banner Image Upload -->
<section class="test-section">
<h2>Banner Image Upload</h2>
<p>Wide banner image with 16:9 aspect ratio.</p>
<ImageUploader
label="Hero Banner"
bind:value={bannerImage}
onUpload={handleBannerUpload}
aspectRatio="16:9"
allowAltText={true}
showBrowseLibrary={true}
placeholder="Drag and drop a banner image here"
helpText="Recommended size: 1920x1080 pixels for best quality."
/>
</section>
<!-- Form Actions -->
<section class="test-section">
<h2>Actions</h2>
<div class="actions-grid">
<button type="button" class="btn btn-primary" onclick={logAllValues}>
Log All Values
</button>
<button type="button" class="btn btn-ghost" onclick={clearAll}> Clear All </button>
</div>
</section>
<!-- Values Display -->
<section class="test-section">
<h2>Current Values</h2>
<div class="values-display">
<div class="value-item">
<h4>Single Image:</h4>
<pre>{JSON.stringify(
singleImage
? {
id: singleImage.id,
filename: singleImage.filename,
altText: singleImage.altText,
description: singleImage.description
}
: null,
null,
2
)}</pre>
</div>
<div class="value-item">
<h4>Logo Image:</h4>
<pre>{JSON.stringify(
logoImage
? {
id: logoImage.id,
filename: logoImage.filename,
altText: logoImage.altText,
description: logoImage.description
}
: null,
null,
2
)}</pre>
</div>
<div class="value-item">
<h4>Banner Image:</h4>
<pre>{JSON.stringify(
bannerImage
? {
id: bannerImage.id,
filename: bannerImage.filename,
altText: bannerImage.altText,
description: bannerImage.description
}
: null,
null,
2
)}</pre>
</div>
</div>
</section>
</div>
</AdminPage>
<style lang="scss">
.test-container {
max-width: 800px;
margin: 0 auto;
padding: $unit-4x;
}
.test-section {
margin-bottom: $unit-6x;
padding: $unit-4x;
background-color: white;
border-radius: $card-corner-radius;
border: 1px solid $gray-90;
h2 {
margin: 0 0 $unit 0;
font-size: 1.25rem;
font-weight: 600;
color: $gray-10;
}
p {
margin: 0 0 $unit-3x 0;
color: $gray-30;
font-size: 0.875rem;
}
}
.actions-grid {
display: flex;
gap: $unit-2x;
justify-content: center;
}
.values-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: $unit-3x;
}
.value-item {
h4 {
margin: 0 0 $unit 0;
font-size: 0.875rem;
font-weight: 600;
color: $gray-20;
}
pre {
background-color: $gray-95;
padding: $unit-2x;
border-radius: $card-corner-radius;
font-size: 0.75rem;
color: $gray-10;
overflow-x: auto;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: $unit;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
position: relative;
white-space: nowrap;
padding: $unit $unit-2x;
font-size: 14px;
border-radius: 24px;
min-height: 36px;
&.btn-primary {
background-color: $red-60;
color: white;
&:hover {
background-color: $red-80;
}
}
&.btn-ghost {
background-color: transparent;
color: $gray-20;
&:hover {
background-color: $gray-5;
color: $gray-00;
}
}
}
@media (max-width: 768px) {
.test-container {
padding: $unit-2x;
}
.test-section {
padding: $unit-3x;
}
.actions-grid {
flex-direction: column;
}
.values-display {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -1,257 +0,0 @@
<script lang="ts">
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import Input from '$lib/components/admin/Input.svelte'
import Button from '$lib/components/admin/Button.svelte'
let textValue = $state('')
let emailValue = $state('')
let passwordValue = $state('')
let urlValue = $state('https://')
let searchValue = $state('')
let numberValue = $state(0)
let textareaValue = $state('')
let colorValue = $state('#ff0000')
let withErrorValue = $state('')
let disabledValue = $state('Disabled input')
let readonlyValue = $state('Readonly input')
let charLimitValue = $state('')
</script>
<AdminPage title="Input Components Demo">
<div class="input-demo">
<section>
<h2>Basic Inputs</h2>
<div class="input-group">
<Input
label="Text Input"
placeholder="Enter some text"
bind:value={textValue}
helpText="This is a helpful hint"
/>
<Input
type="email"
label="Email Input"
placeholder="email@example.com"
bind:value={emailValue}
required
/>
<Input
type="password"
label="Password Input"
placeholder="Enter password"
bind:value={passwordValue}
required
/>
</div>
</section>
<section>
<h2>Specialized Inputs</h2>
<div class="input-group">
<Input
type="url"
label="URL Input"
placeholder="https://example.com"
bind:value={urlValue}
/>
<Input
type="search"
label="Search Input"
placeholder="Search..."
bind:value={searchValue}
prefixIcon
>
<svg slot="prefix" width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5" />
<path d="M11 11l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</Input>
<Input
type="number"
label="Number Input"
bind:value={numberValue}
min={0}
max={100}
step={5}
/>
<Input type="color" label="Color Input" bind:value={colorValue} />
</div>
</section>
<section>
<h2>Textarea</h2>
<Input
type="textarea"
label="Description"
placeholder="Enter a detailed description..."
bind:value={textareaValue}
rows={4}
helpText="Markdown is supported"
/>
</section>
<section>
<h2>Input Sizes</h2>
<div class="input-group">
<Input buttonSize="small" label="Small Input" placeholder="Small size" />
<Input buttonSize="medium" label="Medium Input" placeholder="Medium size (default)" />
<Input buttonSize="large" label="Large Input" placeholder="Large size" />
</div>
</section>
<section>
<h2>Input States</h2>
<div class="input-group">
<Input
label="Input with Error"
placeholder="Try typing something"
bind:value={withErrorValue}
error={withErrorValue.length > 0 && withErrorValue.length < 3
? 'Too short! Minimum 3 characters'
: ''}
/>
<Input label="Disabled Input" bind:value={disabledValue} disabled />
<Input label="Readonly Input" bind:value={readonlyValue} readonly />
</div>
</section>
<section>
<h2>Input with Icons</h2>
<div class="input-group">
<Input label="With Prefix Icon" placeholder="Username" prefixIcon>
<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" />
<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>
</Input>
<Input label="With Suffix Icon" placeholder="Email" type="email" suffixIcon>
<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"
/>
<path
d="M2 5l6 3 6-3"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
</Input>
</div>
</section>
<section>
<h2>Character Limit</h2>
<Input
label="Bio"
placeholder="Tell us about yourself..."
bind:value={charLimitValue}
maxLength={100}
showCharCount
helpText="Keep it brief"
/>
<Input
type="textarea"
label="Tweet-style Input"
placeholder="What's happening?"
maxLength={280}
showCharCount
rows={3}
/>
</section>
<section>
<h2>Form Example</h2>
<form class="demo-form" on:submit|preventDefault>
<Input label="Project Name" placeholder="My Awesome Project" required />
<Input
type="url"
label="Project URL"
placeholder="https://example.com"
helpText="Include the full URL with https://"
/>
<Input
type="textarea"
label="Project Description"
placeholder="Describe your project..."
rows={4}
maxLength={500}
showCharCount
/>
<div class="form-actions">
<Button variant="secondary">Cancel</Button>
<Button variant="primary" type="submit">Save Project</Button>
</div>
</form>
</section>
</div>
</AdminPage>
<style lang="scss">
.input-demo {
display: flex;
flex-direction: column;
gap: $unit-5x;
max-width: 800px;
}
section {
display: flex;
flex-direction: column;
gap: $unit-3x;
h2 {
font-size: 18px;
font-weight: 600;
color: $gray-20;
margin: 0;
}
}
.input-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: $unit-3x;
}
.demo-form {
display: flex;
flex-direction: column;
gap: $unit-3x;
padding: $unit-3x;
background-color: $gray-97;
border-radius: 8px;
}
.form-actions {
display: flex;
gap: $unit-2x;
justify-content: flex-end;
margin-top: $unit-2x;
}
</style>

View file

@ -1,258 +0,0 @@
<script lang="ts">
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import UnifiedMediaModal from '$lib/components/admin/UnifiedMediaModal.svelte'
import Button from '$lib/components/admin/Button.svelte'
import type { Media } from '@prisma/client'
let showSingleModal = $state(false)
let showMultipleModal = $state(false)
let selectedSingleMedia = $state<Media | null>(null)
let selectedMultipleMedia = $state<Media[]>([])
function handleSingleSelect(media: Media) {
selectedSingleMedia = media
console.log('Single media selected:', media)
}
function handleMultipleSelect(media: Media[]) {
selectedMultipleMedia = media
console.log('Multiple media selected:', media)
}
function openSingleModal() {
showSingleModal = true
}
function openMultipleModal() {
showMultipleModal = true
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
</script>
<AdminPage title="Media Library Test" subtitle="Test the UnifiedMediaModal component">
<div class="test-container">
<section class="test-section">
<h2>Single Selection Mode</h2>
<p>Test selecting a single media item.</p>
<Button variant="primary" onclick={openSingleModal}>Open Single Selection Modal</Button>
{#if selectedSingleMedia}
<div class="selected-media">
<h3>Selected Media:</h3>
<div class="media-preview">
{#if selectedSingleMedia.thumbnailUrl}
<img src={selectedSingleMedia.thumbnailUrl} alt={selectedSingleMedia.filename} />
{/if}
<div class="media-details">
<p><strong>Filename:</strong> {selectedSingleMedia.filename}</p>
<p><strong>Size:</strong> {formatFileSize(selectedSingleMedia.size)}</p>
<p><strong>Type:</strong> {selectedSingleMedia.mimeType}</p>
{#if selectedSingleMedia.width && selectedSingleMedia.height}
<p>
<strong>Dimensions:</strong>
{selectedSingleMedia.width}×{selectedSingleMedia.height}
</p>
{/if}
</div>
</div>
</div>
{/if}
</section>
<section class="test-section">
<h2>Multiple Selection Mode</h2>
<p>Test selecting multiple media items.</p>
<Button variant="primary" onclick={openMultipleModal}>Open Multiple Selection Modal</Button>
{#if selectedMultipleMedia.length > 0}
<div class="selected-media">
<h3>Selected Media ({selectedMultipleMedia.length} items):</h3>
<div class="media-grid">
{#each selectedMultipleMedia as media}
<div class="media-item">
{#if media.thumbnailUrl}
<img src={media.thumbnailUrl} alt={media.filename} />
{/if}
<div class="media-info">
<p class="filename">{media.filename}</p>
<p class="size">{formatFileSize(media.size)}</p>
</div>
</div>
{/each}
</div>
</div>
{/if}
</section>
<section class="test-section">
<h2>Image Only Selection</h2>
<p>Test selecting only image files.</p>
<Button
variant="secondary"
onclick={() => {
showSingleModal = true
// This will be passed to the modal for image-only filtering
}}
>
Open Image Selection Modal
</Button>
</section>
</div>
<!-- Modals -->
<UnifiedMediaModal
bind:isOpen={showSingleModal}
mode="single"
fileType="all"
title="Select a Media File"
confirmText="Select File"
onSelect={handleSingleSelect}
/>
<UnifiedMediaModal
bind:isOpen={showMultipleModal}
mode="multiple"
fileType="all"
title="Select Media Files"
confirmText="Select Files"
onSelect={handleMultipleSelect}
/>
</AdminPage>
<style lang="scss">
.test-container {
max-width: 800px;
margin: 0 auto;
padding: $unit-4x;
}
.test-section {
margin-bottom: $unit-6x;
padding: $unit-4x;
background-color: white;
border-radius: $card-corner-radius;
border: 1px solid $gray-90;
h2 {
margin: 0 0 $unit 0;
font-size: 1.25rem;
font-weight: 600;
color: $gray-10;
}
p {
margin: 0 0 $unit-3x 0;
color: $gray-30;
}
}
.selected-media {
margin-top: $unit-4x;
padding: $unit-3x;
background-color: $gray-95;
border-radius: $card-corner-radius;
h3 {
margin: 0 0 $unit-2x 0;
font-size: 1.125rem;
font-weight: 600;
color: $gray-10;
}
}
.media-preview {
display: flex;
gap: $unit-3x;
align-items: flex-start;
img {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: $card-corner-radius;
border: 1px solid $gray-80;
}
.media-details {
flex: 1;
p {
margin: 0 0 $unit-half 0;
font-size: 0.875rem;
color: $gray-20;
strong {
font-weight: 600;
color: $gray-10;
}
}
}
}
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: $unit-2x;
}
.media-item {
display: flex;
flex-direction: column;
gap: $unit;
img {
width: 100%;
height: 80px;
object-fit: cover;
border-radius: $card-corner-radius;
border: 1px solid $gray-80;
}
.media-info {
.filename {
margin: 0;
font-size: 0.75rem;
font-weight: 500;
color: $gray-10;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.size {
margin: 0;
font-size: 0.625rem;
color: $gray-40;
}
}
}
@media (max-width: 768px) {
.test-container {
padding: $unit-2x;
}
.test-section {
padding: $unit-3x;
}
.media-preview {
flex-direction: column;
img {
width: 100%;
height: 200px;
}
}
}
</style>