feat: reskin member pages to zine direction
This commit is contained in:
parent
88caca94c7
commit
1ac21d6a98
4 changed files with 1972 additions and 1754 deletions
|
|
@ -2,231 +2,580 @@
|
|||
<div>
|
||||
<PageHeader
|
||||
title="My Updates"
|
||||
subtitle="View and manage your updates"
|
||||
size="medium"
|
||||
subtitle="Your activity and milestones in the Guild"
|
||||
/>
|
||||
|
||||
<section class="py-12 px-4">
|
||||
<UContainer class="px-4">
|
||||
<!-- Stats -->
|
||||
<div v-if="isAuthenticated && !pending" class="mb-8 flex items-center justify-between">
|
||||
<div class="text-guild-300">
|
||||
<span class="text-2xl font-bold text-guild-100">{{ total }}</span>
|
||||
{{ total === 1 ? "update" : "updates" }} posted
|
||||
</div>
|
||||
<UButton to="/updates/new" icon="i-lucide-plus"> New Update </UButton>
|
||||
<!-- Content Area: two-column with events mini sidebar -->
|
||||
<div class="content-area">
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="content-main">
|
||||
|
||||
<!-- Stats + New Update row -->
|
||||
<div v-if="isAuthenticated && !pending" class="stats-row">
|
||||
<span class="stats-count">
|
||||
<strong>{{ total }}</strong> {{ total === 1 ? 'update' : 'updates' }} posted
|
||||
</span>
|
||||
<NuxtLink to="/updates/new" class="btn btn-primary">+ New Update</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="pending && !updates.length"
|
||||
class="flex justify-center items-center py-20"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-8 h-8 border-4 border-guild-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
|
||||
></div>
|
||||
<p class="text-guild-400">Loading your updates...</p>
|
||||
</div>
|
||||
<div v-if="pending && !updates.length" class="state-box">
|
||||
<div class="spinner"></div>
|
||||
<p class="state-text">Loading your updates...</p>
|
||||
</div>
|
||||
|
||||
<!-- Unauthenticated State -->
|
||||
<div
|
||||
v-else-if="!isAuthenticated"
|
||||
class="flex justify-center items-center py-20"
|
||||
>
|
||||
<div class="text-center max-w-md">
|
||||
<div class="w-16 h-16 bg-guild-800 border border-guild-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Icon name="heroicons:lock-closed" class="w-8 h-8 text-guild-400" />
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-guild-100 mb-2">Sign in required</h2>
|
||||
<p class="text-guild-400 mb-6">Please sign in to view your updates.</p>
|
||||
<UButton @click="openLoginModal({ title: 'Sign in to view your updates', description: 'Enter your email to access your updates' })">
|
||||
Sign In
|
||||
</UButton>
|
||||
<div v-else-if="!isAuthenticated" class="state-box">
|
||||
<div class="state-icon">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="state-heading">Sign in required</h2>
|
||||
<p class="state-text">Please sign in to view your updates.</p>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="openLoginModal({ title: 'Sign in to view your updates', description: 'Enter your email to access your updates' })"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Updates List -->
|
||||
<div v-else-if="updates.length" class="space-y-6">
|
||||
<UpdateCard
|
||||
v-for="update in updates"
|
||||
:key="update._id"
|
||||
:update="update"
|
||||
:show-preview="true"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<!-- Updates Timeline -->
|
||||
<div v-else-if="updates.length" class="timeline-wrap">
|
||||
<div class="timeline">
|
||||
<div
|
||||
v-for="update in updates"
|
||||
:key="update._id"
|
||||
class="tl-item"
|
||||
>
|
||||
<div class="tl-dot">✎</div>
|
||||
<div class="tl-time">{{ formatDate(update.createdAt) }}</div>
|
||||
<div class="tl-text">
|
||||
<NuxtLink :to="`/updates/${update._id}`" class="tl-title">
|
||||
{{ getUpdateTitle(update) }}
|
||||
</NuxtLink>
|
||||
<span v-if="isEdited(update)" class="tl-edited">(edited)</span>
|
||||
<span v-if="update.privacy === 'private'" class="badge">Private</span>
|
||||
<span v-if="update.privacy === 'public'" class="badge">Public</span>
|
||||
</div>
|
||||
<div v-if="getUpdatePreview(update)" class="tl-detail">
|
||||
{{ getUpdatePreview(update) }}
|
||||
</div>
|
||||
|
||||
<!-- Images -->
|
||||
<div v-if="update.images?.length" class="tl-images">
|
||||
<img
|
||||
v-for="(image, index) in update.images"
|
||||
:key="index"
|
||||
:src="image.url"
|
||||
:alt="image.alt || 'Update image'"
|
||||
class="tl-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div v-if="isAuthor(update)" class="tl-actions">
|
||||
<button class="tl-action-btn" @click="handleEdit(update)">Edit</button>
|
||||
<span class="tl-action-sep">·</span>
|
||||
<button class="tl-action-btn tl-action-danger" @click="handleDelete(update)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load More -->
|
||||
<div v-if="hasMore" class="flex justify-center pt-4">
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="neutral"
|
||||
:loading="loadingMore"
|
||||
<div v-if="hasMore" class="load-more">
|
||||
<button
|
||||
class="btn"
|
||||
:disabled="loadingMore"
|
||||
@click="loadMore"
|
||||
>
|
||||
Load More
|
||||
</UButton>
|
||||
{{ loadingMore ? 'Loading...' : 'Load More' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="text-center py-20">
|
||||
<div class="w-16 h-16 mx-auto mb-4 opacity-50">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
class="text-guild-600"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||
/>
|
||||
<div v-else class="state-box">
|
||||
<div class="state-icon">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-guild-300 mb-2">
|
||||
No updates yet
|
||||
</h3>
|
||||
<p class="text-guild-400 mb-6">
|
||||
Share your first update with the community
|
||||
</p>
|
||||
<UButton to="/updates/new" icon="i-lucide-plus">
|
||||
Post Your First Update
|
||||
</UButton>
|
||||
<h2 class="state-heading">No updates yet</h2>
|
||||
<p class="state-text">Share your first update with the community</p>
|
||||
<NuxtLink to="/updates/new" class="btn btn-primary">+ Post Your First Update</NuxtLink>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Events Mini Sidebar -->
|
||||
<EventsMiniSidebar :events="upcomingEvents" />
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<UModal
|
||||
v-model:open="showDeleteModal"
|
||||
title="Delete Update?"
|
||||
description="Are you sure you want to delete this update? This action cannot be undone."
|
||||
>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<UButton
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="showDeleteModal = false"
|
||||
>
|
||||
Cancel
|
||||
</UButton>
|
||||
<UButton color="red" :loading="deleting" @click="confirmDelete">
|
||||
Delete
|
||||
</UButton>
|
||||
<Teleport to="body">
|
||||
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
|
||||
<div class="modal-box">
|
||||
<h3 class="modal-heading">Delete Update?</h3>
|
||||
<p class="modal-text">Are you sure you want to delete this update? This action cannot be undone.</p>
|
||||
<div class="modal-actions">
|
||||
<button class="btn" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn btn-danger" :disabled="deleting" @click="confirmDelete">
|
||||
{{ deleting ? 'Deleting...' : 'Delete' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { isAuthenticated, checkMemberStatus } = useAuth();
|
||||
const { openLoginModal } = useLoginModal();
|
||||
const { isAuthenticated, memberData, checkMemberStatus } = useAuth()
|
||||
const { openLoginModal } = useLoginModal()
|
||||
|
||||
const updates = ref([]);
|
||||
const pending = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const hasMore = ref(false);
|
||||
const total = ref(0);
|
||||
const updates = ref([])
|
||||
const pending = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const hasMore = ref(false)
|
||||
const total = ref(0)
|
||||
|
||||
const showDeleteModal = ref(false);
|
||||
const updateToDelete = ref(null);
|
||||
const deleting = ref(false);
|
||||
const showDeleteModal = ref(false)
|
||||
const updateToDelete = ref(null)
|
||||
const deleting = ref(false)
|
||||
|
||||
const upcomingEvents = ref([])
|
||||
|
||||
// Check if current user is the author of an update
|
||||
const isAuthor = (update) => {
|
||||
return memberData.value && update.author?._id === memberData.value.id
|
||||
}
|
||||
|
||||
// Check if update was edited
|
||||
const isEdited = (update) => {
|
||||
const created = new Date(update.createdAt).getTime()
|
||||
const updated = new Date(update.updatedAt).getTime()
|
||||
return updated - created > 1000
|
||||
}
|
||||
|
||||
// Extract a title from update content (first line or first ~60 chars)
|
||||
const getUpdateTitle = (update) => {
|
||||
if (!update.content) return 'Untitled update'
|
||||
const firstLine = update.content.split('\n')[0]
|
||||
if (firstLine.length <= 80) return firstLine
|
||||
return firstLine.substring(0, 80) + '...'
|
||||
}
|
||||
|
||||
// Get a preview of the update content (after the first line)
|
||||
const getUpdatePreview = (update) => {
|
||||
if (!update.content) return ''
|
||||
const lines = update.content.split('\n')
|
||||
if (lines.length <= 1 && update.content.length <= 80) return ''
|
||||
// If the first line was truncated, show the full content as preview
|
||||
if (lines.length <= 1) return ''
|
||||
const rest = lines.slice(1).join(' ').trim()
|
||||
if (!rest) return ''
|
||||
return rest.length > 200 ? rest.substring(0, 200) + '...' : rest
|
||||
}
|
||||
|
||||
// Format date with relative time
|
||||
const formatDate = (date) => {
|
||||
const now = new Date()
|
||||
const updateDate = new Date(date)
|
||||
const diffInSeconds = Math.floor((now - updateDate) / 1000)
|
||||
|
||||
if (diffInSeconds < 60) return 'just now'
|
||||
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`
|
||||
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`
|
||||
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`
|
||||
|
||||
return updateDate.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: updateDate.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
onMounted(async () => {
|
||||
if (!isAuthenticated.value) {
|
||||
const authenticated = await checkMemberStatus();
|
||||
const authenticated = await checkMemberStatus()
|
||||
if (!authenticated) {
|
||||
// Show login modal instead of redirecting
|
||||
openLoginModal({
|
||||
title: "Sign in to view your updates",
|
||||
description: "Enter your email to access your updates",
|
||||
});
|
||||
return;
|
||||
title: 'Sign in to view your updates',
|
||||
description: 'Enter your email to access your updates',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await loadUpdates();
|
||||
});
|
||||
await Promise.all([loadUpdates(), loadUpcomingEvents()])
|
||||
})
|
||||
|
||||
// Load updates
|
||||
const loadUpdates = async () => {
|
||||
pending.value = true;
|
||||
pending.value = true
|
||||
try {
|
||||
const response = await $fetch("/api/updates/my-updates", {
|
||||
const response = await $fetch('/api/updates/my-updates', {
|
||||
params: { limit: 20, skip: 0 },
|
||||
});
|
||||
updates.value = response.updates;
|
||||
total.value = response.total;
|
||||
hasMore.value = response.hasMore;
|
||||
})
|
||||
updates.value = response.updates
|
||||
total.value = response.total
|
||||
hasMore.value = response.hasMore
|
||||
} catch (error) {
|
||||
console.error("Failed to load updates:", error);
|
||||
console.error('Failed to load updates:', error)
|
||||
} finally {
|
||||
pending.value = false;
|
||||
pending.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Load upcoming events for sidebar
|
||||
const loadUpcomingEvents = async () => {
|
||||
try {
|
||||
const response = await $fetch('/api/events', {
|
||||
params: { limit: 3, upcoming: true },
|
||||
})
|
||||
upcomingEvents.value = response.events || response || []
|
||||
} catch (error) {
|
||||
console.error('Failed to load upcoming events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Load more updates
|
||||
const loadMore = async () => {
|
||||
loadingMore.value = true;
|
||||
loadingMore.value = true
|
||||
try {
|
||||
const response = await $fetch("/api/updates/my-updates", {
|
||||
const response = await $fetch('/api/updates/my-updates', {
|
||||
params: { limit: 20, skip: updates.value.length },
|
||||
});
|
||||
updates.value.push(...response.updates);
|
||||
hasMore.value = response.hasMore;
|
||||
})
|
||||
updates.value.push(...response.updates)
|
||||
hasMore.value = response.hasMore
|
||||
} catch (error) {
|
||||
console.error("Failed to load more updates:", error);
|
||||
console.error('Failed to load more updates:', error)
|
||||
} finally {
|
||||
loadingMore.value = false;
|
||||
loadingMore.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle edit
|
||||
const handleEdit = (update) => {
|
||||
navigateTo(`/updates/${update._id}/edit`);
|
||||
};
|
||||
navigateTo(`/updates/${update._id}/edit`)
|
||||
}
|
||||
|
||||
// Handle delete
|
||||
const handleDelete = (update) => {
|
||||
updateToDelete.value = update;
|
||||
showDeleteModal.value = true;
|
||||
};
|
||||
updateToDelete.value = update
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
const confirmDelete = async () => {
|
||||
if (!updateToDelete.value) return;
|
||||
if (!updateToDelete.value) return
|
||||
|
||||
deleting.value = true;
|
||||
deleting.value = true
|
||||
try {
|
||||
await $fetch(`/api/updates/${updateToDelete.value._id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
// Remove from list
|
||||
updates.value = updates.value.filter(
|
||||
(u) => u._id !== updateToDelete.value._id,
|
||||
);
|
||||
total.value--;
|
||||
)
|
||||
total.value--
|
||||
|
||||
showDeleteModal.value = false;
|
||||
updateToDelete.value = null;
|
||||
showDeleteModal.value = false
|
||||
updateToDelete.value = null
|
||||
} catch (error) {
|
||||
console.error("Failed to delete update:", error);
|
||||
alert("Failed to delete update. Please try again.");
|
||||
console.error('Failed to delete update:', error)
|
||||
alert('Failed to delete update. Please try again.')
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
deleting.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: "My Updates - Ghost Guild",
|
||||
});
|
||||
title: 'My Updates - Ghost Guild',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ---- TWO-COLUMN LAYOUT ---- */
|
||||
.content-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 200px;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ---- STATS ROW ---- */
|
||||
.stats-row {
|
||||
padding: 16px 32px;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats-count {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.stats-count strong {
|
||||
color: var(--text-bright);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* ---- STATE BOXES (loading, empty, unauth) ---- */
|
||||
.state-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 1px dashed var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.state-heading {
|
||||
font-family: 'Brygada 1918', serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-bright);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 20px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px dashed var(--candle);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ---- TIMELINE ---- */
|
||||
.timeline-wrap {
|
||||
padding: 24px 32px 48px;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
border-left: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.tl-item {
|
||||
position: relative;
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
.tl-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.tl-dot {
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
top: 2px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg);
|
||||
border: 1px dashed var(--border);
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.tl-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-faint);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tl-text {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tl-title {
|
||||
color: var(--text-bright);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tl-title:hover {
|
||||
color: var(--candle);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tl-edited {
|
||||
font-size: 11px;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.tl-detail {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 4px;
|
||||
padding: 8px 12px;
|
||||
border-left: 2px solid var(--border);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tl-images {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tl-image {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
border: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.tl-actions {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tl-action-btn {
|
||||
font-family: 'Commit Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-faint);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.tl-action-btn:hover {
|
||||
color: var(--candle);
|
||||
}
|
||||
|
||||
.tl-action-danger:hover {
|
||||
color: var(--ember);
|
||||
}
|
||||
|
||||
.tl-action-sep {
|
||||
color: var(--border);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ---- LOAD MORE ---- */
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* ---- MODAL ---- */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(42, 32, 21, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.modal-box {
|
||||
background: var(--bg);
|
||||
border: 1px dashed var(--border);
|
||||
padding: 28px 32px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.modal-heading {
|
||||
font-family: 'Brygada 1918', serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-bright);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ---- RESPONSIVE ---- */
|
||||
@media (max-width: 1024px) {
|
||||
.content-area {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-row {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.timeline-wrap {
|
||||
padding: 20px 20px 40px;
|
||||
}
|
||||
|
||||
.state-box {
|
||||
padding: 48px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue