mirror of
https://github.com/wahyd4/heygo.git
synced 2026-08-08 21:05:48 +10:00
Update UI
This commit is contained in:
+102
-16
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState, type KeyboardEvent as ReactKeyboardEvent, type SyntheticEvent } from 'react';
|
||||
import type { Link } from '../lib/api';
|
||||
import { safeLinkTargetUrl } from '../lib/url';
|
||||
|
||||
@@ -64,6 +64,25 @@ function SortIndicator({ field, sortField, sortDir }: { field: SortField; sortFi
|
||||
return <span className="sort-icon">{sortDir === 'asc' ? '↑' : '↓'}</span>;
|
||||
}
|
||||
|
||||
function navigateToLinkDetail(link: Pick<Link, 'id' | 'scope'>) {
|
||||
window.location.hash = `/links/${link.scope}/${encodeURIComponent(link.id)}`;
|
||||
}
|
||||
|
||||
function stopRowNavigation(event: SyntheticEvent<HTMLElement>) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function handleRowKeyDown(event: ReactKeyboardEvent<HTMLElement>, link: Pick<Link, 'id' | 'scope'>) {
|
||||
if (event.target !== event.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
navigateToLinkDetail(link);
|
||||
}
|
||||
}
|
||||
|
||||
export default function LinkTable({
|
||||
links,
|
||||
loading = false,
|
||||
@@ -125,6 +144,7 @@ export default function LinkTable({
|
||||
const visible = sorted.slice(0, visibleCount);
|
||||
const hasMore = sorted.length > visibleCount;
|
||||
const remaining = sorted.length - visibleCount;
|
||||
const showActionColumn = actions && (onEdit != null || onDelete != null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -203,7 +223,7 @@ export default function LinkTable({
|
||||
<SortIndicator field={field} sortField={sortField} sortDir={sortDir} />
|
||||
</th>
|
||||
))}
|
||||
<th scope="col" className="col-actions">Actions</th>
|
||||
{showActionColumn ? <th scope="col" className="col-actions">Actions</th> : null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -211,13 +231,22 @@ export default function LinkTable({
|
||||
const selected = selectable && selectedIds?.has(link.id) === true;
|
||||
const safeUrl = link.linkType === 'redirect' ? safeLinkTargetUrl(link.targetUrl) : null;
|
||||
return (
|
||||
<tr key={link.id} className={selected ? 'is-selected' : undefined}>
|
||||
<tr
|
||||
key={link.id}
|
||||
className={`link-table-row--clickable${selected ? ' is-selected' : ''}`}
|
||||
onClick={() => navigateToLinkDetail(link)}
|
||||
onKeyDown={(event) => handleRowKeyDown(event, link)}
|
||||
tabIndex={0}
|
||||
role="link"
|
||||
aria-label={`Open details for ${link.alias}`}
|
||||
>
|
||||
{selectable ? (
|
||||
<td className="col-select">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`Select ${link.alias}`}
|
||||
checked={selected}
|
||||
onClick={stopRowNavigation}
|
||||
onChange={() => onToggleSelect?.(link.id)}
|
||||
/>
|
||||
</td>
|
||||
@@ -229,6 +258,7 @@ export default function LinkTable({
|
||||
rel="noreferrer noopener"
|
||||
className="alias-pill"
|
||||
style={{ textDecoration: 'none' }}
|
||||
onClick={stopRowNavigation}
|
||||
>
|
||||
#{link.alias}
|
||||
</a>
|
||||
@@ -239,7 +269,7 @@ export default function LinkTable({
|
||||
{link.linkType === 'redirect' ? (
|
||||
link.targetUrl ? (
|
||||
safeUrl
|
||||
? <a href={safeUrl} target="_blank" rel="noreferrer noopener" className="truncate">{link.targetUrl}</a>
|
||||
? <a href={safeUrl} target="_blank" rel="noreferrer noopener" className="truncate" onClick={stopRowNavigation}>{link.targetUrl}</a>
|
||||
: <span className="muted" title="Target URL is not a valid http(s) link">Invalid target</span>
|
||||
) : <span className="muted">—</span>
|
||||
) : <span className="muted">markdown</span>}
|
||||
@@ -248,11 +278,34 @@ export default function LinkTable({
|
||||
<td className="col-updated">
|
||||
<time dateTime={link.updatedAt}>{new Date(link.updatedAt).toLocaleDateString()}</time>
|
||||
</td>
|
||||
<td className="col-actions">
|
||||
{actions && onEdit ? <button type="button" className="link-action" onClick={() => onEdit(link)}>Edit</button> : null}
|
||||
<button type="button" className="link-action" onClick={() => { window.location.hash = `/links/${link.scope}/${encodeURIComponent(link.id)}`; }}>View</button>
|
||||
{actions && onDelete ? <button type="button" className="link-action link-action--danger" onClick={() => onDelete(link)}>Delete</button> : null}
|
||||
</td>
|
||||
{showActionColumn ? (
|
||||
<td className="col-actions">
|
||||
{actions && onEdit ? (
|
||||
<button
|
||||
type="button"
|
||||
className="link-action"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onEdit(link);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
) : null}
|
||||
{actions && onDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="link-action link-action--danger"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onDelete(link);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
) : null}
|
||||
</td>
|
||||
) : null}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -267,13 +320,22 @@ export default function LinkTable({
|
||||
const selected = selectable && selectedIds?.has(link.id) === true;
|
||||
const safeUrl = link.linkType === 'redirect' ? safeLinkTargetUrl(link.targetUrl) : null;
|
||||
return (
|
||||
<div key={link.id} className={`link-card${selected ? ' is-selected' : ''}`}>
|
||||
<div
|
||||
key={link.id}
|
||||
className={`link-card link-card--clickable${selected ? ' is-selected' : ''}`}
|
||||
onClick={() => navigateToLinkDetail(link)}
|
||||
onKeyDown={(event) => handleRowKeyDown(event, link)}
|
||||
tabIndex={0}
|
||||
role="link"
|
||||
aria-label={`Open details for ${link.alias}`}
|
||||
>
|
||||
<div className="link-card-header">
|
||||
{selectable ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`Select ${link.alias}`}
|
||||
checked={selected}
|
||||
onClick={stopRowNavigation}
|
||||
onChange={() => onToggleSelect?.(link.id)}
|
||||
/>
|
||||
) : null}
|
||||
@@ -283,6 +345,7 @@ export default function LinkTable({
|
||||
rel="noreferrer noopener"
|
||||
className="alias-pill"
|
||||
style={{ textDecoration: 'none', fontSize: '0.8125rem' }}
|
||||
onClick={stopRowNavigation}
|
||||
>
|
||||
#{link.alias}
|
||||
</a>
|
||||
@@ -292,7 +355,7 @@ export default function LinkTable({
|
||||
{link.linkType === 'redirect' && link.targetUrl ? (
|
||||
<p className="link-card-url">
|
||||
{safeUrl
|
||||
? <a href={safeUrl} target="_blank" rel="noreferrer noopener" className="truncate">{link.targetUrl}</a>
|
||||
? <a href={safeUrl} target="_blank" rel="noreferrer noopener" className="truncate" onClick={stopRowNavigation}>{link.targetUrl}</a>
|
||||
: <span className="muted truncate">{link.targetUrl}</span>}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -302,11 +365,34 @@ export default function LinkTable({
|
||||
{new Date(link.updatedAt).toLocaleDateString()}
|
||||
</time>
|
||||
</div>
|
||||
<div className="link-card-actions">
|
||||
{actions && onEdit ? <button type="button" className="link-action" onClick={() => onEdit(link)}>Edit</button> : null}
|
||||
<button type="button" className="link-action" onClick={() => { window.location.hash = `/links/${link.scope}/${encodeURIComponent(link.id)}`; }}>View</button>
|
||||
{actions && onDelete ? <button type="button" className="link-action link-action--danger" onClick={() => onDelete(link)}>Delete</button> : null}
|
||||
</div>
|
||||
{showActionColumn ? (
|
||||
<div className="link-card-actions">
|
||||
{actions && onEdit ? (
|
||||
<button
|
||||
type="button"
|
||||
className="link-action"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onEdit(link);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
) : null}
|
||||
{actions && onDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="link-action link-action--danger"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onDelete(link);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -509,6 +509,12 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.link-table tbody tr:hover { background: #F8FAFF; }
|
||||
.link-table-row--clickable { cursor: pointer; }
|
||||
.link-table-row--clickable:focus-visible {
|
||||
background: #F8FAFF;
|
||||
outline: 2px solid #818CF8;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.link-table tbody tr:last-child td { border-bottom: none; }
|
||||
.link-table tr.is-selected { background: var(--accent-soft); }
|
||||
.link-table tr.is-selected:hover { background: #E0E7FF; }
|
||||
@@ -588,10 +594,16 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
|
||||
padding: 0.75rem;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.link-card--clickable { cursor: pointer; }
|
||||
.link-card:hover {
|
||||
border-color: #C7D2FE;
|
||||
box-shadow: 0 4px 16px rgba(99,102,241,0.08);
|
||||
}
|
||||
.link-card--clickable:focus-visible {
|
||||
border-color: #818CF8;
|
||||
box-shadow: 0 0 0 3px rgba(99,102,241,0.18), 0 4px 16px rgba(99,102,241,0.08);
|
||||
outline: none;
|
||||
}
|
||||
.link-card.is-selected { background: var(--accent-soft); border-color: #A5B4FC; }
|
||||
|
||||
.link-card-header {
|
||||
|
||||
Reference in New Issue
Block a user