Remove vuetify and add theme toggle button

This commit is contained in:
2026-02-14 22:58:50 -05:00
parent d523c31953
commit ea871e3fb4
8 changed files with 103 additions and 244 deletions

View File

@@ -19,8 +19,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-loading-overlay": "^6.0.3", "vue-loading-overlay": "^6.0.3",
"vue-router": "^5.0.0", "vue-router": "^5.0.0"
"vuetify": "^3.9.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.10", "@babel/core": "^7.22.10",
@@ -34,8 +33,7 @@
"postcss-cli": "^11.0.0", "postcss-cli": "^11.0.0",
"sass-embedded": "^1.89.2", "sass-embedded": "^1.89.2",
"unplugin-vue-components": "^31.0.0", "unplugin-vue-components": "^31.0.0",
"vite": "^6.3.6", "vite": "^6.3.6"
"vite-plugin-vuetify": "^2.1.1"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

View File

@@ -28,7 +28,7 @@ export default {
window.addEventListener("keydown", this.handleKeyDown); window.addEventListener("keydown", this.handleKeyDown);
this.detectMobile(); this.detectMobile();
this.fetchDeals(); this.fetchDeals();
// Initialize theme on next tick to ensure Vuetify is ready // Initialize theme on next tick
this.$nextTick(() => { this.$nextTick(() => {
this.initializeTheme(); this.initializeTheme();
this.setupThemeListener(); this.setupThemeListener();
@@ -44,14 +44,15 @@ export default {
methods: { methods: {
initializeTheme() { initializeTheme() {
// If no saved preference, apply system preference now // If no saved preference, apply system preference now
const savedTheme = localStorage.getItem('vuetify-theme'); const savedTheme = localStorage.getItem('theme');
if (!savedTheme) { if (!savedTheme) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = prefersDark ? 'dark' : 'light'; const theme = prefersDark ? 'dark' : 'light';
this.applyTheme(theme); this.applyTheme(theme);
} else { } else {
// Get current theme name from Vuetify this.currentTheme = savedTheme;
this.currentTheme = this.$vuetify.theme.global.name; // Apply saved theme
this.applyTheme(savedTheme);
} }
}, },
setupThemeListener() { setupThemeListener() {
@@ -63,7 +64,7 @@ export default {
// Use arrow function to preserve 'this' context // Use arrow function to preserve 'this' context
const themeChangeHandler = (e) => { const themeChangeHandler = (e) => {
// Only auto-update theme if user hasn't set a preference manually // Only auto-update theme if user hasn't set a preference manually
const savedTheme = localStorage.getItem('vuetify-theme'); const savedTheme = localStorage.getItem('theme');
if (!savedTheme) { if (!savedTheme) {
const newTheme = e.matches ? 'dark' : 'light'; const newTheme = e.matches ? 'dark' : 'light';
console.log('System theme changed to:', newTheme); console.log('System theme changed to:', newTheme);
@@ -77,10 +78,8 @@ export default {
this.darkModeQuery = darkModeQuery; this.darkModeQuery = darkModeQuery;
}, },
applyTheme(theme) { applyTheme(theme) {
// Apply theme using Vuetify's theme API (using .value for reactive ref)
this.$vuetify.theme.global.name.value = theme;
this.currentTheme = theme; this.currentTheme = theme;
localStorage.setItem('vuetify-theme', theme); localStorage.setItem('theme', theme);
// Update data-bs-theme attribute for CSS variables to work // Update data-bs-theme attribute for CSS variables to work
document.documentElement.setAttribute('data-bs-theme', theme === 'dark' ? 'dark' : 'light'); document.documentElement.setAttribute('data-bs-theme', theme === 'dark' ? 'dark' : 'light');
@@ -192,20 +191,24 @@ export default {
</script> </script>
<template> <template>
<v-app> <div id="app">
<v-main>
<link rel="shortcut icon" type="image/png" href="/favicon.png" /> <link rel="shortcut icon" type="image/png" href="/favicon.png" />
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<v-text-field <div class="header-controls">
<input
v-model="filter" v-model="filter"
label="Filter deals" type="text"
placeholder="Filter deals"
ref="filter" ref="filter"
@keyup.enter="createFilterRoute(filter.toString())" @keyup.enter="createFilterRoute(filter.toString())"
@keyup.esc="$refs.filter.blur()" @keyup.esc="$refs.filter.blur()"
hide-details="true"
class="search-input" class="search-input"
/> />
<button @click="toggleTheme" class="theme-toggle" :title="'Switch to ' + (currentTheme === 'dark' ? 'light' : 'dark') + ' theme'">
<span class="material-symbols-outlined">{{ currentTheme === 'dark' ? 'light_mode' : 'dark_mode' }}</span>
</button>
</div>
</div> </div>
<div class="cards-grid"> <div class="cards-grid">
@@ -263,8 +266,5 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</v-main> </div>
</v-app>
</template> </template>

View File

@@ -4,8 +4,6 @@ import { createRouter, createWebHashHistory } from "vue-router";
import "./theme.css"; import "./theme.css";
import { registerPlugins } from "@/plugins";
const routes = [ const routes = [
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
@@ -20,7 +18,5 @@ const router = createRouter({
const app = createApp(App); const app = createApp(App);
registerPlugins(app);
app.use(router); app.use(router);
app.mount("#app"); app.mount("#app");

View File

@@ -1,3 +0,0 @@
# Plugins
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.

View File

@@ -1,12 +0,0 @@
/**
* plugins/index.js
*
* Automatically included in `./src/main.js`
*/
// Plugins
import vuetify from './vuetify'
export function registerPlugins (app) {
app.use(vuetify)
}

View File

@@ -1,68 +0,0 @@
/**
* plugins/vuetify.js
*
* Framework documentation: https://vuetifyjs.com
*/
// Styles
import "@mdi/font/css/materialdesignicons.css";
import "vuetify/styles";
// Composables
import { createVuetify } from "vuetify";
const lightTheme = {
dark: false,
colors: {
background: "#ffffff",
surface: "#f5f5f5",
primary: "#1976d2",
secondary: "#424242",
accent: "#82b1ff",
error: "#f44336",
info: "#2196f3",
success: "#4caf50",
warning: "#ff9800",
},
};
const darkTheme = {
dark: true,
colors: {
background: "#1a1a1a",
surface: "#2a2a2a",
primary: "#5b9cf5",
secondary: "#a0a0a0",
accent: "#7aa2f7",
error: "#f87171",
info: "#60a5fa",
success: "#4ade80",
warning: "#facc15",
},
};
function getDefaultTheme() {
// Check for saved theme preference
const savedTheme = localStorage.getItem('vuetify-theme');
if (savedTheme) {
return savedTheme;
}
// Check system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light';
}
// Create vuetify instance
const vuetify = createVuetify({
theme: {
defaultTheme: getDefaultTheme(),
themes: {
light: lightTheme,
dark: darkTheme,
},
},
});
// Export the vuetify instance so other parts of the app can access it
export { vuetify as default };

View File

@@ -42,7 +42,7 @@ html[data-bs-theme="dark"] {
--text-primary: #e0e0e0; --text-primary: #e0e0e0;
--text-secondary: #a0a0a0; --text-secondary: #a0a0a0;
--border-color: #3a3a3a; --border-color: #3a3a3a;
--link-color: ##e0e0e0; --link-color: #e0e0e0;
} }
html[data-bs-theme="light"], html[data-bs-theme="light"],
@@ -98,76 +98,7 @@ a:visited {
color: var(--link-color); color: var(--link-color);
} }
@media (min-width: 769px) { /* App styles */
.v-data-table-header th,
.v-data-table__td {
border-color: #888888 !important;
}
}
/* Vuetify table dark theme overrides */
html.dark-theme .v-data-table {
background-color: #1a1a1a !important;
color: #e0e0e0 !important;
}
html.dark-theme .v-data-table__tr {
background-color: #1a1a1a !important;
color: #e0e0e0 !important;
}
html.dark-theme .v-data-table__td {
background-color: #1a1a1a !important;
color: #e0e0e0 !important;
border-color: #888888 !important;
}
html.dark-theme .v-data-table-header {
background-color: #2a2a2a !important;
}
html.dark-theme .v-data-table-header th {
background-color: #2a2a2a !important;
color: #e0e0e0 !important;
border-color: #888888 !important;
}
html.dark-theme .v-data-table__divider {
border-color: #888888 !important;
}
html.dark-theme .v-table__wrapper {
background-color: #1a1a1a !important;
}
/* Light theme table overrides */
html.light-theme .v-data-table {
background-color: #ffffff !important;
color: #212529 !important;
}
html.light-theme .v-data-table__tr {
background-color: #ffffff !important;
color: #212529 !important;
}
html.light-theme .v-data-table__td {
background-color: #ffffff !important;
color: #212529 !important;
border-color: #dee2e6 !important;
}
html.light-theme .v-data-table-header {
background-color: #f5f5f5 !important;
}
html.light-theme .v-data-table-header th {
background-color: #f5f5f5 !important;
color: #212529 !important;
border-color: #dee2e6 !important;
}
/* App.vue scoped styles */
.container { .container {
max-width: 1200px; max-width: 1200px;
@@ -182,9 +113,82 @@ html.light-theme .v-data-table-header th {
margin-bottom: 30px; margin-bottom: 30px;
} }
.header-controls {
display: flex;
gap: 12px;
align-items: center;
}
.search-input { .search-input {
width: 100%; flex: 1;
max-width: 500px; max-width: 500px;
padding: 10px 12px;
font-size: 14px;
border: 1px solid #cccccc;
border-radius: 4px;
background-color: #f5f5f5;
color: var(--text-primary);
transition: all 0.2s ease;
font-family: inherit;
}
.search-input:focus {
outline: none;
border-color: #999999;
background-color: #ffffff;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
}
html.dark-theme .search-input {
border-color: #555555;
background-color: #1a1a1a;
color: #e0e0e0;
}
html.dark-theme .search-input:focus {
background-color: #2a2a2a;
border-color: #777777;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}
.search-input::placeholder {
color: var(--text-secondary);
}
.theme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: 1px solid #cccccc;
border-radius: 4px;
background-color: #f5f5f5;
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s ease;
font-size: 18px;
padding: 0;
}
.theme-toggle:hover {
background-color: #e8e8e8;
border-color: #999999;
}
.theme-toggle:active {
transform: scale(0.95);
}
html.dark-theme .theme-toggle {
border-color: #555555;
background-color: #1a1a1a;
color: #e0e0e0;
}
html.dark-theme .theme-toggle:hover {
background-color: #2a2a2a;
border-color: #777777;
} }
.cards-grid { .cards-grid {
@@ -237,7 +241,7 @@ html.light-theme .v-data-table-header th {
} }
.deal-title:visited { .deal-title:visited {
color: var(--link-visited); color: var(--link-color);
} }
.deal-title:hover { .deal-title:hover {
@@ -404,50 +408,3 @@ html.dark-theme mark {
font-weight: 600; font-weight: 600;
border-radius: 2px; border-radius: 2px;
} }
/* Vuetify overrides */
.v-text-field {
--v-field-border-color: #cccccc;
}
html[data-bs-theme="light"] .v-text-field {
--v-field-border-color: #e8e8e8;
}
html[data-bs-theme="light"] .v-field__input {
background-color: #d0d0d0 !important;
}
html[data-bs-theme="light"] .v-field--focused .v-field__input {
background-color: #e8e8e8 !important;
}
html[data-bs-theme="dark"] .v-text-field {
--v-field-border-color: #555555;
}
html.light-theme .v-text-field {
--v-field-border-color: #cccccc;
}
html.light-theme .v-field__input {
background-color: #d0d0d0 !important;
}
html.light-theme .v-field--focused .v-field__input {
background-color: #e8e8e8 !important;
}
html.dark-theme .v-text-field {
--v-field-border-color: #555555;
}
/* Ensure v-app and v-main use theme colors */
.v-app {
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
}
.v-main {
background-color: var(--bg-primary) !important;
}

View File

@@ -1,7 +1,6 @@
// Plugins // Plugins
import Components from "unplugin-vue-components/vite"; import Components from "unplugin-vue-components/vite";
import Vue from "@vitejs/plugin-vue"; import Vue from "@vitejs/plugin-vue";
import Vuetify, { transformAssetUrls } from "vite-plugin-vuetify";
// Utilities // Utilities
import { defineConfig } from "vite"; import { defineConfig } from "vite";
@@ -10,16 +9,9 @@ import { fileURLToPath, URL } from "node:url";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
Vue({ Vue(),
template: { transformAssetUrls },
}),
Vuetify(),
Components(), Components(),
], ],
optimizeDeps: {
exclude: ["vuetify"],
include: ["axios", "vue-router", "vue-loading-overlay"],
},
define: { "process.env": {} }, define: { "process.env": {} },
resolve: { resolve: {
alias: { alias: {
@@ -54,7 +46,6 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: { manualChunks: {
"vuetify": ["vuetify"],
"vendor": ["axios", "dayjs", "vue-router", "vue-loading-overlay"], "vendor": ["axios", "dayjs", "vue-router", "vue-loading-overlay"],
}, },
chunkFileNames: "js/[name].[hash].js", chunkFileNames: "js/[name].[hash].js",