All files / Bandstand/src/components SongLibrary.tsx

98.36% Statements 120/122
88.88% Branches 32/36
100% Functions 3/3
98.36% Lines 120/122

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163    1x 1x 1x                                     11x 11x 11x   11x 11x 11x 33x 33x 33x 33x 12x 26x 12x 11x 11x   11x 11x 11x   11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x   11x 11x   11x 11x   11x 11x 11x 11x 11x 11x 11x 11x 11x 11x   11x 11x 11x 11x 11x 11x 11x 44x 44x 44x 44x 44x 44x   44x 44x 11x 11x 11x   11x 11x 23x 23x 23x 23x   23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x       23x 23x 23x 23x   23x 23x 23x 18x 18x 18x   5x   5x   23x 23x 11x 11x     11x 11x 11x 11x   11x 11x 11x   11x 11x 11x 11x   11x  
'use client'
 
import Link from 'next/link'
import {useMemo, useState} from 'react'
import {isRecent, shortDate} from '@/lib/dates'
 
export type LibrarySong = {
  id: string
  title: string
  leadSinger: string | null
  writer: string | null
  ready: boolean
  key: string | null
  hasChart: boolean
  editedBy: string | null
  editedAt: string | null
  /** Latest version came from an import, not someone editing */
  imported?: boolean
}
 
/** learning: has a chart, not gig-ready yet */
type Filter = 'all' | 'ready' | 'learning' | 'needs'
 
export default function SongLibrary({songs}: {songs: LibrarySong[]}) {
  const [q, setQ] = useState('')
  const [filter, setFilter] = useState<Filter>('all')
 
  const shown = useMemo(() => {
    const needle = q.trim().toLowerCase()
    return songs.filter((s) => {
      if (filter === 'ready' && !s.ready) return false
      if (filter === 'learning' && (s.ready || !s.hasChart)) return false
      if (filter === 'needs' && s.hasChart) return false
      if (!needle) return true
      return [s.title, s.leadSinger, s.writer].some((v) =>
        v?.toLowerCase().includes(needle),
      )
    })
  }, [songs, q, filter])
 
  const ready = songs.filter((s) => s.ready).length
  const needs = songs.filter((s) => !s.hasChart).length
  const learning = songs.filter((s) => s.hasChart && !s.ready).length
 
  return (
    <main className="mx-auto max-w-4xl px-4 pt-5">
      <div className="flex items-end justify-between gap-3">
        <div>
          <h1 className="text-3xl font-extrabold">The book</h1>
          <p className="mt-1 text-muted">
            {songs.length} songs · {ready} gig-ready
            {learning > 0 && ` · ${learning} learning`}
            {needs > 0 && ` · ${needs} need charts`}
          </p>
        </div>
        <Link
          href="/songs/new"
          className="inline-flex min-h-11 items-center rounded-lg border border-line-2 px-4 font-semibold no-underline hover:border-text"
        >
          Add song
        </Link>
      </div>
 
      <div className="mt-4 flex flex-wrap items-center gap-2">
        <label htmlFor="song-search" className="sr-only">
          Search songs
        </label>
        <input
          id="song-search"
          type="search"
          value={q}
          onChange={(e) => setQ(e.target.value)}
          placeholder="Search title, singer, writer"
          className="min-h-11 flex-1 basis-60 rounded-lg border border-line-2 bg-panel px-3.5 text-text placeholder:text-faint"
        />
        <div className="flex flex-wrap gap-2" role="group" aria-label="Filter">
          {(
            [
              ['all', 'All'],
              ['ready', 'Gig-ready'],
              ['learning', 'Learning'],
              ['needs', 'Needs chart'],
            ] as const
          ).map(([f, label]) => (
            <button
              key={f}
              type="button"
              aria-pressed={filter === f}
              onClick={() => setFilter(f)}
              className={`min-h-11 rounded-full px-4 text-sm font-semibold ${filter === f ? 'bg-text text-ink' : 'border border-line-2 text-text'}`}
            >
              {label}
            </button>
          ))}
        </div>
      </div>
 
      <ul className="mt-3">
        {shown.map((s) => (
          <li key={s.id} className="border-t border-line first:border-t-0">
            <Link
              href={`/songs/${s.id}`}
              className="flex min-h-16 items-center gap-3.5 py-2.5 no-underline"
            >
              <span
                aria-hidden="true"
                className={`h-2.5 w-2.5 shrink-0 rounded-full ${s.ready ? 'bg-good' : 'border-2 border-muted'}`}
              />
              <span className="flex min-w-0 flex-1 flex-col gap-0.5">
                <span className="truncate text-[17px] font-semibold">
                  {s.title}
                  {s.ready && <span className="sr-only"> (gig-ready)</span>}
                </span>
                <span className="truncate text-[13px] text-muted">
                  {[s.leadSinger, s.writer].filter(Boolean).join(' · ')}
                  {s.editedBy && s.editedAt && (
                    <>
                      {' · '}
                      <span
                        className={
                          isRecent(s.editedAt) && !s.imported
                            ? 'text-amber'
                            : undefined
                        }
                      >
                        {s.imported ? 'imported' : s.editedBy} ·{' '}
                        {shortDate(s.editedAt)}
                      </span>
                    </>
                  )}
                </span>
              </span>
              {s.hasChart ? (
                <span className="w-10 text-center font-mono font-bold text-amber">
                  {s.key ?? ''}
                </span>
              ) : (
                <span className="text-xs font-bold uppercase tracking-wider text-bad">
                  No chart
                </span>
              )}
            </Link>
          </li>
        ))}
        {!shown.length && (
          <li className="py-10 text-center text-muted">No songs match.</li>
        )}
      </ul>
      <p className="mt-4 flex flex-wrap gap-4 border-t border-line py-4 text-[13px] text-faint">
        <span>
          <span className="mr-1.5 inline-block h-2.5 w-2.5 rounded-full bg-good" />
          Gig-ready
        </span>
        <span>
          <span className="mr-1.5 inline-block h-2.5 w-2.5 rounded-full border-2 border-muted" />
          Learning
        </span>
        <span>Gold = edited in the last week</span>
      </p>
    </main>
  )
}