feat(board): inline delete confirmation + a11y polish
Replace window.confirm with an inline Delete? / Cancel / Confirm flow on post cards. Add focus-visible outlines, initials in avatar placeholders, and promote post/form titles from h3 to h2 for heading order.
This commit is contained in:
parent
7292b11c0b
commit
f691f095dc
3 changed files with 81 additions and 24 deletions
|
|
@ -2,13 +2,18 @@
|
||||||
<article class="board-post">
|
<article class="board-post">
|
||||||
<header class="post-header">
|
<header class="post-header">
|
||||||
<span class="post-meta">{{ typeLabel }}</span>
|
<span class="post-meta">{{ typeLabel }}</span>
|
||||||
<div v-if="editable" class="post-actions">
|
<div v-if="editable && !pendingDelete" class="post-actions">
|
||||||
<button type="button" class="action-btn" @click="$emit('edit', post)">Edit</button>
|
<button type="button" class="action-btn" @click="$emit('edit', post)">Edit</button>
|
||||||
<button type="button" class="action-btn danger" @click="$emit('delete', post)">Delete</button>
|
<button type="button" class="action-btn danger" @click="$emit('delete', post)">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="editable && pendingDelete" class="post-actions confirm">
|
||||||
|
<span class="confirm-label">Delete?</span>
|
||||||
|
<button type="button" class="action-btn" @click="$emit('cancel-delete', post)">Cancel</button>
|
||||||
|
<button type="button" class="action-btn danger" @click="$emit('confirm-delete', post)">Confirm</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<h3 class="post-title">{{ post.title }}</h3>
|
<h2 class="post-title">{{ post.title }}</h2>
|
||||||
|
|
||||||
<div v-if="post.seeking" class="post-block">
|
<div v-if="post.seeking" class="post-block">
|
||||||
<div class="block-label">Seeking</div>
|
<div class="block-label">Seeking</div>
|
||||||
|
|
@ -34,7 +39,7 @@
|
||||||
:alt="post.author.name"
|
:alt="post.author.name"
|
||||||
class="author-avatar"
|
class="author-avatar"
|
||||||
>
|
>
|
||||||
<span v-else class="author-avatar avatar-placeholder" />
|
<span v-else class="author-avatar avatar-placeholder" aria-hidden="true">{{ authorInitial }}</span>
|
||||||
<span class="author-name">{{ post.author ? post.author.name : 'Unknown' }}</span>
|
<span class="author-name">{{ post.author ? post.author.name : 'Unknown' }}</span>
|
||||||
<span v-if="slackHandle" class="slack-handle-wrap">
|
<span v-if="slackHandle" class="slack-handle-wrap">
|
||||||
<button
|
<button
|
||||||
|
|
@ -76,9 +81,10 @@ const props = defineProps({
|
||||||
channels: { type: Array, default: () => [] },
|
channels: { type: Array, default: () => [] },
|
||||||
tags: { type: Array, default: () => [] },
|
tags: { type: Array, default: () => [] },
|
||||||
editable: { type: Boolean, default: false },
|
editable: { type: Boolean, default: false },
|
||||||
|
pendingDelete: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['edit', 'delete'])
|
defineEmits(['edit', 'delete', 'confirm-delete', 'cancel-delete'])
|
||||||
|
|
||||||
const capitalizeAvatar = (str) => {
|
const capitalizeAvatar = (str) => {
|
||||||
if (str.toLowerCase() === 'wtf') return 'WTF'
|
if (str.toLowerCase() === 'wtf') return 'WTF'
|
||||||
|
|
@ -96,6 +102,11 @@ const authorAvatar = computed(() => {
|
||||||
|
|
||||||
const slackHandle = computed(() => props.post.author?.board?.slackHandle || '')
|
const slackHandle = computed(() => props.post.author?.board?.slackHandle || '')
|
||||||
|
|
||||||
|
const authorInitial = computed(() => {
|
||||||
|
const name = props.post.author?.name || ''
|
||||||
|
return name.trim().charAt(0).toUpperCase() || '?'
|
||||||
|
})
|
||||||
|
|
||||||
const copied = ref(false)
|
const copied = ref(false)
|
||||||
const copySlackHandle = async () => {
|
const copySlackHandle = async () => {
|
||||||
if (!slackHandle.value) return
|
if (!slackHandle.value) return
|
||||||
|
|
@ -171,6 +182,14 @@ const slackLinks = computed(() => {
|
||||||
.post-actions {
|
.post-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.post-actions.confirm .confirm-label {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ember);
|
||||||
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
.action-btn {
|
.action-btn {
|
||||||
font-family: "Commit Mono", monospace;
|
font-family: "Commit Mono", monospace;
|
||||||
|
|
@ -191,6 +210,10 @@ const slackLinks = computed(() => {
|
||||||
color: var(--ember);
|
color: var(--ember);
|
||||||
border-color: var(--ember);
|
border-color: var(--ember);
|
||||||
}
|
}
|
||||||
|
.action-btn:focus-visible {
|
||||||
|
outline: 2px dashed var(--candle);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.post-title {
|
.post-title {
|
||||||
font-family: "Brygada 1918", serif;
|
font-family: "Brygada 1918", serif;
|
||||||
|
|
@ -262,7 +285,14 @@ const slackLinks = computed(() => {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.avatar-placeholder {
|
.avatar-placeholder {
|
||||||
background: var(--surface);
|
background: transparent;
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
}
|
}
|
||||||
.author-name {
|
.author-name {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
@ -286,6 +316,11 @@ const slackLinks = computed(() => {
|
||||||
.slack-handle:hover {
|
.slack-handle:hover {
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
}
|
}
|
||||||
|
.slack-handle:focus-visible,
|
||||||
|
.copy-link:focus-visible {
|
||||||
|
outline: 2px dashed var(--candle);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
.copy-link {
|
.copy-link {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-family: "Commit Mono", monospace;
|
font-family: "Commit Mono", monospace;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<form class="post-form" @submit.prevent="handleSubmit">
|
<form class="post-form" @submit.prevent="handleSubmit">
|
||||||
<div class="form-header">
|
<div class="form-header">
|
||||||
<h3 class="form-title">{{ isEdit ? 'Edit post' : 'New post' }}</h3>
|
<h2 class="form-title">{{ isEdit ? 'Edit post' : 'New post' }}</h2>
|
||||||
<p class="form-hint">Fill in <em>Seeking</em> or <em>Offering</em> (or both).</p>
|
<p class="form-hint">Fill in <em>Seeking</em> or <em>Offering</em> (or both).</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ function handleSubmit() {
|
||||||
.post-form {
|
.post-form {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
background: var(--bg);
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-header {
|
.form-header {
|
||||||
|
|
@ -204,14 +204,6 @@ function handleSubmit() {
|
||||||
border-color: var(--candle);
|
border-color: var(--candle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.char-count {
|
|
||||||
font-size: 9px;
|
|
||||||
font-family: "Commit Mono", monospace;
|
|
||||||
color: var(--text-faint);
|
|
||||||
text-align: right;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-grid {
|
.pill-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -220,11 +212,11 @@ function handleSubmit() {
|
||||||
.pill {
|
.pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2px 9px;
|
padding: 2px 8px;
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
font-family: "Commit Mono", monospace;
|
font-family: "Commit Mono", monospace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
@ -241,6 +233,10 @@ function handleSubmit() {
|
||||||
border-color: var(--candle);
|
border-color: var(--candle);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
.pill:focus-visible {
|
||||||
|
outline: 2px dashed var(--candle);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-error {
|
.form-error {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,11 @@
|
||||||
:channels="channels"
|
:channels="channels"
|
||||||
:tags="cooperativeTags"
|
:tags="cooperativeTags"
|
||||||
:editable="isAuthor(post)"
|
:editable="isAuthor(post)"
|
||||||
|
:pending-delete="pendingDeleteId === post._id"
|
||||||
@edit="handleEdit"
|
@edit="handleEdit"
|
||||||
@delete="handleDelete"
|
@delete="requestDelete"
|
||||||
|
@confirm-delete="confirmDelete"
|
||||||
|
@cancel-delete="cancelDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -103,6 +106,7 @@ const activeTagFilter = ref(null)
|
||||||
|
|
||||||
const showForm = ref(false)
|
const showForm = ref(false)
|
||||||
const editingPost = ref(null)
|
const editingPost = ref(null)
|
||||||
|
const pendingDeleteId = ref(null)
|
||||||
|
|
||||||
const currentMemberId = computed(() => memberData.value?._id || null)
|
const currentMemberId = computed(() => memberData.value?._id || null)
|
||||||
|
|
||||||
|
|
@ -144,12 +148,18 @@ const handleEdit = (post) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (post) => {
|
const requestDelete = (post) => {
|
||||||
if (typeof window === 'undefined') return
|
pendingDeleteId.value = post._id
|
||||||
const ok = window.confirm('Delete this post? This cannot be undone.')
|
}
|
||||||
if (!ok) return
|
|
||||||
|
const cancelDelete = () => {
|
||||||
|
pendingDeleteId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async (post) => {
|
||||||
try {
|
try {
|
||||||
await deletePost(post._id)
|
await deletePost(post._id)
|
||||||
|
pendingDeleteId.value = null
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'Failed to delete post',
|
title: 'Failed to delete post',
|
||||||
|
|
@ -219,6 +229,10 @@ onMounted(async () => {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
background: rgba(154, 116, 32, 0.08);
|
background: rgba(154, 116, 32, 0.08);
|
||||||
}
|
}
|
||||||
|
.new-post-btn:focus-visible {
|
||||||
|
outline: 2px dashed var(--candle);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- TAGS DRAWER ---- */
|
/* ---- TAGS DRAWER ---- */
|
||||||
.tags-drawer-toggle {
|
.tags-drawer-toggle {
|
||||||
|
|
@ -242,6 +256,10 @@ onMounted(async () => {
|
||||||
border-color: var(--candle-faint);
|
border-color: var(--candle-faint);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
.drawer-btn:focus-visible {
|
||||||
|
outline: 2px dashed var(--candle);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
.tag-count-badge {
|
.tag-count-badge {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
background: var(--candle-faint);
|
background: var(--candle-faint);
|
||||||
|
|
@ -288,6 +306,11 @@ onMounted(async () => {
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
background: rgba(154, 116, 32, 0.08);
|
background: rgba(154, 116, 32, 0.08);
|
||||||
}
|
}
|
||||||
|
.skills-bar .skill-tag:focus-visible,
|
||||||
|
.more-btn:focus-visible {
|
||||||
|
outline: 2px dashed var(--candle);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
.more-btn {
|
.more-btn {
|
||||||
font-family: "Commit Mono", monospace;
|
font-family: "Commit Mono", monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
@ -355,11 +378,14 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.action-bar {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
.tags-drawer-toggle {
|
.tags-drawer-toggle {
|
||||||
padding: 8px 20px;
|
padding: 8px 16px;
|
||||||
}
|
}
|
||||||
.skills-bar {
|
.skills-bar {
|
||||||
padding: 10px 20px;
|
padding: 10px 16px;
|
||||||
}
|
}
|
||||||
.post-grid,
|
.post-grid,
|
||||||
.form-wrapper {
|
.form-wrapper {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue