feat: complete UI redesign with dark mode, sidebar navigation, and modern design system

Implemented comprehensive UI overhaul with three-layer architecture:

Layer 1 - Theme System:
- CSS variables for light/dark themes (theme.css)
- Theme composable with light/dark/auto mode (useTheme.ts)
- Sidebar state management composable (useSidebar.ts)
- Refactored main.css to use CSS variables throughout

Layer 2 - Core Components:
- AppSidebar with collapsible navigation (desktop) and overlay (mobile)
- CollapsibleSection reusable component for expandable cards
- Restructured App.vue with new sidebar layout
- Integrated Lucide icons library (lucide-vue-next)

Layer 3 - Views & Components:
- Updated all 14 views with CSS variables and responsive design
- Replaced inline SVG with Lucide icon components
- Added collapsible sections to Dashboard, Admin pages, UserProfile
- Updated 3 shared components (BookingForm, SpaceCalendar, AttachmentsList)

Features:
- Dark/light/auto theme with persistent preference
- Collapsible sidebar (icons-only on desktop, overlay on mobile)
- Consistent color palette using CSS variables
- Full responsive design across all pages
- Modern minimalist aesthetic with Indigo accent color

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-11 21:27:05 +00:00
parent 9c2846cf00
commit 0bf3e6a7e2
28 changed files with 1960 additions and 1641 deletions

View File

