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:
parent
bc102fba0a
commit
d964bf05cd
9 changed files with 0 additions and 2010 deletions
|
|
@ -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>
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
Loading…
Reference in a new issue