Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F24865071
D3989.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
9 KB
Referenced Files
None
Subscribers
None
D3989.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D3989: Add dark mode toggle button
Attached
Detach File
Event Timeline
Log In to Comment