@@ -3,8 +3,7 @@
<h2>Admin Dashboard - Pending Booking Requests</h2>
<!-- Filters Card -->
<div class="card">
<h3>Filters</h3>
<CollapsibleSection title="Filters" :icon="Filter">
<div class="filters">
<div class="form-group">
<label for="filter-space">Filter by Space</label>
@@ -16,7 +15,7 @@
</select>
</div>
</div>
</div>
</CollapsibleSection>
<!-- Loading State -->
<div v-if="loading" class="card">
@@ -32,65 +31,66 @@
</div>
<!-- Bookings Table -->
<div v-else class="card">
<h3>Pending Requests ({{ bookings.length }})</h3>
<table class="bookings-table">
<thead>
<tr>
<th>User</th>
<th>Space</th>
<th>Date</th>
<th>Time</th>
<th>Title</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="booking in bookings" :key="booking.id">
<td>
<div class="user-info">
<div class="user-name">{{ booking.user?.full_name || 'Unknown' }}</div>
<div class="user-email">{{ booking.user?.email || '-' }}</div>
<div class="user-org" v-if="booking.user?.organization">
{{ booking.user.organization }}
<CollapsibleSection v-else :title="`Pending Requests (${bookings.length})`" :icon="ClipboardCheck">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>User</th>
<th>Space</th>
<th>Date</th>
<th>Time</th>
<th>Title</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="booking in bookings" :key="booking.id">
<td>
<div class="user-info">
<div class="user-name">{{ booking.user?.full_name || 'Unknown' }}</div>
<div class="user-email">{{ booking.user?.email || '-' }}</div>
<div class="user-org" v-if="booking.user?.organization">
{{ booking.user.organization }}
</div>
</div>
</div>
</td>
<td>
<div class="space-info">
<div class="space-name">{{ booking.space?.name || 'Unknown Space' }}</div>
<div class="space-type">{{ formatType(booking.space?.type || '') }}</div>
</div>
</td>
<td>{{ formatDate(booking.start_datetime) }}</td>
<td>{{ formatTime(booking.start_datetime, booking.end_datetime) }}</td>
<td>{{ booking.title }}</td>
<td>
<div class="description" :title="booking.description || '-'">
{{ truncateText(booking.description || '-', 40) }}
</div>
</td>
<td class="actions">
<button
class="btn btn-sm btn-success"
@click="handleApprove(booking)"
:disabled="processing === booking.id"
>
{{ processing === booking.id ? 'Processing...' : 'Approve' }}
</button>
<button
class="btn btn-sm btn-danger"
@click="showRejectModal(booking)"
:disabled="processing === booking.id"
>
Reject
</button>
</td>
</tr>
</tbody>
</table>
</div>
</td>
<td>
<div class="space-info">
<div class="space-name">{{ booking.space?.name || 'Unknown Space' }}</div>
<div class="space-type">{{ formatType(booking.space?.type || '') }}</div>
</div>
</td>
<td>{{ formatDate(booking.start_datetime) }}</td>
<td>{{ formatTime(booking.start_datetime, booking.end_datetime) }}</td>
<td>{{ booking.title }}</td>
<td>
<div class="description" :title="booking.description || '-'">
{{ truncateText(booking.description || '-', 40) }}
</div>
</td>
<td class="actions">
<button
class="btn btn-sm btn-success"
@click="handleApprove(booking)"
:disabled="processing === booking.id"
>
{{ processing === booking.id ? 'Processing...' : 'Approve' }}
</button>
<button
class="btn btn-sm btn-danger"
@click="showRejectModal(booking)"
:disabled="processing === booking.id"
>
Reject
</button>
</td>
</tr>
</tbody>
</table>
</div>
</CollapsibleSection>
<!-- Reject Modal -->
<div v-if="rejectingBooking" class="modal" @click.self="closeRejectModal">
@@ -134,7 +134,7 @@
<!-- Success Message -->
<div v-if="success" class="card">
<div class="success">{{ success }}</div>
<div class="success-msg">{{ success }}</div>
</div>
</div>
</template>
@@ -144,6 +144,8 @@ import { ref, computed, onMounted } from 'vue'
import { adminBookingsApi, spacesApi, handleApiError } from '@/services/api'
import { useAuthStore } from '@/stores/auth'
import { formatDate as formatDateUtil, formatTime as formatTimeUtil } from '@/utils/datetime'
import CollapsibleSection from '@/components/CollapsibleSection.vue'
import { Filter, ClipboardCheck } from 'lucide-vue-next'
import type { Booking, Space } from '@/types'
const authStore = useAuthStore()
@@ -279,17 +281,24 @@ onMounted(() => {
</script>
<style scoped>
.admin-pending {
max-width: 1600px;
margin: 0 auto;
h2 {
margin-bottom: 24px;
color: var(--color-text-primary);
}
.card {
background: white;
border-radius: 8px;
background: var(--color-surface);
border-radius: var(--radius-md);
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: 16px;
box-shadow: var(--shadow-sm);
border: 1px solid var(--color-border);
}
.collapsible-section + .collapsible-section,
.card + .collapsible-section,
.collapsible-section + .card {
margin-top: 16px;
}
.filters {
@@ -306,22 +315,25 @@ onMounted(() => {
.form-group label {
font-weight: 500;
color: #374151;
color: var(--color-text-primary);
font-size: 14px;
}
.form-group select,
.form-group textarea {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
font-size: 14px;
background: var(--color-surface);
color: var(--color-text-primary);
}
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
border-color: var(--color-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 15%, transparent);
}
.form-actions {
@@ -332,53 +344,58 @@ onMounted(() => {
.loading {
text-align: center;
color: #6b7280;
color: var(--color-text-secondary);
padding: 24px;
}
.empty {
text-align: center;
color: #9ca3af;
color: var(--color-text-muted);
padding: 24px;
}
.error {
padding: 12px;
background: #fee2e2;
color: #991b1b;
border-radius: 4px;
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
color: var(--color-danger);
border-radius: var(--radius-sm);
}
.success {
.success-msg {
padding: 12px;
background: #d1fae5;
color: #065f46;
border-radius: 4px;
background: color-mix(in srgb, var(--color-success) 10%, transparent);
color: var(--color-success);
border-radius: var(--radius-sm);
}
.bookings-table {
.table-responsive {
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.bookings-table th {
.data-table th {
text-align: left;
padding: 12px;
background: #f9fafb;
background: var(--color-bg-secondary);
font-weight: 600;
color: #374151;
border-bottom: 2px solid #e5e7eb;
color: var(--color-text-primary);
border-bottom: 2px solid var(--color-border);
white-space: nowrap;
}
.bookings-table td {
.data-table td {
padding: 12px;
border-bottom: 1px solid #e5e7eb;
border-bottom: 1px solid var(--color-border);
vertical-align: top;
color: var(--color-text-primary);
}
.bookings-table tr:hover {
background: #f9fafb;
.data-table tr:hover {
background: var(--color-surface-hover);
}
.user-info,
@@ -391,14 +408,14 @@ onMounted(() => {
.user-name,
.space-name {
font-weight: 500;
color: #374151;
color: var(--color-text-primary);
}
.user-email,
.user-org,
.space-type {
font-size: 12px;
color: #6b7280;
color: var(--color-text-secondary);
}
.description {
@@ -416,11 +433,11 @@ onMounted(() => {
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
transition: all var(--transition-fast);
}
.btn:disabled {
@@ -434,30 +451,30 @@ onMounted(() => {
}
.btn-success {
background: #10b981;
background: var(--color-success);
color: white;
}
.btn-success:hover:not(:disabled) {
background: #059669;
background: color-mix(in srgb, var(--color-success) 85%, black);
}
.btn-danger {
background: #ef4444;
background: var(--color-danger);
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
background: color-mix(in srgb, var(--color-danger) 85%, black);
}
.btn-secondary {
background: #6b7280;
color: white;
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.btn-secondary:hover:not(:disabled) {
background: #4b5563;
background: var(--color-border);
}
.modal {
@@ -474,22 +491,23 @@ onMounted(() => {
}
.modal-content {
background: white;
border-radius: 8px;
background: var(--color-surface);
border-radius: var(--radius-md);
padding: 24px;
max-width: 600px;
width: 90%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
box-shadow: var(--shadow-lg);
}
.modal-content h3 {
margin-top: 0;
margin-bottom: 16px;
color: var(--color-text-primary);
}
.booking-summary {
background: #f9fafb;
border-radius: 4px;
background: var(--color-bg-secondary);
border-radius: var(--radius-sm);
padding: 12px;
margin-bottom: 16px;
}
@@ -497,10 +515,10 @@ onMounted(() => {
.booking-summary p {
margin: 8px 0;
font-size: 14px;
color: #374151;
color: var(--color-text-secondary);
}
.booking-summary strong {
color: #1f2937;
color: var(--color-text-primary);
}
</style>