refactor: remove 5 unused components

- Remove Squiggly.svelte
- Remove PhotoLightbox.svelte
- Remove Pill.svelte
- Remove SVGHoverEffect.svelte
- Remove MusicPreview.svelte

These components were identified as unused in the codebase analysis.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-06-25 21:00:49 -04:00
parent d4caad10a3
commit ddfe1cac8f
6 changed files with 6 additions and 971 deletions

View file

@ -56,12 +56,12 @@ This PRD outlines a comprehensive cleanup and refactoring plan for the jedmund-s
### Phase 1: Quick Wins (Week 1)
Focus on low-risk, high-impact changes that don't require architectural modifications.
- [ ] **Remove unused components** (5 components)
- [ ] Delete `/src/lib/components/Squiggly.svelte`
- [ ] Delete `/src/lib/components/PhotoLightbox.svelte`
- [ ] Delete `/src/lib/components/Pill.svelte`
- [ ] Delete `/src/lib/components/SVGHoverEffect.svelte`
- [ ] Delete `/src/lib/components/MusicPreview.svelte`
- [x] **Remove unused components** (5 components)
- [x] Delete `/src/lib/components/Squiggly.svelte`
- [x] Delete `/src/lib/components/PhotoLightbox.svelte`
- [x] Delete `/src/lib/components/Pill.svelte`
- [x] Delete `/src/lib/components/SVGHoverEffect.svelte`
- [x] Delete `/src/lib/components/MusicPreview.svelte`
- [ ] **Remove unused SVG files** (13 files)
- [ ] Delete unused icons: `close.svg`, `music.svg`

View file

@ -1,350 +0,0 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import { fade } from 'svelte/transition'
interface Props {
previewUrl: string
albumName?: string
artistName?: string
onPlayStateChange?: (isPlaying: boolean) => void
}
let { previewUrl, albumName = '', artistName = '', onPlayStateChange }: Props = $props()
let audio: HTMLAudioElement | null = $state(null)
let isPlaying = $state(false)
let isLoading = $state(false)
let currentTime = $state(0)
let duration = $state(30) // Apple Music previews are 30 seconds
let volume = $state(1)
let hasError = $state(false)
$effect(() => {
if (audio) {
audio.volume = volume
}
})
$effect(() => {
onPlayStateChange?.(isPlaying)
})
onMount(() => {
// Listen for other audio elements playing
const handleAudioPlay = (e: Event) => {
const playingAudio = e.target as HTMLAudioElement
if (playingAudio !== audio && audio && !audio.paused) {
pause()
}
}
document.addEventListener('play', handleAudioPlay, true)
return () => {
document.removeEventListener('play', handleAudioPlay, true)
}
})
onDestroy(() => {
if (audio) {
audio.pause()
audio = null
}
})
function togglePlayPause() {
if (isPlaying) {
pause()
} else {
play()
}
}
async function play() {
if (!audio || hasError) return
isLoading = true
try {
await audio.play()
isPlaying = true
} catch (error) {
console.error('Failed to play preview:', error)
hasError = true
} finally {
isLoading = false
}
}
function pause() {
if (!audio) return
audio.pause()
isPlaying = false
}
function handleTimeUpdate() {
if (audio) {
currentTime = audio.currentTime
}
}
function handleEnded() {
isPlaying = false
currentTime = 0
if (audio) {
audio.currentTime = 0
}
}
function handleError() {
hasError = true
isPlaying = false
isLoading = false
}
function handleLoadedMetadata() {
if (audio) {
duration = audio.duration
}
}
function seek(e: MouseEvent) {
const target = e.currentTarget as HTMLElement
const rect = target.getBoundingClientRect()
const x = e.clientX - rect.left
const percentage = x / rect.width
const newTime = percentage * duration
if (audio) {
audio.currentTime = newTime
currentTime = newTime
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.code === 'Space') {
e.preventDefault()
togglePlayPause()
}
}
function formatTime(time: number): string {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
const progressPercentage = $derived((currentTime / duration) * 100)
</script>
<div class="music-preview" role="region" aria-label="Music preview player">
<audio
bind:this={audio}
src={previewUrl}
onloadedmetadata={handleLoadedMetadata}
ontimeupdate={handleTimeUpdate}
onended={handleEnded}
onerror={handleError}
preload="metadata"
/>
<div class="controls">
<button
class="play-button"
onclick={togglePlayPause}
onkeydown={handleKeydown}
disabled={hasError || isLoading}
aria-label={isPlaying ? 'Pause' : 'Play'}
aria-pressed={isPlaying}
>
{#if isLoading}
<span class="loading-spinner" aria-hidden="true"></span>
{:else if hasError}
<span aria-hidden="true"></span>
{:else if isPlaying}
<span aria-hidden="true">❚❚</span>
{:else}
<span aria-hidden="true"></span>
{/if}
</button>
<div class="progress-container">
<div
class="progress-bar"
onclick={seek}
role="slider"
aria-label="Seek"
aria-valuemin={0}
aria-valuemax={duration}
aria-valuenow={currentTime}
tabindex="0"
>
<div class="progress-fill" style="width: {progressPercentage}%"></div>
</div>
<div class="time-display">
<span>{formatTime(currentTime)}</span>
<span>/</span>
<span>{formatTime(duration)}</span>
</div>
</div>
<div class="volume-control">
<label for="volume" class="visually-hidden">Volume</label>
<input
id="volume"
type="range"
bind:value={volume}
min="0"
max="1"
step="0.1"
aria-label="Volume control"
/>
</div>
</div>
{#if hasError}
<p class="error-message" transition:fade={{ duration: 200 }}>Preview unavailable</p>
{/if}
</div>
<style lang="scss">
.music-preview {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
padding: var(--spacing-sm);
background: var(--color-surface);
border-radius: var(--radius-md);
}
.controls {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.play-button {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
background: var(--color-primary);
color: var(--color-primary-contrast);
border: none;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
&:hover:not(:disabled) {
transform: scale(1.05);
background: var(--color-primary-hover);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading-spinner {
animation: spin 1s linear infinite;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.progress-container {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-2xs);
}
.progress-bar {
position: relative;
height: 6px;
background: var(--color-surface-secondary);
border-radius: 3px;
cursor: pointer;
overflow: hidden;
&:hover .progress-fill {
background: var(--color-primary-hover);
}
}
.progress-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: var(--color-primary);
transition: background 0.2s ease;
}
.time-display {
display: flex;
gap: var(--spacing-2xs);
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
font-variant-numeric: tabular-nums;
}
.volume-control {
width: 80px;
input[type='range'] {
width: 100%;
height: 6px;
background: var(--color-surface-secondary);
border-radius: 3px;
outline: none;
-webkit-appearance: none;
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: var(--color-primary);
border-radius: 50%;
cursor: pointer;
}
&::-moz-range-thumb {
width: 14px;
height: 14px;
background: var(--color-primary);
border-radius: 50%;
cursor: pointer;
border: none;
}
}
}
.error-message {
margin: 0;
padding: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--color-error);
text-align: center;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>

View file

@ -1,376 +0,0 @@
<script lang="ts">
import type { Photo } from '$lib/types/photos'
const {
photo,
albumPhotos = [],
currentIndex = 0,
onClose,
onNavigate
}: {
photo: Photo
albumPhotos?: Photo[]
currentIndex?: number
onClose: () => void
onNavigate: (direction: 'prev' | 'next') => void
} = $props()
let imageLoaded = $state(false)
let currentPhotoId = $state(photo.id)
const hasNavigation = $derived(albumPhotos.length > 1)
const hasExifData = $derived(photo.exif && Object.keys(photo.exif).length > 0)
// Reset loading state when photo changes
$effect(() => {
if (photo.id !== currentPhotoId) {
imageLoaded = false
currentPhotoId = photo.id
}
})
function handleImageLoad() {
imageLoaded = true
}
function handleKeydown(event: KeyboardEvent) {
switch (event.key) {
case 'Escape':
onClose()
break
case 'ArrowLeft':
if (hasNavigation) onNavigate('prev')
break
case 'ArrowRight':
if (hasNavigation) onNavigate('next')
break
}
}
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) {
onClose()
}
}
function formatExifValue(key: string, value: string): string {
switch (key) {
case 'dateTaken':
return new Date(value).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
default:
return value
}
}
function getExifLabel(key: string): string {
const labels: Record<string, string> = {
camera: 'Camera',
lens: 'Lens',
focalLength: 'Focal Length',
aperture: 'Aperture',
shutterSpeed: 'Shutter Speed',
iso: 'ISO',
dateTaken: 'Date Taken',
location: 'Location'
}
return labels[key] || key
}
</script>
<svelte:window onkeydown={handleKeydown} />
<div class="lightbox-backdrop" onclick={handleBackdropClick}>
<div class="lightbox-container">
<!-- Close button -->
<button class="close-button" onclick={onClose} type="button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M6 6l12 12M18 6l-12 12"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</button>
<!-- Navigation buttons -->
{#if hasNavigation}
<button class="nav-button nav-prev" onclick={() => onNavigate('prev')} type="button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M15 18l-6-6 6-6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<button class="nav-button nav-next" onclick={() => onNavigate('next')} type="button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M9 18l6-6-6-6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
{/if}
<!-- Photo -->
<div class="photo-container">
<img src={photo.src} alt={photo.alt} onload={handleImageLoad} class:loaded={imageLoaded} />
{#if !imageLoaded}
<div class="loading-indicator">
<div class="spinner"></div>
</div>
{/if}
</div>
<!-- Photo info -->
<div class="photo-info" class:loaded={imageLoaded}>
{#if photo.caption}
<h3 class="photo-caption">{photo.caption}</h3>
{/if}
{#if hasExifData}
<div class="exif-data">
<h4>Camera Settings</h4>
<dl class="exif-list">
{#each Object.entries(photo.exif) as [key, value]}
<div class="exif-item">
<dt>{getExifLabel(key)}</dt>
<dd>{formatExifValue(key, value)}</dd>
</div>
{/each}
</dl>
</div>
{/if}
{#if hasNavigation}
<div class="navigation-info">
{currentIndex + 1} of {albumPhotos.length}
</div>
{/if}
</div>
</div>
</div>
<style lang="scss">
.lightbox-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 1400;
padding: $unit-2x;
@include breakpoint('phone') {
padding: $unit;
}
}
.lightbox-container {
position: relative;
max-width: 90vw;
max-height: 90vh;
display: flex;
gap: $unit-3x;
@include breakpoint('phone') {
flex-direction: column;
max-width: 95vw;
max-height: 95vh;
gap: $unit-2x;
}
}
.close-button {
position: absolute;
top: -$unit-6x;
right: 0;
background: none;
border: none;
color: white;
cursor: pointer;
padding: $unit;
border-radius: $corner-radius;
transition: background-color 0.2s ease;
z-index: 10;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
@include breakpoint('phone') {
top: -$unit-4x;
right: -$unit;
}
}
.nav-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
cursor: pointer;
padding: $unit-2x;
border-radius: $corner-radius;
transition: background-color 0.2s ease;
z-index: 10;
&:hover {
background: rgba(0, 0, 0, 0.7);
}
&.nav-prev {
left: -$unit-6x;
}
&.nav-next {
right: -$unit-6x;
}
@include breakpoint('phone') {
&.nav-prev {
left: $unit;
}
&.nav-next {
right: $unit;
}
}
}
.photo-container {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
img {
max-width: 70vw;
max-height: 80vh;
height: auto;
border-radius: $corner-radius;
opacity: 0;
transition: opacity 0.3s ease;
&.loaded {
opacity: 1;
}
@include breakpoint('phone') {
max-width: 95vw;
max-height: 60vh;
}
}
}
.loading-indicator {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.photo-info {
width: 300px;
color: white;
overflow-y: auto;
padding: $unit-2x;
background: rgba(0, 0, 0, 0.3);
border-radius: $corner-radius;
backdrop-filter: blur(10px);
opacity: 0;
transition: opacity 0.3s ease 0.1s; // Slight delay to sync with image
&.loaded {
opacity: 1;
}
@include breakpoint('phone') {
width: 100%;
max-height: 30vh;
}
}
.photo-caption {
margin: 0 0 $unit-3x 0;
font-size: 1.2rem;
font-weight: 600;
line-height: 1.4;
}
.exif-data {
margin-bottom: $unit-3x;
h4 {
margin: 0 0 $unit-2x 0;
font-size: 1rem;
font-weight: 600;
opacity: 0.8;
}
}
.exif-list {
display: flex;
flex-direction: column;
gap: $unit;
margin: 0;
}
.exif-item {
display: flex;
justify-content: space-between;
align-items: baseline;
dt {
font-weight: 500;
opacity: 0.7;
margin-right: $unit-2x;
}
dd {
margin: 0;
text-align: right;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9rem;
}
}
.navigation-info {
text-align: center;
opacity: 0.7;
font-size: 0.9rem;
}
</style>

View file

@ -1,83 +0,0 @@
<script lang="ts">
import type { ComponentType } from 'svelte'
let {
icon,
text,
href,
active = false,
variant = 'default'
}: {
icon: ComponentType
text: string
href?: string
active?: boolean
variant?: 'work' | 'universe' | 'default'
} = $props()
</script>
<a {href} class="pill {variant}" class:active>
<icon />
<span>{text}</span>
</a>
<style lang="scss">
.pill {
display: flex;
align-items: center;
gap: 4px;
padding: 10px 12px;
border-radius: 100px;
text-decoration: none;
color: $grey-20; // #666
font-size: 1rem;
font-weight: 400;
transition: all 0.2s ease;
:global(svg) {
width: 20px;
height: 20px;
flex-shrink: 0;
fill: currentColor;
}
// Work variant
&.work {
&:hover,
&.active {
background: $work-bg;
color: $work-color;
:global(svg) {
fill: $work-color;
}
}
}
// Universe variant
&.universe {
&:hover,
&.active {
background: $universe-bg;
color: $universe-color;
:global(svg) {
fill: $universe-color;
}
}
}
// Default variant (Labs)
&.default {
&:hover,
&.active {
background: $labs-bg;
color: $labs-color;
:global(svg) {
fill: $labs-color;
}
}
}
}
</style>

View file

@ -1,73 +0,0 @@
<script>
import { onMount } from 'svelte'
import { spring } from 'svelte/motion'
const {
SVGComponent,
backgroundColor = '#f0f0f0',
maxMovement = 20,
containerHeight = '300px',
stiffness = 0.15,
damping = 0.8
} = $props()
let container = $state(null)
let svg = $state(null)
const position = spring(
{ x: 0, y: 0 },
{
stiffness,
damping
}
)
$effect(() => {
if (svg) {
const { x, y } = $position
svg.style.transform = `translate(calc(-50% + ${x}px), calc(-50% + ${y}px))`
}
})
function handleMouseMove(event) {
const rect = container.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
position.set({
x: (x / rect.width - 0.5) * 2 * maxMovement,
y: (y / rect.height - 0.5) * 2 * maxMovement
})
}
function handleMouseLeave() {
position.set({ x: 0, y: 0 })
}
onMount(() => {
svg = container.querySelector('svg')
if (svg) {
svg.style.position = 'absolute'
svg.style.left = '50%'
svg.style.top = '50%'
svg.style.transform = 'translate(-50%, -50%)'
}
})
</script>
<div
bind:this={container}
on:mousemove={handleMouseMove}
on:mouseleave={handleMouseLeave}
style="position: relative; overflow: hidden; background-color: {backgroundColor}; height: {containerHeight}; display: flex; justify-content: center; align-items: center;"
>
<div style="position: relative; width: 100%; height: 100%;">
<SVGComponent />
</div>
</div>
<style lang="scss">
div {
border-radius: $corner-radius;
}
</style>

View file

@ -1,83 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte'
export let text = 'Hello, Squiggly World!'
export let frequency = 0.4
export let amplitude = 1.5
export let color = '#e33d3d'
export let distance = 3
export let lineWidth = 1.75
let textWidth = 0
let textElement: HTMLHeadingElement
let squigglyHeight: number
$: path = generatePath(textWidth, frequency, amplitude, distance)
$: squigglyHeight = distance + amplitude * 2 + lineWidth
onMount(() => {
updateTextWidth()
})
function updateTextWidth(): void {
textWidth = textElement?.getBoundingClientRect().width || 0
}
function generatePath(width: number, freq: number, amp: number, dist: number): string {
if (width === 0) return ''
const startX = 2
const endX = width - 2
const startY = amp * Math.sin(startX * freq) + dist
let pathData = `M${startX},${startY} `
for (let x = startX; x <= endX; x++) {
const y = amp * Math.sin(x * freq) + dist
pathData += `L${x},${y} `
}
return pathData
}
$: {
text
updateTextWidth()
}
</script>
<div class="squiggly-container" style="padding-bottom: {squigglyHeight}px;">
<h2 bind:this={textElement} class="squiggly-header" style="color: {color}">{text}</h2>
<svg
class="squiggly-underline"
width={textWidth}
height={squigglyHeight}
viewBox="0 0 {textWidth} {squigglyHeight}"
>
<path
d={path}
fill="none"
stroke={color}
stroke-width={lineWidth}
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<style lang="scss">
.squiggly-header {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: $unit-fourth;
}
.squiggly-container {
position: relative;
display: inline-block;
}
.squiggly-underline {
position: absolute;
left: 0;
bottom: 0;
}
</style>