feat: 添加分类与标签的管理功能
This commit is contained in:
1 parent
b4b2907d77
commit
19520f5efb
4 files changed
+724
No files matched your search
@@ -0,0 +1,115 @@
|
||||
async function ensureCategoriesTable(db: any) {
|
||||
try {
|
||||
await db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL UNIQUE
|
||||
)
|
||||
`).run();
|
||||
try {
|
||||
await db.prepare('ALTER TABLE posts ADD COLUMN category_id INTEGER').run();
|
||||
} catch {
|
||||
// Column might already exist
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to ensure categories table:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ platform }) => {
|
||||
const db = platform?.env?.DB;
|
||||
|
||||
if (!db) {
|
||||
return { categories: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const { results } = await db.prepare(
|
||||
`SELECT c.id, c.name, c.slug, COUNT(p.id) as post_count
|
||||
FROM categories c
|
||||
LEFT JOIN posts p ON c.id = p.category_id
|
||||
GROUP BY c.id, c.name, c.slug
|
||||
ORDER BY c.name ASC`
|
||||
).all();
|
||||
|
||||
return {
|
||||
categories: results as unknown as { id: number; name: string; slug: string; post_count: number }[]
|
||||
};
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes('no such table')) {
|
||||
await ensureCategoriesTable(db);
|
||||
try {
|
||||
const { results } = await db.prepare(
|
||||
`SELECT c.id, c.name, c.slug, COUNT(p.id) as post_count
|
||||
FROM categories c
|
||||
LEFT JOIN posts p ON c.id = p.category_id
|
||||
GROUP BY c.id, c.name, c.slug
|
||||
ORDER BY c.name ASC`
|
||||
).all();
|
||||
return { categories: results as unknown as { id: number; name: string; slug: string; post_count: number }[] };
|
||||
} catch (e) {
|
||||
return { categories: [] };
|
||||
}
|
||||
}
|
||||
console.error('Error fetching categories:', err);
|
||||
return { categories: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
create: async ({ request, platform }) => {
|
||||
const db = platform?.env?.DB;
|
||||
if (!db) return fail(500, { error: '数据库未连接' });
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name')?.toString().trim();
|
||||
let slug = formData.get('slug')?.toString().trim();
|
||||
|
||||
if (!name) {
|
||||
return fail(400, { error: '分类名称不能为空' });
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
slug = name.toLowerCase().replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
try {
|
||||
await db.prepare('INSERT INTO categories (name, slug) VALUES (?, ?)').bind(name, slug).run();
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes('no such table')) {
|
||||
await ensureCategoriesTable(db);
|
||||
try {
|
||||
await db.prepare('INSERT INTO categories (name, slug) VALUES (?, ?)').bind(name, slug).run();
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
return fail(500, { error: '创建分类失败: ' + e.message });
|
||||
}
|
||||
}
|
||||
console.error('Error creating category:', err);
|
||||
if (err?.message?.includes('UNIQUE constraint failed')) {
|
||||
return fail(400, { error: '分类名称或 URL 别名 (Slug) 已存在' });
|
||||
}
|
||||
return fail(500, { error: '创建分类失败' });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ request, platform }) => {
|
||||
const db = platform?.env?.DB;
|
||||
if (!db) return fail(500, { error: '数据库未连接' });
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id');
|
||||
|
||||
if (!id) return fail(400, { error: '缺少分类 ID' });
|
||||
|
||||
try {
|
||||
await db.prepare('DELETE FROM categories WHERE id = ?').bind(id).run();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error deleting category:', err);
|
||||
return { success: false, error: '删除分类失败' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,279 @@
|
||||
<script lang="ts">
|
||||
import type { PageData, ActionData } from './$types';
|
||||
|
||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
|
||||
let name = $state('');
|
||||
let slug = $state('');
|
||||
|
||||
function handleNameInput() {
|
||||
if (!slug) {
|
||||
slug = name.toLowerCase().replace(/\s+/g, '-');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="admin-page-header">
|
||||
<h2>分类管理</h2>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="error-alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="categories-grid-layout">
|
||||
<!-- 左侧:新建分类卡片 -->
|
||||
<div class="create-category-card">
|
||||
<h3>新分类</h3>
|
||||
<form method="POST" action="?/create">
|
||||
<div class="form-group">
|
||||
<label for="name">名称 *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
oninput={handleNameInput}
|
||||
placeholder="例如: 技术分享"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="slug">别名</label>
|
||||
<input
|
||||
type="text"
|
||||
id="slug"
|
||||
name="slug"
|
||||
bind:value={slug}
|
||||
placeholder="例如: tech"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">创建</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:分类列表 -->
|
||||
<div class="table-container">
|
||||
{#if data.categories.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>暂无任何分类,在左侧添加一个吧!</p>
|
||||
</div>
|
||||
{:else}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>分类名称</th>
|
||||
<th>别名 (Slug)</th>
|
||||
<th>关联文章数</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.categories as category}
|
||||
<tr>
|
||||
<td>{category.id}</td>
|
||||
<td><strong>{category.name}</strong></td>
|
||||
<td><span class="badge">{category.slug}</span></td>
|
||||
<td><span class="badge">{category.post_count} 篇文章</span></td>
|
||||
<td>
|
||||
<form action="?/delete" method="POST" class="inline-form" onsubmit={(e) => {
|
||||
if(!confirm(`确定要删除分类 "${category.name}" 吗?删除后相关文章将归为未分类。`)) e.preventDefault();
|
||||
}}>
|
||||
<input type="hidden" name="id" value={category.id} />
|
||||
<button type="submit" class="action-btn delete">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.admin-page-header {
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 2px solid var(--text-color);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.error-alert {
|
||||
padding: 12px 16px;
|
||||
background-color: #fdf7f7;
|
||||
border: 2px solid #d9534f;
|
||||
color: #d9534f;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.categories-grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.categories-grid-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.create-category-card {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.create-category-card h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
border: none;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-color);
|
||||
box-sizing: border-box;
|
||||
transition: border-bottom-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-bottom-color: var(--accent-color, #bd6262);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--text-color);
|
||||
background-color: transparent;
|
||||
color: var(--text-color);
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--text-color);
|
||||
color: var(--bg-color, #ffffff);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--accent-color, #bd6262);
|
||||
border-color: var(--accent-color, #bd6262);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: transparent;
|
||||
border: 2px solid var(--text-color);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.data-table th, .data-table td {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.08));
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background-color: transparent;
|
||||
color: var(--text-color);
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
border-bottom: 2px solid var(--text-color);
|
||||
}
|
||||
|
||||
.data-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background-color: rgba(0,0,0,0.02);
|
||||
}
|
||||
|
||||
.actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s, text-decoration-color 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
color: #d9534f;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 50px 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted, rgba(0,0,0,0.6));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ platform }) => {
|
||||
const db = platform?.env?.DB;
|
||||
|
||||
if (!db) {
|
||||
return { tags: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询所有标签以及对应的文章数量
|
||||
const { results } = await db.prepare(
|
||||
`SELECT t.id, t.name, COUNT(pt.post_id) as post_count
|
||||
FROM tags t
|
||||
LEFT JOIN post_tags pt ON t.id = pt.tag_id
|
||||
GROUP BY t.id, t.name
|
||||
ORDER BY t.name ASC`
|
||||
).all();
|
||||
|
||||
return {
|
||||
tags: results as unknown as { id: number; name: string; post_count: number }[]
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error fetching tags:', err);
|
||||
return { tags: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
create: async ({ request, platform }) => {
|
||||
const db = platform?.env?.DB;
|
||||
if (!db) return fail(500, { error: '数据库未连接' });
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name')?.toString().trim();
|
||||
|
||||
if (!name) {
|
||||
return fail(400, { error: '标签名称不能为空' });
|
||||
}
|
||||
|
||||
try {
|
||||
await db.prepare('INSERT INTO tags (name) VALUES (?)').bind(name).run();
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
console.error('Error creating tag:', err);
|
||||
if (err?.message?.includes('UNIQUE constraint failed')) {
|
||||
return fail(400, { error: '标签名称已存在' });
|
||||
}
|
||||
return fail(500, { error: '创建标签失败' });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ request, platform }) => {
|
||||
const db = platform?.env?.DB;
|
||||
if (!db) return fail(500, { error: '数据库未连接' });
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id');
|
||||
|
||||
if (!id) return fail(400, { error: '缺少标签 ID' });
|
||||
|
||||
try {
|
||||
await db.prepare('DELETE FROM tags WHERE id = ?').bind(id).run();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error deleting tag:', err);
|
||||
return { success: false, error: '删除标签失败' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
<script lang="ts">
|
||||
import type { PageData, ActionData } from './$types';
|
||||
|
||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
|
||||
let newTagName = $state('');
|
||||
</script>
|
||||
|
||||
<div class="admin-page-header">
|
||||
<h2>标签管理</h2>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="error-alert">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="tags-grid-layout">
|
||||
<!-- 左侧:新建标签卡片 -->
|
||||
<div class="create-tag-card">
|
||||
<h3>新标签</h3>
|
||||
<form method="POST" action="?/create">
|
||||
<div class="form-group">
|
||||
<label for="name">名称 *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={newTagName}
|
||||
placeholder="例如: SvelteKit"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">创建</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:现有标签列表 -->
|
||||
<div class="table-container">
|
||||
{#if data.tags.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>暂无任何标签,在左侧添加一个吧!</p>
|
||||
</div>
|
||||
{:else}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标签名称</th>
|
||||
<th>关联文章数</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.tags as tag}
|
||||
<tr>
|
||||
<td>{tag.id}</td>
|
||||
<td><strong>{tag.name}</strong></td>
|
||||
<td><span class="badge">{tag.post_count} 篇文章</span></td>
|
||||
<td>
|
||||
<form action="?/delete" method="POST" class="inline-form" onsubmit={(e) => {
|
||||
if(!confirm(`确定要删除标签 "${tag.name}" 吗?`)) e.preventDefault();
|
||||
}}>
|
||||
<input type="hidden" name="id" value={tag.id} />
|
||||
<button type="submit" class="action-btn delete">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.admin-page-header {
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 2px solid var(--text-color);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.error-alert {
|
||||
padding: 12px 16px;
|
||||
background-color: #fdf7f7;
|
||||
border: 2px solid #d9534f;
|
||||
color: #d9534f;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tags-grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tags-grid-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.create-tag-card {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.create-tag-card h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
border: none;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-color);
|
||||
box-sizing: border-box;
|
||||
transition: border-bottom-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-bottom-color: var(--accent-color, #bd6262);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--text-color);
|
||||
background-color: transparent;
|
||||
color: var(--text-color);
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--text-color);
|
||||
color: var(--bg-color, #ffffff);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--accent-color, #bd6262);
|
||||
border-color: var(--accent-color, #bd6262);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: transparent;
|
||||
border: 2px solid var(--text-color);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.data-table th, .data-table td {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.08));
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background-color: transparent;
|
||||
color: var(--text-color);
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
border-bottom: 2px solid var(--text-color);
|
||||
}
|
||||
|
||||
.data-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background-color: rgba(0,0,0,0.02);
|
||||
}
|
||||
|
||||
.actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s, text-decoration-color 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
color: #d9534f;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 50px 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted, rgba(0,0,0,0.6));
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user