Page MenuHomeDevCentral

D3989.diff
No OneTemporary

D3989.diff

diff --git a/frontend/src/components/AppNavbar.vue b/frontend/src/components/AppNavbar.vue
--- a/frontend/src/components/AppNavbar.vue
+++ b/frontend/src/components/AppNavbar.vue
@@ -1,11 +1,14 @@
<script setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, computed } from 'vue'
import { RouterLink } from 'vue-router'
import { configApi } from '@/plugins/api'
import { useAuth } from '@/composables/useAuth'
+import { useDarkMode } from '@/composables/useDarkMode'
const config = ref(null)
const { isAuthenticated, logout } = useAuth()
+const { currentVariant, isToggleable, toggle } = useDarkMode()
+
onMounted(async () => {
try {
@@ -27,42 +30,59 @@
<div class="flex items-center justify-between h-14">
<div class="flex items-center gap-4">
<a
- v-for="btn in config?.navbar?.buttons_left"
- :key="btn.name"
- :href="btn.link"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-for="btn in (config && config.navbar ? config.navbar.buttons_left : [])"
+ :key="btn.name"
+ :href="btn.link"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
{{ btn.name }}
</a>
</div>
- <RouterLink to="/" class="text-sm font-bold tracking-widest uppercase hover:text-gray-300 transition-colors">
- {{ config?.navbar?.title || 'ServPulse' }}
+ <RouterLink to="/" class="flex items-center gap-2 hover:opacity-80 transition-opacity">
+ <span class="text-sm font-bold tracking-widest uppercase">
+ {{ config && config.navbar ? config.navbar.title : 'ServPulse' }}
+ </span>
</RouterLink>
<div class="flex items-center gap-4">
<RouterLink
- v-if="isAuthenticated"
- to="/admin"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-if="isAuthenticated"
+ to="/admin"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Dashboard
</RouterLink>
<button
- v-if="isAuthenticated"
- @click="handleLogout"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-if="isAuthenticated"
+ @click="handleLogout"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Logout
</button>
<RouterLink
- v-else
- to="/admin/login"
- class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
+ v-else
+ to="/admin/login"
+ class="text-xs font-medium text-gray-400 hover:text-white transition-colors"
>
Admin
</RouterLink>
+ <button
+ v-if="isToggleable"
+ @click="toggle"
+ class="text-gray-400 hover:text-white transition-colors"
+ :aria-label="currentVariant === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
+ >
+
+ <svg v-if="currentVariant !== 'dark'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12.79A9 9 0 1111.21 3a7 7 0 109.79 9.79z" />
+ </svg>
+
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M12 8a4 4 0 100 8 4 4 0 000-8z" />
+ </svg>
+ </button>
</div>
</div>
</div>
diff --git a/frontend/src/composables/useDarkMode.js b/frontend/src/composables/useDarkMode.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/composables/useDarkMode.js
@@ -0,0 +1,49 @@
+import { ref, computed } from 'vue'
+
+export const THEMES = [
+ { id: 'standard', label: 'Standard', variants: ['light', 'dark'] },
+ { id: 'solarized', label: 'Solarized', variants: ['light', 'dark'] },
+ { id: 'sepia', label: 'Sepia', variants: ['sepia'] },
+]
+
+const currentThemeId = ref('standard')
+const currentVariant = ref('light')
+
+const isToggleable = computed(() => {
+ const theme = THEMES.find(t => t.id === currentThemeId.value)
+ return theme ? theme.variants.length > 1 : false
+})
+
+function applyTheme(id, variant) {
+ const theme = THEMES.find(t => t.id === id) || THEMES[0]
+ if (!theme.variants.includes(variant)) variant = theme.variants[0]
+
+ const dataTheme = theme.variants.length > 1 ? `${id}-${variant}` : id
+ document.documentElement.setAttribute('data-theme', dataTheme)
+
+ currentThemeId.value = id
+ currentVariant.value = variant
+}
+
+const savedTheme = localStorage.getItem('servpulse_theme') || 'standard'
+const savedVariant = localStorage.getItem('servpulse_variant') || 'light'
+applyTheme(savedTheme, savedVariant)
+
+export function useDarkMode() {
+ function toggle() {
+ if (!isToggleable.value) return
+ const next = currentVariant.value === 'light' ? 'dark' : 'light'
+ applyTheme(currentThemeId.value, next)
+ localStorage.setItem('servpulse_theme', currentThemeId.value)
+ localStorage.setItem('servpulse_variant', next)
+ }
+
+ function setTheme(id) {
+ const theme = THEMES.find(t => t.id === id) || THEMES[0]
+ applyTheme(id, theme.variants[0])
+ localStorage.setItem('servpulse_theme', id)
+ localStorage.setItem('servpulse_variant', theme.variants[0])
+ }
+
+ return { currentThemeId, currentVariant, isToggleable, toggle, setTheme }
+}
diff --git a/frontend/src/views/AdminDashboard.vue b/frontend/src/views/AdminDashboard.vue
--- a/frontend/src/views/AdminDashboard.vue
+++ b/frontend/src/views/AdminDashboard.vue
@@ -6,6 +6,9 @@
import { servicesApi, incidentsApi, maintenancesApi, configApi } from '@/plugins/api'
import StatusBadge from '@/components/StatusBadge.vue'
import { formatDate } from '@/utils/status'
+import { THEMES, useDarkMode } from '@/composables/useDarkMode'
+
+const { setTheme } = useDarkMode()
const activeTab = ref('services')
const tabs = [
@@ -147,7 +150,7 @@
// Settings
const settingsForm = ref({
- navbar: { title: 'ServPulse', buttons_left: [] },
+ navbar: { title: 'ServPulse', theme: 'standard', logo_url: '', buttons_left: [] },
footer: {
line1: { text: '', link1_label: '', link1_url: '', link2_label: '', link2_url: '' },
line2: { text: '', link_label: '', link_url: '' },
@@ -182,6 +185,7 @@
}
try {
await configApi.update(settingsForm.value)
+ setTheme(settingsForm.value.navbar.theme)
settingsSaved.value = true
setTimeout(() => { settingsSaved.value = false }, 3000)
} catch (err) {
@@ -457,12 +461,26 @@
</div>
<form @submit.prevent="saveSettings" class="space-y-6">
+
<!-- Site Title -->
<div class="card p-5">
<h3 class="text-sm font-semibold mb-3">Site Title</h3>
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Navbar Title</label>
<input v-model="settingsForm.navbar.title" class="input-field max-w-sm" placeholder="ServPulse" />
+ </div>
+ </div>
+
+ <!-- Theme-->
+ <div class="card p-5">
+ <h3 class="text-sm font-semibold mb-3">Theme</h3>
+ <div>
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Select Theme</label>
+ <select v-model="settingsForm.navbar.theme" class="input-field max-w-sm">
+ <option v-for="theme in THEMES" :key="theme.id" :value="theme.id">
+ {{ theme.label }} {{ theme.variants.length === 1 ? '(mono)' : '(light + dark)' }}
+ </option>
+ </select>
</div>
</div>
@@ -470,9 +488,9 @@
<div class="card p-5">
<div class="flex justify-between items-center mb-3">
<h3 class="text-sm font-semibold">Navigation Links</h3>
- <button v-if="(settingsForm.navbar.buttons_left?.length || 0) < MAX_NAV_BUTTONS" type="button" @click="addNavButton" class="btn-secondary text-xs">+ Add Link</button>
+ <button v-if="settingsForm.navbar.buttons_left.length < MAX_NAV_BUTTONS" type="button" @click="addNavButton" class="btn-secondary text-xs">+ Add Link</button>
</div>
- <div v-if="settingsForm.navbar.buttons_left?.length" class="space-y-3">
+ <div v-if="settingsForm.navbar.buttons_left.length" class="space-y-3">
<div v-for="(btn, index) in settingsForm.navbar.buttons_left" :key="index" class="flex gap-3 items-end">
<div class="flex-1">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Name</label>
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -5,7 +5,7 @@
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
]
-export const darkMode = 'class'
+export const darkMode = ['class', '[data-theme$="-dark"]']
export const theme = {
extend: {
colors: {

File Metadata

Mime Type
text/plain
Expires
Sun, Mar 15, 21:33 (22 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3533361
Default Alt Text
D3989.diff (9 KB)

Event Timeline