- Run record
- loading
Loading this Run…
Loading the Revisions of this Run…
A published source file of Revision 0 of Boardly — Website Relaunch 1.0.0, shown as text in the browser.
import { useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import initial from '../public/data/board.json';
import team from '../public/data/members.json';
import './styles.css';
type Card = {id:string;title:string;description:string;assignee:string|null;labels:string[];priority:string;due:string|null;checklist:{id:string;text:string;done:boolean}[]};
type Board = Omit<typeof initial,'cards'> & {cards:Card[]};
const copy = <T,>(v:T):T => structuredClone(v);
const members = team.members;
const initials = (name:string) => name.split(' ').map(n=>n[0]).join('');
function Icon({name,size=18}:{name:string,size?:number}) { const paths:Record<string,React.ReactNode>={board:<><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M9 4v16M15 4v16"/></>,search:<><circle cx="10" cy="10" r="6"/><path d="m15 15 5 5"/></>,plus:<path d="M12 5v14M5 12h14"/>,arrow:<path d="M4 12h16m-5-5 5 5-5 5"/>,check:<path d="m5 12 4 4L19 6"/>,calendar:<><rect x="4" y="5" width="16" height="16" rx="3"/><path d="M8 3v4m8-4v4M4 11h16"/></>,list:<><rect x="3" y="4" width="18" height="17" rx="3"/><path d="m7 12 2 2 3-4m3 2h3"/></>,undo:<><path d="M8 5 3 10l5 5M3 10h11a6 6 0 0 1 0 12"/></>,grid:<><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></>,chevron:<path d="m8 10 4 4 4-4"/>,close:<path d="m6 6 12 12M6 18 18 6"/>,help:<><circle cx="12" cy="12" r="9"/><path d="M9 8a3 3 0 0 1 6 1c0 2-3 2-3 5m0 3h.01"/></>};return <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.65" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">{paths[name]||paths.board}</svg> }
function Avatar({id,small=false}:{id:string|null,small?:boolean}){const m=members.find(m=>m.id===id);return <span className={`avatar ${id||'unassigned'} ${small?'small':''}`} title={m?.name||'Unassigned'} aria-label={m?.name||'Unassigned'}>{m?initials(m.name):'–'}</span>}
function App(){
const [board,setBoard]=useState<Board>(()=>{try{const b=JSON.parse(localStorage.getItem('boardly-state')||'null');return b?.columns&&b?.cards?b:copy(initial)}catch{return copy(initial)}});
const [undoState,setUndoState]=useState<Board|null>(null),[query,setQuery]=useState(''),[assignee,setAssignee]=useState(''),[label,setLabel]=useState('');
const [message,setMessage]=useState(''),[modal,setModal]=useState<{card:Card,column:string,isNew:boolean}|null>(null),[error,setError]=useState(''),[confirmDelete,setConfirmDelete]=useState(false),[help,setHelp]=useState(false);
const [drag,setDrag]=useState<string|null>(null),[target,setTarget]=useState<{col:string,index:number}|null>(null),[picked,setPicked]=useState<{id:string,original:Board}|null>(null);
const dialog=useRef<HTMLDialogElement>(null),returnFocus=useRef<HTMLElement|null>(null),boardRef=useRef(board); boardRef.current=board;
useEffect(()=>{localStorage.setItem('boardly-state',JSON.stringify(board))},[board]);
useEffect(()=>{if(modal||help)dialog.current?.showModal();else dialog.current?.close()},[modal,help]);
const announce=(s:string)=>setMessage(s);
function undo(){if(undoState){setBoard(undoState);setUndoState(null);announce('Last action undone.')}}
useEffect(()=>{const handler=(e:KeyboardEvent)=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='z'&&!['INPUT','TEXTAREA','SELECT'].includes((e.target as HTMLElement).tagName)){e.preventDefault();undo()}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler)},[undoState]);
function close(){setModal(null);setHelp(false);setConfirmDelete(false);setError('');setTimeout(()=>returnFocus.current?.focus(),0)}
function open(card:Card,column:string,isNew=false){returnFocus.current=document.activeElement as HTMLElement;setError('');setConfirmDelete(false);setModal({card:copy(card),column,isNew})}
function allowed(b:Board,col:string,id?:string){const c=b.columns.find(c=>c.id===col)!;if(c.wip_limit&&c.card_ids.length>=c.wip_limit&&!c.card_ids.includes(id||'')){const s=`${c.title} is at its limit of ${c.wip_limit} cards. Move a card out before adding another so the team can stay focused.`;announce(s);setError(s);return false}return true}
function move(id:string,col:string,index:number,keyboard=false){const current=boardRef.current;if(!allowed(current,col,id))return false;const b=copy(current);const from=b.columns.find(c=>c.card_ids.includes(id))!;const oldIndex=from.card_ids.indexOf(id);from.card_ids.splice(oldIndex,1);const dest=b.columns.find(c=>c.id===col)!;dest.card_ids.splice(Math.max(0,Math.min(index,dest.card_ids.length)),0,id);if(!keyboard)setUndoState(copy(current));setBoard(b);boardRef.current=b;announce(`${b.cards.find(c=>c.id===id)?.title} moved to ${dest.title}, position ${dest.card_ids.indexOf(id)+1}.`);return true}
function create(col:string){open({id:`c-${Date.now()}`,title:'',description:'',assignee:null,labels:[],priority:'medium',due:null,checklist:[]},col,true)}
function save(){if(!modal)return;if(!modal.card.title.trim()){setError('Give this card a title before saving.');return}if(modal.isNew&&!allowed(board,modal.column))return;const b=copy(board);modal.card.title=modal.card.title.trim();if(modal.isNew){setUndoState(copy(board));b.cards.push(modal.card);b.columns.find(c=>c.id===modal.column)!.card_ids.push(modal.card.id)}else b.cards=b.cards.map(c=>c.id===modal.card.id?modal.card:c);setBoard(b);announce(modal.isNew?'Card added. A little closer to launch.':'Card changes saved.');close()}
function remove(){if(!modal)return;const b=copy(board);b.cards=b.cards.filter(c=>c.id!==modal.card.id);b.columns.forEach(c=>c.card_ids=c.card_ids.filter(id=>id!==modal.card.id));setUndoState(copy(board));setBoard(b);announce('Card deleted. You can undo this action.');close()}
function keyboard(e:React.KeyboardEvent,id:string){if(e.target!==e.currentTarget)return;const col=board.columns.find(c=>c.card_ids.includes(id))!;if(e.key==='Enter'&&!picked){e.preventDefault();open(board.cards.find(c=>c.id===id)!,col.id);return}if(e.key===' '||e.key==='Enter'){e.preventDefault();if(picked){setUndoState(picked.original);setPicked(null);announce('Card dropped.')}else{setPicked({id,original:copy(board)});announce('Card picked up. Use arrow keys to move, Space to drop, or Escape to cancel.')}return}if(picked?.id===id){if(e.key==='Escape'){e.preventDefault();setBoard(picked.original);setPicked(null);announce('Move cancelled. Card returned to its original position.');setTimeout(()=>document.getElementById(id)?.focus(),0)}else if(e.key.startsWith('Arrow')){e.preventDefault();const ci=board.columns.indexOf(col),idx=col.card_ids.indexOf(id);if(e.key==='ArrowUp')move(id,col.id,idx-1,true);if(e.key==='ArrowDown')move(id,col.id,idx+1,true);if(e.key==='ArrowLeft'&&ci>0)move(id,board.columns[ci-1].id,idx,true);if(e.key==='ArrowRight'&&ci<4)move(id,board.columns[ci+1].id,idx,true);setTimeout(()=>document.getElementById(id)?.focus(),0)}}}
const matches=(c:Card)=>(!query||`${c.title} ${c.description}`.toLowerCase().includes(query.toLowerCase()))&&(!assignee||(assignee==='unassigned'?!c.assignee:c.assignee===assignee))&&(!label||c.labels.includes(label));
const visible=board.cards.filter(matches).length,done=board.columns.find(c=>c.id==='done')!.card_ids.length,filtered=!!(query||assignee||label);
const clear=()=>{setQuery('');setAssignee('');setLabel('')};
function patch(values:Partial<Card>){setModal(m=>m?{...m,card:{...m.card,...values}}:m);setError('')}
return <div className="app"><aside className="sidebar"><a className="brand" href="#"><span className="brand-icon"><i/><i/><i/></span>boardly<span className="brand-dot">.</span></a><div className="workspace"><span className="workspace-icon">H<span>↗</span></span><div><strong>Harbor Studio</strong><small>Team workspace</small></div><Icon name="chevron" size={15}/></div><div className="nav-label">WORKSPACE</div><div className="nav-item"><Icon name="grid"/>Projects <span className="nav-number">1</span></div><div className="project-link"><span/>Website Relaunch</div><div className="sidebar-project"><div className="nav-label">YOUR TEAM <span>5</span></div>{members.map(m=><div className="member" key={m.id}><Avatar id={m.id}/><div>{m.name}<small>{m.role}</small></div><i/></div>)}</div><div className="sidebar-bottom"><div className="tip-icon">✧</div><strong>Less busywork.<br/>More good work.</strong><p>A little space for your team's<br/>next big thing.</p><button className="help-button" onClick={()=>{returnFocus.current=document.activeElement as HTMLElement;setHelp(true)}}><Icon name="help"/>Board tips & shortcuts</button><div className="profile"><Avatar id="maya"/><div>Maya Lindqvist<small>Harbor Studio</small></div><span>⌄</span></div></div></aside>
<main><header className="topbar"><div className="breadcrumbs">Projects <span>/</span><strong>Website Relaunch</strong></div><div className="topbar-right"><span className="saved"><i/>All changes saved</span><button className="text-button" onClick={()=>{setUndoState(copy(board));setBoard(copy(initial));clear();announce('Demo reset to the original board.')}}>Reset demo</button><Avatar id="maya"/></div></header>
<section className="project-header"><div className="project-eyebrow"><span className="project-symbol"><Icon name="board" size={21}/></span><span className="mobile-workspace">HARBOR STUDIO · </span>THE NEXT CHAPTER</div><div className="title-row"><h1>Website Relaunch</h1><div className="team-stack">{members.map(m=><Avatar id={m.id} key={m.id}/>)}<span>Our team</span></div></div><p>A fresh home for Harbor. Let’s make it happen.</p><div className="project-meta"><span className="active-badge"><i/>In motion</span><span className="meta-separator"/><span>{board.cards.length} tasks</span><span className="progress-track"><i style={{width:`${done/Math.max(1,board.cards.length)*100}%`}}/></span><span>{done} completed</span><span className="project-date">Wednesday, October 14</span></div></section>
<div className="viewbar"><div className="view-tab"><Icon name="board" size={17}/>Board <span>{board.cards.length}</span></div><div className="viewbar-note">A clear view of what’s next.</div><button className="undo-button" disabled={!undoState} onClick={undo}><Icon name="undo" size={15}/>Undo</button><button className="primary" onClick={()=>create('backlog')}><Icon name="plus" size={16}/>New card</button></div>
<div className="filters"><div className="search"><Icon name="search" size={17}/><input aria-label="Search cards" value={query} onChange={e=>setQuery(e.target.value)} placeholder="Search cards…"/><span>⌕</span></div><div className="filter-select"><span>Assignee</span><select aria-label="Filter by assignee" value={assignee} onChange={e=>setAssignee(e.target.value)}><option value="">Everyone</option><option value="unassigned">Unassigned</option>{members.map(m=><option key={m.id} value={m.id}>{m.name}</option>)}</select></div><div className="filter-select"><span>Label</span><select aria-label="Filter by label" value={label} onChange={e=>setLabel(e.target.value)}><option value="">All labels</option>{board.labels.map(l=><option key={l.id} value={l.id}>{l.name}</option>)}</select></div>{filtered?<button className="clear" onClick={clear}>Clear filters ×</button>:<span className="filter-count">{visible} cards on this board</span>}</div>
{filtered&&visible===0&&<div className="empty-global"><strong>No cards found</strong><span>Try a different search or give your filters a fresh start.</span><button onClick={clear}>Clear all filters</button></div>}
<section className="board-region" aria-label="Website Relaunch board"><div className="board">{board.columns.map((col,ci)=><section key={col.id} className={`column column-${col.id}`} aria-label={col.title} onDragOver={e=>{e.preventDefault();if(!(e.target as HTMLElement).closest('.card'))setTarget({col:col.id,index:col.card_ids.length})}} onDrop={e=>{e.preventDefault();if(drag&&target){const source=board.columns.find(c=>c.card_ids.includes(drag))!;let index=target.index;if(source.id===target.col&&source.card_ids.indexOf(drag)<index)index--;move(drag,target.col,index)}setDrag(null);setTarget(null)}}><div className="column-heading"><span className={`status-dot status-${ci}`}>{ci===4?'✓':''}</span><h2>{col.title}</h2><span className={`count ${col.wip_limit?'limit':''}`}>{col.card_ids.filter(id=>matches(board.cards.find(c=>c.id===id)!)).length}{col.wip_limit&&<span> / {col.wip_limit}</span>}</span><button aria-label={`Add card to ${col.title}`} onClick={()=>create(col.id)}><Icon name="plus" size={16}/></button></div><div className="column-rule"/><div className="cards">{col.card_ids.map((id,index)=>{const c=board.cards.find(c=>c.id===id)!;if(!matches(c))return null;const overdue=!!c.due&&c.due<board.today&&col.id!=='done';return <div key={id}>{target?.col===col.id&&target.index===index&&<div className="drop-placeholder">Drop card here</div>}<article id={id} tabIndex={0} role="button" aria-label={`${c.title}. Enter to open; Space to pick up.`} className={`card ${drag===id?'dragging':''} ${picked?.id===id?'picked':''}`} draggable={!picked} onDragStart={e=>{setDrag(id);e.dataTransfer.setData('text/plain',id);e.dataTransfer.effectAllowed='move'}} onDragEnd={()=>{setDrag(null);setTarget(null)}} onDragOver={e=>{e.preventDefault();e.stopPropagation();const rect=e.currentTarget.getBoundingClientRect();setTarget({col:col.id,index:index+(e.clientY>rect.top+rect.height/2?1:0)})}} onKeyDown={e=>keyboard(e,id)} onClick={e=>{if(!(e.target as HTMLElement).closest('select,button')){e.currentTarget.focus();open(c,col.id)}}}><div className="card-top"><span className="card-id">{c.id.replace('c-','WEB-')}</span><span className={`priority ${c.priority}`} title={`${c.priority} priority`} aria-label={`${c.priority} priority`}>{c.priority==='urgent'?'⚑':c.priority==='high'?'▥':c.priority==='medium'?'▰':'▱'}</span></div><h3>{c.title}</h3><div className="labels">{c.labels.map(l=><span key={l} className={`label label-${l}`}><i/>{board.labels.find(x=>x.id===l)?.name}</span>)}</div><div className={`card-due ${overdue?'overdue':''}`}><Icon name="calendar" size={13}/>{c.due?new Date(c.due+'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric'}):'No due date'}{overdue&&<span>Overdue</span>}{c.due===board.today&&col.id!=='done'&&<span className="today">Today</span>}</div><div className="card-bottom"><div className="card-person"><Avatar id={c.assignee} small/>{!c.assignee&&<span>Unassigned</span>}</div><div className="card-tools">{c.checklist.length>0&&<span className={`checklist-progress ${c.checklist.every(i=>i.done)?'complete':''}`}><Icon name="list" size={14}/>{c.checklist.filter(i=>i.done).length}/{c.checklist.length}</span>}<div className="move-control" title="Move card"><Icon name="arrow" size={14}/><select aria-label={`Move ${c.title} to column`} value="" onClick={e=>e.stopPropagation()} onChange={e=>{if(e.target.value)move(c.id,e.target.value,board.columns.find(x=>x.id===e.target.value)!.card_ids.length)}}><option value="">Move to…</option>{board.columns.map(x=><option key={x.id} value={x.id} disabled={x.id===col.id}>{x.title}</option>)}</select></div></div></div></article></div>})}{target?.col===col.id&&target.index===col.card_ids.length&&<div className="drop-placeholder">Drop card here</div>}{col.card_ids.filter(id=>matches(board.cards.find(c=>c.id===id)!)).length===0&&<div className="column-empty">{filtered?'No matching cards':'Room for what’s next.'}</div>}</div><button className="add-card" onClick={()=>create(col.id)}><Icon name="plus" size={15}/>Add card</button></section>)}</div></section>
<footer><span><span className="footer-mark">✧</span> Boardly is a fictional product created for UIBenchmark.</span><span className="keyboard-hint">Space to pick up <span>·</span> Arrows to move <span>·</span> Esc to cancel</span></footer></main>
<div className={`toast ${message?'show':''}`} role="status" aria-live="polite">{message}{message&&<button aria-label="Dismiss message" onClick={()=>setMessage('')}><Icon name="close" size={15}/></button>}</div>
<dialog ref={dialog} aria-label={help?'Board tips and shortcuts':modal?.isNew?'Create card':'Card details'} onCancel={e=>{e.preventDefault();close()}} onClick={e=>{if(e.target===e.currentTarget)close()}}>{help?<div className="dialog-body"><div className="dialog-heading"><h2>A little help along the way</h2><button onClick={close} aria-label="Close tips"><Icon name="close"/></button></div><p>Open any card to update the details. Drag a card into place, or use its arrow control to choose a column.</p><p><strong>Prefer your keyboard?</strong> Tab to a card and press Space to pick it up. Arrow keys move it; Space or Enter drops it. Escape puts it back. Enter opens a card when it isn’t picked up.</p><p>Undo your last move, creation, or deletion with Ctrl+Z or Cmd+Z. In progress is limited to three cards to keep work flowing.</p><button className="primary" onClick={close}>Got it</button></div>:modal&&<form onSubmit={e=>{e.preventDefault();save()}} className="dialog-body"><div className="dialog-heading"><span className="dialog-eyebrow">{modal.isNew?'A NEW NEXT STEP':modal.card.id.replace('c-','WEB-')} <span> / {board.columns.find(c=>c.id===modal.column)?.title}</span></span><button type="button" onClick={close} aria-label="Close card details"><Icon name="close"/></button></div><h2>{modal.isNew?'Add a card':'Card details'}</h2><label className="field">Title <input autoFocus value={modal.card.title} onChange={e=>patch({title:e.target.value})} placeholder="What needs to happen?"/></label><label className="field">Description<textarea rows={3} value={modal.card.description} onChange={e=>patch({description:e.target.value})} placeholder="Add a little context…"/></label><div className="field-row"><label className="field">Assignee<select value={modal.card.assignee||''} onChange={e=>patch({assignee:e.target.value||null})}><option value="">Unassigned</option>{members.map(m=><option key={m.id} value={m.id}>{m.name}</option>)}</select></label><label className="field">Priority<select value={modal.card.priority} onChange={e=>patch({priority:e.target.value})}>{board.priorities.map(p=><option key={p}>{p}</option>)}</select></label></div><div className="field-row"><label className="field">Due date<input type="date" value={modal.card.due||''} onChange={e=>patch({due:e.target.value||null})}/></label><label className="field">Column<select value={modal.column} disabled={!modal.isNew} onChange={e=>setModal({...modal,column:e.target.value})}>{board.columns.map(c=><option key={c.id} value={c.id}>{c.title}</option>)}</select></label></div><div className="field">Labels<div className="label-options">{board.labels.map(l=><label className={`label label-${l.id}`} key={l.id}><input type="checkbox" checked={modal.card.labels.includes(l.id)} onChange={e=>patch({labels:e.target.checked?[...modal.card.labels,l.id]:modal.card.labels.filter(x=>x!==l.id)})}/>{l.name}</label>)}</div></div><div className="field">Checklist <span className="subtle">{modal.card.checklist.filter(i=>i.done).length}/{modal.card.checklist.length}</span>{modal.card.checklist.length?modal.card.checklist.map(item=><label className="check-item" key={item.id}><input type="checkbox" checked={item.done} onChange={e=>patch({checklist:modal.card.checklist.map(i=>i.id===item.id?{...i,done:e.target.checked}:i)})}/><span className={item.done?'checked':''}>{item.text}</span></label>):<span className="subtle">No checklist items on this card.</span>}</div>{error&&<p className="form-error" role="alert">{error}</p>}{confirmDelete?<div className="delete-confirm"><strong>Delete this card?</strong><p>This removes the card and its checklist. You can undo the deletion.</p><button type="button" className="danger" onClick={remove}>Yes, delete card</button><button type="button" onClick={()=>setConfirmDelete(false)}>Keep card</button></div>:<div className="dialog-actions">{!modal.isNew&&<button type="button" className="delete-button" onClick={()=>setConfirmDelete(true)}>Delete card</button>}<button type="button" onClick={close}>Cancel</button><button className="primary" type="submit">{modal.isNew?'Create card':'Save changes'}</button></div>}</form>}</dialog></div>
}
createRoot(document.getElementById('root')!).render(<App/>);Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.