lib was accidentally being ignored
Some checks failed
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 3s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 3s
CI/CD Pipeline / VM Test - performance (push) Successful in 2s
CI/CD Pipeline / VM Test - security (push) Successful in 2s
CI/CD Pipeline / Backend Linting (push) Successful in 2s
CI/CD Pipeline / Frontend Linting (push) Failing after 12s
CI/CD Pipeline / Nix Flake Check (push) Successful in 37s
CI/CD Pipeline / CI Summary (push) Failing after 0s

This commit is contained in:
Danilo Reyes
2025-11-02 12:23:25 -06:00
parent 681fa0903b
commit c52ac86739
34 changed files with 4164 additions and 6 deletions

View File

@@ -0,0 +1,206 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import type { BoardSummary } from '$lib/types/boards';
export let board: BoardSummary;
const dispatch = createEventDispatcher<{ delete: void }>();
function openBoard() {
goto(`/boards/${board.id}`);
}
function handleDelete(event: MouseEvent) {
event.stopPropagation();
dispatch('delete');
}
function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<article
class="board-card"
on:click={openBoard}
on:keydown={(e) => e.key === 'Enter' && openBoard()}
tabindex="0"
>
<div class="card-thumbnail">
{#if board.thumbnail_url}
<img src={board.thumbnail_url} alt={board.title} />
{:else}
<div class="placeholder-thumbnail">
<span class="placeholder-icon">🖼️</span>
</div>
{/if}
{#if board.image_count > 0}
<div class="image-count">
{board.image_count}
{board.image_count === 1 ? 'image' : 'images'}
</div>
{/if}
</div>
<div class="card-content">
<h3 class="board-title">{board.title}</h3>
{#if board.description}
<p class="board-description">{board.description}</p>
{/if}
<div class="card-meta">
<span class="meta-date">Updated {formatDate(board.updated_at)}</span>
</div>
</div>
<div class="card-actions">
<button
class="action-btn delete-btn"
on:click={handleDelete}
title="Delete board"
aria-label="Delete board"
>
🗑️
</button>
</div>
</article>
<style>
.board-card {
position: relative;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
}
.board-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
border-color: #667eea;
}
.board-card:focus {
outline: 2px solid #667eea;
outline-offset: 2px;
}
.card-thumbnail {
position: relative;
width: 100%;
height: 180px;
background: #f3f4f6;
overflow: hidden;
}
.card-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.placeholder-thumbnail {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%);
}
.placeholder-icon {
font-size: 3rem;
opacity: 0.3;
}
.image-count {
position: absolute;
bottom: 0.75rem;
right: 0.75rem;
padding: 0.25rem 0.75rem;
background: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.card-content {
padding: 1rem;
}
.board-title {
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 0.5rem 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.board-description {
font-size: 0.875rem;
color: #6b7280;
margin: 0 0 0.75rem 0;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-meta {
display: flex;
align-items: center;
justify-content: space-between;
}
.meta-date {
font-size: 0.75rem;
color: #9ca3af;
}
.card-actions {
position: absolute;
top: 0.75rem;
right: 0.75rem;
display: flex;
gap: 0.5rem;
opacity: 0;
transition: opacity 0.2s;
}
.board-card:hover .card-actions {
opacity: 1;
}
.action-btn {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: rgba(255, 255, 255, 0.95);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.action-btn:hover {
transform: scale(1.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.delete-btn:hover {
background: #fee2e2;
}
</style>

View File

@@ -0,0 +1,266 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let initialTitle: string = '';
export let initialDescription: string = '';
let title = initialTitle;
let description = initialDescription;
let errors: Record<string, string> = {};
const dispatch = createEventDispatcher<{
create: { title: string; description?: string };
close: void;
}>();
function validate(): boolean {
errors = {};
if (!title.trim()) {
errors.title = 'Title is required';
} else if (title.length > 255) {
errors.title = 'Title must be 255 characters or less';
}
if (description.length > 1000) {
errors.description = 'Description must be 1000 characters or less';
}
return Object.keys(errors).length === 0;
}
function handleSubmit() {
if (!validate()) return;
dispatch('create', {
title: title.trim(),
description: description.trim() || undefined,
});
}
function handleClose() {
dispatch('close');
}
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) {
handleClose();
}
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div
class="modal-backdrop"
on:click={handleBackdropClick}
on:keydown={(e) => e.key === 'Escape' && handleClose()}
role="dialog"
aria-modal="true"
tabindex="-1"
>
<div class="modal-content">
<header class="modal-header">
<h2>Create New Board</h2>
<button class="close-btn" on:click={handleClose} aria-label="Close">×</button>
</header>
<form on:submit|preventDefault={handleSubmit} class="modal-form">
<div class="form-group">
<label for="title">Board Title <span class="required">*</span></label>
<input
id="title"
type="text"
bind:value={title}
placeholder="e.g., Character Design References"
class:error={errors.title}
maxlength="255"
required
/>
{#if errors.title}
<span class="error-text">{errors.title}</span>
{/if}
</div>
<div class="form-group">
<label for="description">Description (optional)</label>
<textarea
id="description"
bind:value={description}
placeholder="Add a description for this board..."
rows="3"
maxlength="1000"
class:error={errors.description}
/>
{#if errors.description}
<span class="error-text">{errors.description}</span>
{:else}
<span class="help-text">{description.length}/1000 characters</span>
{/if}
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" on:click={handleClose}>Cancel</button>
<button type="submit" class="btn-primary">Create Board</button>
</div>
</form>
</div>
</div>
<style>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal-content {
background: white;
border-radius: 16px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.modal-header h2 {
font-size: 1.5rem;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 2rem;
color: #6b7280;
cursor: pointer;
padding: 0;
line-height: 1;
transition: color 0.2s;
}
.close-btn:hover {
color: #1f2937;
}
.modal-form {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group:last-of-type {
margin-bottom: 2rem;
}
label {
display: block;
font-weight: 500;
color: #374151;
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.required {
color: #ef4444;
}
input,
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: all 0.2s;
}
input:focus,
textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
input.error,
textarea.error {
border-color: #ef4444;
}
textarea {
resize: vertical;
min-height: 80px;
}
.error-text {
display: block;
color: #ef4444;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.help-text {
display: block;
color: #6b7280;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-primary,
.btn-secondary {
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-secondary:hover {
background: #f9fafb;
}
</style>