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:
@@ -3,8 +3,7 @@
|
||||
<h2>User Profile</h2>
|
||||
|
||||
<!-- Profile Information Card -->
|
||||
<div class="card">
|
||||
<h3>Profile Information</h3>
|
||||
<CollapsibleSection title="Profile Information" :icon="UserIcon">
|
||||
<div v-if="user" class="profile-info">
|
||||
<div class="info-item">
|
||||
<label>Email:</label>
|
||||
@@ -23,12 +22,10 @@
|
||||
<span>{{ user.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Timezone Preferences Card -->
|
||||
<div class="card">
|
||||
<h3>Timezone Preferences</h3>
|
||||
|
||||
<CollapsibleSection title="Timezone Preferences" :icon="Globe">
|
||||
<div v-if="loadingTimezones" class="loading">Loading timezones...</div>
|
||||
|
||||
<div v-else class="timezone-settings">
|
||||
@@ -56,18 +53,16 @@
|
||||
<div v-if="timezoneSuccess" class="success">{{ timezoneSuccess }}</div>
|
||||
<div v-if="timezoneError" class="error">{{ timezoneError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Google Calendar Integration Card -->
|
||||
<div class="card">
|
||||
<h3>Google Calendar Integration</h3>
|
||||
|
||||
<CollapsibleSection title="Google Calendar Integration" :icon="CalendarDays">
|
||||
<div v-if="loadingGoogleStatus" class="loading">Checking connection status...</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="googleStatus.connected" class="google-connected">
|
||||
<div class="status-indicator">
|
||||
<span class="status-icon">✓</span>
|
||||
<CheckCircle :size="20" class="status-icon-connected" />
|
||||
<span>Connected to Google Calendar</span>
|
||||
</div>
|
||||
|
||||
@@ -103,12 +98,11 @@
|
||||
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div v-if="success" class="success">{{ success }}</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="card info-card">
|
||||
<h4>About Calendar Integration</h4>
|
||||
<ul>
|
||||
<CollapsibleSection title="About Calendar Integration" :icon="Info">
|
||||
<ul class="info-list">
|
||||
<li>
|
||||
<strong>Automatic Sync:</strong> When your booking is approved, it's automatically added to
|
||||
your Google Calendar
|
||||
@@ -122,7 +116,7 @@
|
||||
above
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -131,6 +125,8 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { usersApi, googleCalendarApi, handleApiError } from '@/services/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { formatDateTime as formatDateTimeUtil } from '@/utils/datetime'
|
||||
import CollapsibleSection from '@/components/CollapsibleSection.vue'
|
||||
import { User as UserIcon, Globe, CalendarDays, CheckCircle, Info } from 'lucide-vue-next'
|
||||
import type { User } from '@/types'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -184,7 +180,6 @@ const updateTimezone = async () => {
|
||||
try {
|
||||
await usersApi.updateTimezone(selectedTimezone.value)
|
||||
|
||||
// Update auth store
|
||||
if (authStore.user) {
|
||||
authStore.user.timezone = selectedTimezone.value
|
||||
}
|
||||
@@ -195,7 +190,6 @@ const updateTimezone = async () => {
|
||||
}, 5000)
|
||||
} catch (err) {
|
||||
timezoneError.value = handleApiError(err)
|
||||
// Revert selection on error
|
||||
if (user.value) {
|
||||
selectedTimezone.value = user.value.timezone
|
||||
}
|
||||
@@ -223,21 +217,17 @@ const connectGoogle = async () => {
|
||||
try {
|
||||
const response = await googleCalendarApi.connect()
|
||||
|
||||
// Open OAuth URL in popup window
|
||||
const popup = window.open(
|
||||
response.authorization_url,
|
||||
'Google Calendar Authorization',
|
||||
'width=600,height=600,toolbar=no,menubar=no,location=no'
|
||||
)
|
||||
|
||||
// Poll for connection status
|
||||
const pollInterval = setInterval(async () => {
|
||||
// Check if popup was closed
|
||||
if (popup && popup.closed) {
|
||||
clearInterval(pollInterval)
|
||||
connecting.value = false
|
||||
|
||||
// Check status one more time
|
||||
await checkGoogleStatus()
|
||||
|
||||
if (googleStatus.value.connected) {
|
||||
@@ -247,7 +237,6 @@ const connectGoogle = async () => {
|
||||
}, 3000)
|
||||
}
|
||||
} else {
|
||||
// Poll for connection status
|
||||
try {
|
||||
const status = await googleCalendarApi.status()
|
||||
if (status.connected) {
|
||||
@@ -256,7 +245,6 @@ const connectGoogle = async () => {
|
||||
googleStatus.value = status
|
||||
success.value = 'Google Calendar connected successfully!'
|
||||
|
||||
// Close popup
|
||||
if (popup) {
|
||||
popup.close()
|
||||
}
|
||||
@@ -271,7 +259,6 @@ const connectGoogle = async () => {
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
// Stop polling after 5 minutes
|
||||
setTimeout(() => {
|
||||
clearInterval(pollInterval)
|
||||
connecting.value = false
|
||||
@@ -328,22 +315,7 @@ onMounted(() => {
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h3,
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #444;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
@@ -360,17 +332,17 @@ h4 {
|
||||
.info-item label {
|
||||
font-weight: 600;
|
||||
min-width: 120px;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.google-connected,
|
||||
@@ -386,29 +358,21 @@ h4 {
|
||||
gap: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: #4caf50;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 0.9rem;
|
||||
.status-icon-connected {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.expiry-info {
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -420,35 +384,35 @@ h4 {
|
||||
|
||||
.benefits-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: all var(--transition-fast);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4285f4;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #357ae8;
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background-color: #c82333;
|
||||
background: color-mix(in srgb, var(--color-danger) 85%, black);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@@ -458,34 +422,32 @@ h4 {
|
||||
|
||||
.error {
|
||||
padding: 0.75rem;
|
||||
background-color: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 4px;
|
||||
color: #c33;
|
||||
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-danger);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
padding: 0.75rem;
|
||||
background-color: #efe;
|
||||
border: 1px solid #cfc;
|
||||
border-radius: 4px;
|
||||
color: #3c3;
|
||||
background: color-mix(in srgb, var(--color-success) 10%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-success);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.info-card ul {
|
||||
.info-list {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.info-card li {
|
||||
.info-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.info-list strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.timezone-settings {
|
||||
@@ -502,16 +464,18 @@ h4 {
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.timezone-select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
max-width: 400px;
|
||||
cursor: pointer;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.timezone-select:disabled {
|
||||
@@ -520,9 +484,13 @@ h4 {
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.collapsible-section + .collapsible-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user