diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..763301f
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,2 @@
+dist/
+node_modules/
\ No newline at end of file
diff --git a/src/App.vue b/src/App.vue
index 92c02b0..b441d95 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -3,157 +3,178 @@ import axios from "axios";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
-import "vue-loading-overlay/dist/css/index.css";
import "./theme.css";
-// Configure day.js with UTC support
dayjs.extend(utc);
export default {
data() {
return {
- ascending: this.ascending,
filter: decodeURIComponent(window.location.href.split("filter=")[1] || ""),
- sortColumn: this.sortColumn,
- sortMethod: 'score',
+ sortMethod: "score",
topics: [],
isMobile: false,
- currentTheme: typeof localStorage !== 'undefined' ? (localStorage.getItem('theme') || 'auto') : 'auto',
- mediaQueryListener: null,
- vuetifyTheme: null,
+ currentTheme: "auto",
darkModeQuery: null,
themeChangeHandler: null,
};
},
+
mounted() {
window.addEventListener("keydown", this.handleKeyDown);
+ window.addEventListener("resize", this.handleResize);
this.detectMobile();
this.fetchDeals();
- // Initialize sort method from local storage
this.initializeSortMethod();
- // Initialize theme immediately to prevent flash
this.initializeTheme();
this.setupThemeListener();
},
+
beforeUnmount() {
window.removeEventListener("keydown", this.handleKeyDown);
- window.removeEventListener("resize", this.detectMobile);
+ window.removeEventListener("resize", this.handleResize);
if (this.darkModeQuery && this.themeChangeHandler) {
- this.darkModeQuery.removeEventListener('change', this.themeChangeHandler);
+ this.darkModeQuery.removeEventListener("change", this.themeChangeHandler);
}
},
- methods: {
- initializeTheme() {
- // If no saved preference, default to auto
- const savedTheme = localStorage.getItem('theme');
- if (!savedTheme) {
- this.currentTheme = 'auto';
- this.applyTheme('auto', true); // skipSave=true to avoid redundant write
- } else {
- this.currentTheme = savedTheme;
- // Apply saved theme (skipSave=true since it's already saved)
- this.applyTheme(savedTheme, true);
- }
+
+ computed: {
+ filteredTopics() {
+ const filterTerm = this.filter.toLowerCase();
+
+ const filtered = this.topics.filter((row) => {
+ const searchText = `${row.title} [${row.Offer.dealer_name}]`.toLowerCase();
+ return searchText.includes(filterTerm);
+ });
+
+ const sortFns = {
+ score: (a, b) => b.score - a.score,
+ views: (a, b) => b.total_views - a.total_views,
+ recency: (a, b) => new Date(b.last_post_time) - new Date(a.last_post_time),
+ };
+
+ return filtered.sort(sortFns[this.sortMethod] || sortFns.score);
},
+
+ themeIcon() {
+ const icons = { auto: "brightness_auto", dark: "light_mode", light: "dark_mode" };
+ return icons[this.currentTheme];
+ },
+
+ themeTitle() {
+ const titles = {
+ auto: "Theme: Auto (click for Light)",
+ light: "Theme: Light (click for Dark)",
+ dark: "Theme: Dark (click for Auto)",
+ };
+ return titles[this.currentTheme];
+ },
+
+ sortIcon() {
+ const icons = { score: "trending_up", views: "visibility", recency: "schedule" };
+ return icons[this.sortMethod];
+ },
+
+ sortTitle() {
+ const titles = {
+ score: "Sort by Score (click for Views)",
+ views: "Sort by Views (click for Recency)",
+ recency: "Sort by Recency (click for Score)",
+ };
+ return titles[this.sortMethod];
+ },
+ },
+
+ methods: {
+ formatDate(dateString) {
+ return dayjs(String(dateString)).format("YYYY-MM-DD hh:mm A");
+ },
+
+ highlightText(text) {
+ if (!this.filter) return text;
+
+ const lowerText = text.toLowerCase();
+ const lowerFilter = this.filter.toLowerCase();
+
+ if (!lowerText.includes(lowerFilter)) return text;
+
+ const regex = new RegExp(this.filter, "ig");
+ return text.replace(regex, (match) => `${match}`);
+ },
+
+ initializeTheme() {
+ const savedTheme = localStorage.getItem("theme") || "auto";
+ this.currentTheme = savedTheme;
+ this.applyTheme(savedTheme, true);
+ },
+
setupThemeListener() {
- // Listen for system theme preference changes
- const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ this.darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
- this.mediaQueryListener = darkModeQuery;
-
- // Use arrow function to preserve 'this' context
- const themeChangeHandler = (e) => {
- // Only auto-update theme if set to 'auto'
- const savedTheme = localStorage.getItem('theme');
- if (savedTheme === 'auto' || !savedTheme) {
- const newTheme = e.matches ? 'dark' : 'light';
- console.log('System theme changed to:', newTheme);
- this.applyThemeActual(newTheme);
+ this.themeChangeHandler = (e) => {
+ const savedTheme = localStorage.getItem("theme");
+ if (savedTheme === "auto" || !savedTheme) {
+ this.applyThemeActual(e.matches ? "dark" : "light");
}
};
- darkModeQuery.addEventListener('change', themeChangeHandler);
- // Store the handler so we can remove it later if needed
- this.themeChangeHandler = themeChangeHandler;
- this.darkModeQuery = darkModeQuery;
+ this.darkModeQuery.addEventListener("change", this.themeChangeHandler);
},
+
applyTheme(theme, skipSave = false) {
this.currentTheme = theme;
+
if (!skipSave) {
- localStorage.setItem('theme', theme);
+ localStorage.setItem("theme", theme);
}
- // Determine actual theme to apply
let actualTheme = theme;
- if (theme === 'auto') {
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
- actualTheme = prefersDark ? 'dark' : 'light';
+ if (theme === "auto") {
+ actualTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
this.applyThemeActual(actualTheme);
},
- applyThemeActual(actualTheme) {
- // Update data-bs-theme attribute for CSS variables to work
- document.documentElement.setAttribute('data-bs-theme', actualTheme === 'dark' ? 'dark' : 'light');
- // Update HTML class for theme-based CSS selectors
- if (actualTheme === 'dark') {
- document.documentElement.classList.add('dark-theme');
- document.documentElement.classList.remove('light-theme');
- } else {
- document.documentElement.classList.add('light-theme');
- document.documentElement.classList.remove('dark-theme');
- }
+ applyThemeActual(theme) {
+ document.documentElement.setAttribute("data-bs-theme", theme);
+ document.documentElement.classList.toggle("dark-theme", theme === "dark");
+ document.documentElement.classList.toggle("light-theme", theme === "light");
},
+
toggleTheme() {
- // Cycle through: auto -> light -> dark -> auto
- let newTheme;
- if (this.currentTheme === 'auto') {
- newTheme = 'light';
- } else if (this.currentTheme === 'light') {
- newTheme = 'dark';
- } else {
- newTheme = 'auto';
- }
- this.applyTheme(newTheme);
+ const cycle = { auto: "light", light: "dark", dark: "auto" };
+ this.applyTheme(cycle[this.currentTheme]);
},
+
detectMobile() {
- // Detect if device is mobile/tablet based on touch capability and screen size
- const hasTouch = () => {
- return (
- (typeof window !== "undefined" &&
- ("ontouchstart" in window ||
- navigator.maxTouchPoints > 0 ||
- navigator.msMaxTouchPoints > 0)) ||
- false
- );
- };
+ const hasTouch =
+ "ontouchstart" in window ||
+ navigator.maxTouchPoints > 0 ||
+ navigator.msMaxTouchPoints > 0;
- const isMobileScreen = () => {
- return window.innerWidth <= 1024;
- };
-
- this.isMobile = hasTouch() || isMobileScreen();
- window.addEventListener("resize", this.detectMobile);
+ const isMobileScreen = window.innerWidth <= 1024;
+ this.isMobile = hasTouch || isMobileScreen;
},
+
+ handleResize() {
+ this.detectMobile();
+ },
+
handleKeyDown(event) {
- const isInput = ["INPUT", "TEXTAREA"].includes(
- document.activeElement.tagName
- );
+ const isInput = ["INPUT", "TEXTAREA"].includes(document.activeElement.tagName);
+
if (event.key === "/" && !isInput) {
- event.preventDefault(); // prevent typing `/` into whatever is focused
+ event.preventDefault();
this.$refs.filter.focus();
}
},
createFilterRoute(params) {
this.$refs.filter.blur();
- history.pushState(
- {},
- null,
- `${window.location.origin}#/filter=${encodeURIComponent(params)}`
- );
+ history.pushState({}, null, `${window.location.origin}#/filter=${encodeURIComponent(params)}`);
},
+
fetchDeals() {
axios
.get("api/v1/topics")
@@ -161,111 +182,21 @@ export default {
this.topics = response.data;
})
.catch((err) => {
- console.log(err.response);
+ console.error("Failed to fetch deals:", err.response || err);
});
},
- },
- computed: {
- formatDate() {
- return (v) => {
- const date = dayjs(String(v));
- return date.format("YYYY-MM-DD hh:mm A");
- };
- },
- filteredTopics() {
- let filtered = this.topics
- .filter((row) => {
- const titles = (
- row.title.toString() +
- " [" +
- row.Offer.dealer_name +
- "]"
- ).toLowerCase();
- const filterTerm = this.filter.toLowerCase();
- return titles.includes(filterTerm);
- });
- // Sort based on selected method
- if (this.sortMethod === 'score') {
- return filtered.sort((a, b) => b.score - a.score);
- } else if (this.sortMethod === 'views') {
- return filtered.sort((a, b) => b.total_views - a.total_views);
- } else {
- return filtered.sort((a, b) => new Date(b.last_post_time) - new Date(a.last_post_time));
- }
- },
- highlightMatches() {
- return (v) => {
- if (this.filter == "") return v;
- const matchExists = v.toLowerCase().includes(this.filter.toLowerCase());
- if (!matchExists) return v;
-
- const re = new RegExp(this.filter, "ig");
- return v.replace(re, (matchedText) => `${matchedText}`);
- };
- },
- highlightDealerName() {
- return (dealerName) => {
- if (this.filter == "") return dealerName;
- const matchExists = dealerName.toLowerCase().includes(this.filter.toLowerCase());
- if (!matchExists) return dealerName;
-
- const re = new RegExp(this.filter, "ig");
- return dealerName.replace(re, (matchedText) => `${matchedText}`);
- };
- },
- getThemeIcon() {
- if (this.currentTheme === 'auto') {
- return 'brightness_auto';
- } else if (this.currentTheme === 'dark') {
- return 'light_mode';
- } else {
- return 'dark_mode';
- }
- },
- getThemeTitle() {
- if (this.currentTheme === 'auto') {
- return 'Theme: Auto (click for Light)';
- } else if (this.currentTheme === 'light') {
- return 'Theme: Light (click for Dark)';
- } else {
- return 'Theme: Dark (click for Auto)';
- }
- },
initializeSortMethod() {
- const saved = localStorage.getItem('sortMethod');
+ const saved = localStorage.getItem("sortMethod");
if (saved) {
this.sortMethod = saved;
}
},
+
toggleSort() {
- // Cycle through: score -> views -> recency -> score
- if (this.sortMethod === 'score') {
- this.sortMethod = 'views';
- } else if (this.sortMethod === 'views') {
- this.sortMethod = 'recency';
- } else {
- this.sortMethod = 'score';
- }
- localStorage.setItem('sortMethod', this.sortMethod);
- },
- getSortIcon() {
- if (this.sortMethod === 'score') {
- return 'trending_up';
- } else if (this.sortMethod === 'views') {
- return 'visibility';
- } else {
- return 'schedule';
- }
- },
- getSortTitle() {
- if (this.sortMethod === 'score') {
- return 'Sort by Score (click for Views)';
- } else if (this.sortMethod === 'views') {
- return 'Sort by Views (click for Recency)';
- } else {
- return 'Sort by Recency (click for Score)';
- }
+ const cycle = { score: "views", views: "recency", recency: "score" };
+ this.sortMethod = cycle[this.sortMethod];
+ localStorage.setItem("sortMethod", this.sortMethod);
},
},
};
@@ -273,82 +204,80 @@ export default {