Filter updates

This commit is contained in:
Justin Edmund 2025-06-02 02:50:08 -07:00
parent 5e066093d8
commit 5c32be88c5
7 changed files with 254 additions and 183 deletions

View 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>

View file

@ -3,7 +3,17 @@
// 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 & {
@ -17,6 +27,7 @@
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,
@ -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(' ')
}) })
@ -202,7 +216,11 @@
{/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}
@ -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);
@ -430,7 +473,7 @@
} }
// Special input types // Special input types
input[type="color"].input { input[type='color'].input {
padding: $unit; padding: $unit;
cursor: pointer; cursor: pointer;
@ -444,7 +487,7 @@
} }
} }
input[type="number"].input { input[type='number'].input {
-moz-appearance: textfield; -moz-appearance: textfield;
&::-webkit-outer-spin-button, &::-webkit-outer-spin-button,
@ -455,7 +498,7 @@
} }
// 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;

View file

@ -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;
} }
} }

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;