Filter updates
This commit is contained in:
parent
5e066093d8
commit
5c32be88c5
7 changed files with 254 additions and 183 deletions
44
src/lib/components/admin/AdminFilters.svelte
Normal file
44
src/lib/components/admin/AdminFilters.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
left?: any
|
||||||
|
right?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
let { left, right }: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="admin-filters">
|
||||||
|
<div class="filters-left">
|
||||||
|
{#if left}
|
||||||
|
{@render left()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="filters-right">
|
||||||
|
{#if right}
|
||||||
|
{@render right()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.admin-filters {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 $unit-2x 0 $unit;
|
||||||
|
margin-bottom: $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-left {
|
||||||
|
display: flex;
|
||||||
|
gap: $unit-2x;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-right {
|
||||||
|
display: flex;
|
||||||
|
gap: $unit-2x;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,22 +1,33 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLInputAttributes, HTMLTextareaAttributes } from 'svelte/elements'
|
import type { HTMLInputAttributes, HTMLTextareaAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
// Type helpers for different input elements
|
// Type helpers for different input elements
|
||||||
type InputProps = HTMLInputAttributes & {
|
type InputProps = HTMLInputAttributes & {
|
||||||
type?: 'text' | 'email' | 'password' | 'url' | 'search' | 'number' | 'tel' | 'date' | 'time' | 'color'
|
type?:
|
||||||
|
| 'text'
|
||||||
|
| 'email'
|
||||||
|
| 'password'
|
||||||
|
| 'url'
|
||||||
|
| 'search'
|
||||||
|
| 'number'
|
||||||
|
| 'tel'
|
||||||
|
| 'date'
|
||||||
|
| 'time'
|
||||||
|
| 'color'
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextareaProps = HTMLTextareaAttributes & {
|
type TextareaProps = HTMLTextareaAttributes & {
|
||||||
type: 'textarea'
|
type: 'textarea'
|
||||||
rows?: number
|
rows?: number
|
||||||
autoResize?: boolean
|
autoResize?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = (InputProps | TextareaProps) & {
|
type Props = (InputProps | TextareaProps) & {
|
||||||
label?: string
|
label?: string
|
||||||
error?: string
|
error?: string
|
||||||
helpText?: string
|
helpText?: string
|
||||||
size?: 'small' | 'medium' | 'large'
|
size?: 'small' | 'medium' | 'large'
|
||||||
|
pill?: boolean
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
required?: boolean
|
required?: boolean
|
||||||
class?: string
|
class?: string
|
||||||
|
|
@ -34,6 +45,7 @@
|
||||||
error,
|
error,
|
||||||
helpText,
|
helpText,
|
||||||
size = 'medium',
|
size = 'medium',
|
||||||
|
pill = false,
|
||||||
fullWidth = true,
|
fullWidth = true,
|
||||||
required = false,
|
required = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
|
@ -56,30 +68,30 @@
|
||||||
let textareaElement: HTMLTextAreaElement | undefined = $state()
|
let textareaElement: HTMLTextAreaElement | undefined = $state()
|
||||||
let charCount = $derived(String(value).length)
|
let charCount = $derived(String(value).length)
|
||||||
let charsRemaining = $derived(maxLength ? maxLength - charCount : 0)
|
let charsRemaining = $derived(maxLength ? maxLength - charCount : 0)
|
||||||
|
|
||||||
// Color swatch validation and display
|
// Color swatch validation and display
|
||||||
const isValidHexColor = $derived(() => {
|
const isValidHexColor = $derived(() => {
|
||||||
if (!colorSwatch || !value) return false
|
if (!colorSwatch || !value) return false
|
||||||
const hexRegex = /^#[0-9A-Fa-f]{6}$/
|
const hexRegex = /^#[0-9A-Fa-f]{6}$/
|
||||||
return hexRegex.test(String(value))
|
return hexRegex.test(String(value))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Color picker functionality
|
// Color picker functionality
|
||||||
let colorPickerInput: HTMLInputElement
|
let colorPickerInput: HTMLInputElement
|
||||||
|
|
||||||
function handleColorSwatchClick() {
|
function handleColorSwatchClick() {
|
||||||
if (colorPickerInput) {
|
if (colorPickerInput) {
|
||||||
colorPickerInput.click()
|
colorPickerInput.click()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleColorPickerChange(event: Event) {
|
function handleColorPickerChange(event: Event) {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
if (target.value) {
|
if (target.value) {
|
||||||
value = target.value.toUpperCase()
|
value = target.value.toUpperCase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-resize textarea
|
// Auto-resize textarea
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (type === 'textarea' && textareaElement && isTextarea(restProps) && restProps.autoResize) {
|
if (type === 'textarea' && textareaElement && isTextarea(restProps) && restProps.autoResize) {
|
||||||
|
|
@ -100,7 +112,8 @@
|
||||||
if (prefixIcon) classes.push('has-prefix-icon')
|
if (prefixIcon) classes.push('has-prefix-icon')
|
||||||
if (suffixIcon) classes.push('has-suffix-icon')
|
if (suffixIcon) classes.push('has-suffix-icon')
|
||||||
if (colorSwatch) classes.push('has-color-swatch')
|
if (colorSwatch) classes.push('has-color-swatch')
|
||||||
if (type === 'textarea' && isTextarea(restProps) && restProps.autoResize) classes.push('has-auto-resize')
|
if (type === 'textarea' && isTextarea(restProps) && restProps.autoResize)
|
||||||
|
classes.push('has-auto-resize')
|
||||||
if (wrapperClass) classes.push(wrapperClass)
|
if (wrapperClass) classes.push(wrapperClass)
|
||||||
if (className) classes.push(className)
|
if (className) classes.push(className)
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
|
|
@ -109,6 +122,7 @@
|
||||||
const inputClasses = $derived(() => {
|
const inputClasses = $derived(() => {
|
||||||
const classes = ['input']
|
const classes = ['input']
|
||||||
classes.push(`input-${size}`)
|
classes.push(`input-${size}`)
|
||||||
|
if (pill) classes.push('input-pill')
|
||||||
if (inputClass) classes.push(inputClass)
|
if (inputClass) classes.push(inputClass)
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
})
|
})
|
||||||
|
|
@ -128,17 +142,17 @@
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
{#if prefixIcon}
|
{#if prefixIcon}
|
||||||
<span class="input-icon prefix-icon">
|
<span class="input-icon prefix-icon">
|
||||||
<slot name="prefix" />
|
<slot name="prefix" />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if colorSwatch && isValidHexColor}
|
{#if colorSwatch && isValidHexColor}
|
||||||
<span
|
<span
|
||||||
class="color-swatch"
|
class="color-swatch"
|
||||||
style="background-color: {value}"
|
style="background-color: {value}"
|
||||||
onclick={handleColorSwatchClick}
|
onclick={handleColorSwatchClick}
|
||||||
role="button"
|
role="button"
|
||||||
|
|
@ -146,7 +160,7 @@
|
||||||
aria-label="Open color picker"
|
aria-label="Open color picker"
|
||||||
></span>
|
></span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if type === 'textarea' && isTextarea(restProps)}
|
{#if type === 'textarea' && isTextarea(restProps)}
|
||||||
<textarea
|
<textarea
|
||||||
bind:this={textareaElement}
|
bind:this={textareaElement}
|
||||||
|
|
@ -173,13 +187,13 @@
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if suffixIcon}
|
{#if suffixIcon}
|
||||||
<span class="input-icon suffix-icon">
|
<span class="input-icon suffix-icon">
|
||||||
<slot name="suffix" />
|
<slot name="suffix" />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if colorSwatch}
|
{#if colorSwatch}
|
||||||
<input
|
<input
|
||||||
bind:this={colorPickerInput}
|
bind:this={colorPickerInput}
|
||||||
|
|
@ -192,7 +206,7 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (error || helpText || showCharCount) && !disabled}
|
{#if (error || helpText || showCharCount) && !disabled}
|
||||||
<div class="input-footer">
|
<div class="input-footer">
|
||||||
{#if error}
|
{#if error}
|
||||||
|
|
@ -200,9 +214,13 @@
|
||||||
{:else if helpText}
|
{:else if helpText}
|
||||||
<span class="input-help">{helpText}</span>
|
<span class="input-help">{helpText}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCharCount && maxLength}
|
{#if showCharCount && maxLength}
|
||||||
<span class="char-count" class:warning={charsRemaining < maxLength * 0.1} class:error={charsRemaining < 0}>
|
<span
|
||||||
|
class="char-count"
|
||||||
|
class:warning={charsRemaining < maxLength * 0.1}
|
||||||
|
class:error={charsRemaining < 0}
|
||||||
|
>
|
||||||
{charsRemaining}
|
{charsRemaining}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -217,7 +235,7 @@
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.full-width {
|
&.full-width {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -226,7 +244,7 @@
|
||||||
&.has-error {
|
&.has-error {
|
||||||
.input {
|
.input {
|
||||||
border-color: $red-50;
|
border-color: $red-50;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: $red-50;
|
border-color: $red-50;
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +311,7 @@
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: $grey-50;
|
color: $grey-50;
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +350,31 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shape variants - pill vs rounded
|
||||||
|
.input-pill {
|
||||||
|
&.input-small {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
&.input-medium {
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
&.input-large {
|
||||||
|
border-radius: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:not(.input-pill) {
|
||||||
|
&.input-small {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
&.input-medium {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
&.input-large {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Icon adjustments
|
// Icon adjustments
|
||||||
.has-prefix-icon .input {
|
.has-prefix-icon .input {
|
||||||
padding-left: calc($unit-2x + 24px);
|
padding-left: calc($unit-2x + 24px);
|
||||||
|
|
@ -350,11 +393,11 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: $grey-40;
|
color: $grey-40;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&.prefix-icon {
|
&.prefix-icon {
|
||||||
left: $unit-2x;
|
left: $unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.suffix-icon {
|
&.suffix-icon {
|
||||||
right: $unit-2x;
|
right: $unit-2x;
|
||||||
}
|
}
|
||||||
|
|
@ -373,18 +416,18 @@
|
||||||
padding-bottom: calc($unit * 1.5);
|
padding-bottom: calc($unit * 1.5);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow-y: hidden; // Important for auto-resize
|
overflow-y: hidden; // Important for auto-resize
|
||||||
|
|
||||||
&.input-small {
|
&.input-small {
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
padding-top: $unit;
|
padding-top: $unit;
|
||||||
padding-bottom: $unit;
|
padding-bottom: $unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.input-large {
|
&.input-large {
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-resizing textarea
|
// Auto-resizing textarea
|
||||||
.has-auto-resize textarea.input {
|
.has-auto-resize textarea.input {
|
||||||
resize: none; // Disable manual resize when auto-resize is enabled
|
resize: none; // Disable manual resize when auto-resize is enabled
|
||||||
|
|
@ -418,11 +461,11 @@
|
||||||
color: $grey-50;
|
color: $grey-50;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
color: $universe-color;
|
color: $universe-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
color: $red-50;
|
color: $red-50;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -430,23 +473,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special input types
|
// Special input types
|
||||||
input[type="color"].input {
|
input[type='color'].input {
|
||||||
padding: $unit;
|
padding: $unit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&::-webkit-color-swatch-wrapper {
|
&::-webkit-color-swatch-wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-color-swatch {
|
&::-webkit-color-swatch {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"].input {
|
input[type='number'].input {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
|
|
||||||
&::-webkit-outer-spin-button,
|
&::-webkit-outer-spin-button,
|
||||||
&::-webkit-inner-spin-button {
|
&::-webkit-inner-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
@ -455,10 +498,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search input
|
// Search input
|
||||||
input[type="search"].input {
|
input[type='search'].input {
|
||||||
&::-webkit-search-decoration,
|
&::-webkit-search-decoration,
|
||||||
&::-webkit-search-cancel-button {
|
&::-webkit-search-cancel-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
border-radius: 8px;
|
border-radius: $corner-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.select-medium {
|
&.select-medium {
|
||||||
|
|
@ -144,7 +144,7 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
border-radius: 8px;
|
border-radius: $corner-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.select-large {
|
&.select-large {
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
border-radius: 8px;
|
border-radius: $card-corner-radius;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||||
|
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
|
||||||
import DataTable from '$lib/components/admin/DataTable.svelte'
|
import DataTable from '$lib/components/admin/DataTable.svelte'
|
||||||
import Button from '$lib/components/admin/Button.svelte'
|
import Button from '$lib/components/admin/Button.svelte'
|
||||||
import Select from '$lib/components/admin/Select.svelte'
|
import Select from '$lib/components/admin/Select.svelte'
|
||||||
|
|
@ -145,15 +146,17 @@
|
||||||
<div class="error">{error}</div>
|
<div class="error">{error}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="filters">
|
<AdminFilters>
|
||||||
<Select
|
{#snippet left()}
|
||||||
bind:value={photographyFilter}
|
<Select
|
||||||
options={filterOptions}
|
bind:value={photographyFilter}
|
||||||
size="small"
|
options={filterOptions}
|
||||||
variant="minimal"
|
size="small"
|
||||||
onchange={handleFilterChange}
|
variant="minimal"
|
||||||
/>
|
onchange={handleFilterChange}
|
||||||
</div>
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</AdminFilters>
|
||||||
|
|
||||||
<!-- Albums Table -->
|
<!-- Albums Table -->
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
|
|
@ -185,12 +188,6 @@
|
||||||
margin-bottom: $unit-4x;
|
margin-bottom: $unit-4x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-2x;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: $unit-4x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||||
|
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||||
|
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
|
||||||
import Input from '$lib/components/admin/Input.svelte'
|
import Input from '$lib/components/admin/Input.svelte'
|
||||||
|
import Select from '$lib/components/admin/Select.svelte'
|
||||||
|
import Button from '$lib/components/admin/Button.svelte'
|
||||||
import MediaDetailsModal from '$lib/components/admin/MediaDetailsModal.svelte'
|
import MediaDetailsModal from '$lib/components/admin/MediaDetailsModal.svelte'
|
||||||
import type { Media } from '@prisma/client'
|
import type { Media } from '@prisma/client'
|
||||||
|
|
||||||
|
|
@ -19,6 +23,21 @@
|
||||||
let searchQuery = $state('')
|
let searchQuery = $state('')
|
||||||
let searchTimeout: ReturnType<typeof setTimeout>
|
let searchTimeout: ReturnType<typeof setTimeout>
|
||||||
|
|
||||||
|
// Filter options
|
||||||
|
const typeFilterOptions = [
|
||||||
|
{ value: 'all', label: 'All types' },
|
||||||
|
{ value: 'image', label: 'Images' },
|
||||||
|
{ value: 'video', label: 'Videos' },
|
||||||
|
{ value: 'audio', label: 'Audio' },
|
||||||
|
{ value: 'application/pdf', label: 'PDFs' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const photographyFilterOptions = [
|
||||||
|
{ value: 'all', label: 'All media' },
|
||||||
|
{ value: 'true', label: 'Photography only' },
|
||||||
|
{ value: 'false', label: 'Non-photography' }
|
||||||
|
]
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
let selectedMedia = $state<Media | null>(null)
|
let selectedMedia = $state<Media | null>(null)
|
||||||
let isDetailsModalOpen = $state(false)
|
let isDetailsModalOpen = $state(false)
|
||||||
|
|
@ -286,47 +305,51 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AdminPage>
|
<AdminPage>
|
||||||
<header slot="header">
|
<AdminHeader title="Media Library" slot="header">
|
||||||
<h1>Media Library</h1>
|
{#snippet actions()}
|
||||||
<div class="header-actions">
|
<Button
|
||||||
<button
|
variant="secondary"
|
||||||
|
size="large"
|
||||||
onclick={toggleMultiSelectMode}
|
onclick={toggleMultiSelectMode}
|
||||||
class="btn btn-secondary"
|
class={isMultiSelectMode ? 'active' : ''}
|
||||||
class:active={isMultiSelectMode}
|
|
||||||
>
|
>
|
||||||
{isMultiSelectMode ? '✓' : '☐'}
|
{isMultiSelectMode ? '✓' : '☐'}
|
||||||
{isMultiSelectMode ? 'Exit Select' : 'Select'}
|
{isMultiSelectMode ? 'Exit Select' : 'Select'}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="large"
|
||||||
onclick={() => (viewMode = viewMode === 'grid' ? 'list' : 'grid')}
|
onclick={() => (viewMode = viewMode === 'grid' ? 'list' : 'grid')}
|
||||||
class="btn btn-secondary"
|
|
||||||
>
|
>
|
||||||
{viewMode === 'grid' ? '📋' : '🖼️'}
|
{viewMode === 'grid' ? '📋' : '🖼️'}
|
||||||
{viewMode === 'grid' ? 'List' : 'Grid'}
|
{viewMode === 'grid' ? 'List' : 'Grid'}
|
||||||
</button>
|
</Button>
|
||||||
<a href="/admin/media/upload" class="btn btn-primary">Upload Media</a>
|
<Button variant="primary" size="large" href="/admin/media/upload">Upload Media</Button>
|
||||||
</div>
|
{/snippet}
|
||||||
</header>
|
</AdminHeader>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="error">{error}</div>
|
<div class="error">{error}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="media-controls">
|
<!-- Filters -->
|
||||||
<div class="filters">
|
<AdminFilters>
|
||||||
<select bind:value={filterType} onchange={handleFilterChange} class="filter-select">
|
{#snippet left()}
|
||||||
<option value="all">All types</option>
|
<Select
|
||||||
<option value="image">Images</option>
|
bind:value={filterType}
|
||||||
<option value="video">Videos</option>
|
options={typeFilterOptions}
|
||||||
<option value="audio">Audio</option>
|
size="small"
|
||||||
<option value="application/pdf">PDFs</option>
|
variant="minimal"
|
||||||
</select>
|
onchange={handleFilterChange}
|
||||||
|
/>
|
||||||
<select bind:value={photographyFilter} onchange={handleFilterChange} class="filter-select">
|
<Select
|
||||||
<option value="all">All media</option>
|
bind:value={photographyFilter}
|
||||||
<option value="true">Photography only</option>
|
options={photographyFilterOptions}
|
||||||
<option value="false">Non-photography</option>
|
size="small"
|
||||||
</select>
|
variant="minimal"
|
||||||
|
onchange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet right()}
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
|
|
@ -334,8 +357,8 @@
|
||||||
placeholder="Search files..."
|
placeholder="Search files..."
|
||||||
size="small"
|
size="small"
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
|
pill={true}
|
||||||
prefixIcon
|
prefixIcon
|
||||||
wrapperClass="search-input-wrapper"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
slot="prefix"
|
slot="prefix"
|
||||||
|
|
@ -352,8 +375,8 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Input>
|
</Input>
|
||||||
</div>
|
{/snippet}
|
||||||
</div>
|
</AdminFilters>
|
||||||
|
|
||||||
{#if isMultiSelectMode && media.length > 0}
|
{#if isMultiSelectMode && media.length > 0}
|
||||||
<div class="bulk-actions">
|
<div class="bulk-actions">
|
||||||
|
|
@ -614,25 +637,6 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0;
|
|
||||||
color: $grey-10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-2x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: $unit-2x $unit-3x;
|
padding: $unit-2x $unit-3x;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
|
|
@ -668,44 +672,6 @@
|
||||||
color: #d33;
|
color: #d33;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: $unit-4x;
|
|
||||||
margin-bottom: $unit-4x;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-2x;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select {
|
|
||||||
padding: $unit $unit-3x;
|
|
||||||
border: 1px solid $grey-80;
|
|
||||||
border-radius: 50px;
|
|
||||||
background: white;
|
|
||||||
font-size: 0.925rem;
|
|
||||||
color: $grey-20;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: $grey-40;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper {
|
|
||||||
width: 240px;
|
|
||||||
|
|
||||||
:global(.input) {
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $unit-6x;
|
padding: $unit-6x;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||||
|
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
|
||||||
import PostDropdown from '$lib/components/admin/PostDropdown.svelte'
|
import PostDropdown from '$lib/components/admin/PostDropdown.svelte'
|
||||||
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
|
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
|
||||||
import Select from '$lib/components/admin/Select.svelte'
|
import Select from '$lib/components/admin/Select.svelte'
|
||||||
|
|
@ -239,15 +240,17 @@
|
||||||
<div class="error-message">{error}</div>
|
<div class="error-message">{error}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="filters">
|
<AdminFilters>
|
||||||
<Select
|
{#snippet left()}
|
||||||
bind:value={selectedFilter}
|
<Select
|
||||||
options={filterOptions}
|
bind:value={selectedFilter}
|
||||||
size="small"
|
options={filterOptions}
|
||||||
variant="minimal"
|
size="small"
|
||||||
onchange={handleFilterChange}
|
variant="minimal"
|
||||||
/>
|
onchange={handleFilterChange}
|
||||||
</div>
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</AdminFilters>
|
||||||
|
|
||||||
<!-- Posts List -->
|
<!-- Posts List -->
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
|
|
@ -364,12 +367,6 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '$styles/variables.scss';
|
@import '$styles/variables.scss';
|
||||||
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-2x;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: $unit-4x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||||
|
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
|
||||||
import ProjectListItem from '$lib/components/admin/ProjectListItem.svelte'
|
import ProjectListItem from '$lib/components/admin/ProjectListItem.svelte'
|
||||||
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
|
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
|
||||||
import Button from '$lib/components/admin/Button.svelte'
|
import Button from '$lib/components/admin/Button.svelte'
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
year: number
|
year: number
|
||||||
client: string | null
|
client: string | null
|
||||||
status: string
|
status: string
|
||||||
|
projectType: string
|
||||||
backgroundColor: string | null
|
backgroundColor: string | null
|
||||||
highlightColor: string | null
|
highlightColor: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
|
@ -31,15 +33,22 @@
|
||||||
let statusCounts = $state<Record<string, number>>({})
|
let statusCounts = $state<Record<string, number>>({})
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
let selectedFilter = $state<string>('all')
|
let selectedStatusFilter = $state<string>('all')
|
||||||
|
let selectedTypeFilter = $state<string>('all')
|
||||||
|
|
||||||
// Create filter options
|
// Create filter options
|
||||||
const filterOptions = $derived([
|
const statusFilterOptions = $derived([
|
||||||
{ value: 'all', label: 'All projects' },
|
{ value: 'all', label: 'All projects' },
|
||||||
{ value: 'published', label: 'Published' },
|
{ value: 'published', label: 'Published' },
|
||||||
{ value: 'draft', label: 'Draft' }
|
{ value: 'draft', label: 'Draft' }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const typeFilterOptions = [
|
||||||
|
{ value: 'all', label: 'All types' },
|
||||||
|
{ value: 'work', label: 'Work' },
|
||||||
|
{ value: 'labs', label: 'Labs' }
|
||||||
|
]
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadProjects()
|
await loadProjects()
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
|
|
@ -167,14 +176,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyFilter() {
|
function applyFilter() {
|
||||||
if (selectedFilter === 'all') {
|
let filtered = projects
|
||||||
filteredProjects = projects
|
|
||||||
} else {
|
// Apply status filter
|
||||||
filteredProjects = projects.filter((project) => project.status === selectedFilter)
|
if (selectedStatusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter((project) => project.status === selectedStatusFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply type filter based on projectType field
|
||||||
|
if (selectedTypeFilter !== 'all') {
|
||||||
|
filtered = filtered.filter((project) => project.projectType === selectedTypeFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredProjects = filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFilterChange() {
|
function handleStatusFilterChange() {
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTypeFilterChange() {
|
||||||
applyFilter()
|
applyFilter()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -190,15 +211,24 @@
|
||||||
<div class="error">{error}</div>
|
<div class="error">{error}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="filters">
|
<AdminFilters>
|
||||||
<Select
|
{#snippet left()}
|
||||||
bind:value={selectedFilter}
|
<Select
|
||||||
options={filterOptions}
|
bind:value={selectedStatusFilter}
|
||||||
size="small"
|
options={statusFilterOptions}
|
||||||
variant="minimal"
|
size="small"
|
||||||
onchange={handleFilterChange}
|
variant="minimal"
|
||||||
/>
|
onchange={handleStatusFilterChange}
|
||||||
</div>
|
/>
|
||||||
|
<Select
|
||||||
|
bind:value={selectedTypeFilter}
|
||||||
|
options={typeFilterOptions}
|
||||||
|
size="small"
|
||||||
|
variant="minimal"
|
||||||
|
onchange={handleTypeFilterChange}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</AdminFilters>
|
||||||
|
|
||||||
<!-- Projects List -->
|
<!-- Projects List -->
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
|
|
@ -209,10 +239,10 @@
|
||||||
{:else if filteredProjects.length === 0}
|
{:else if filteredProjects.length === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>
|
<p>
|
||||||
{#if selectedFilter === 'all'}
|
{#if selectedStatusFilter === 'all' && selectedTypeFilter === 'all'}
|
||||||
No projects found. Create your first project!
|
No projects found. Create your first project!
|
||||||
{:else}
|
{:else}
|
||||||
No {selectedFilter} projects found. Try a different filter or create a new project.
|
No projects found matching the current filters. Try adjusting your filters or create a new project.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -245,12 +275,6 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-2x;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: $unit-4x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue