diff --git a/backend/app.go b/backend/app.go index 09eefa1..79391e8 100644 --- a/backend/app.go +++ b/backend/app.go @@ -82,10 +82,47 @@ func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { // @Summary Lists all topics stored in the database // @Description All topics will be listed. There is currently no pagination implemented. // @ID list-topics +// @Param filters query string false "JSON array of filter strings" // @Router /topics [get] // @Success 200 {array} Topic func (a *App) listTopics(w http.ResponseWriter, r *http.Request) { - respondWithJSON(w, http.StatusOK, a.CurrentTopics) + filtersParam := r.URL.Query().Get("filters") + + if filtersParam == "" { + respondWithJSON(w, http.StatusOK, a.CurrentTopics) + return + } + + var filters []string + err := json.Unmarshal([]byte(filtersParam), &filters) + if err != nil { + log.Warn().Msgf("could not parse filters parameter: %s", err) + respondWithJSON(w, http.StatusOK, a.CurrentTopics) + return + } + + if len(filters) == 0 { + respondWithJSON(w, http.StatusOK, a.CurrentTopics) + return + } + + // Filter topics + var filteredTopics []Topic + for _, topic := range a.CurrentTopics { + searchText := strings.ToLower(topic.Title + " [" + topic.Offer.DealerName + "]") + matchesAll := true + for _, filter := range filters { + if !strings.Contains(searchText, strings.ToLower(filter)) { + matchesAll = false + break + } + } + if matchesAll { + filteredTopics = append(filteredTopics, topic) + } + } + + respondWithJSON(w, http.StatusOK, filteredTopics) } // getTopicDetails godoc diff --git a/src/App.vue b/src/App.vue index 9a0274c..9cf6d8e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -54,7 +54,8 @@ function hashString(str) { export default { data() { return { - filter: decodeURIComponent(window.location.href.split("filter=")[1] || ""), + filterInput: "", + activeFilters: this.parseFiltersFromUrl(), sortMethod: "score", topics: [], isMobile: false, @@ -85,11 +86,12 @@ export default { computed: { filteredTopics() { - const filterTerm = this.filter.toLowerCase(); + const filterTerms = this.activeFilters.map(f => f.toLowerCase()); const filtered = this.topics.filter((row) => { + if (filterTerms.length === 0) return true; const searchText = `${row.title} [${row.Offer.dealer_name}]`.toLowerCase(); - return searchText.includes(filterTerm); + return filterTerms.every(term => searchText.includes(term)); }); const sortFns = { @@ -136,15 +138,19 @@ export default { }, highlightText(text) { - if (!this.filter) return text; + if (!this.activeFilters || this.activeFilters.length === 0) return text; - const lowerText = text.toLowerCase(); - const lowerFilter = this.filter.toLowerCase(); + let result = text; + for (const filter of this.activeFilters) { + const lowerText = result.toLowerCase(); + const lowerFilter = filter.toLowerCase(); - if (!lowerText.includes(lowerFilter)) return text; - - const regex = new RegExp(this.filter, "ig"); - return text.replace(regex, (match) => `${match}`); + if (lowerText.includes(lowerFilter)) { + const regex = new RegExp(filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), "ig"); + result = result.replace(regex, (match) => `${match}`); + } + } + return result; }, initializeTheme() { @@ -211,13 +217,58 @@ export default { if (event.key === "/" && !isInput) { event.preventDefault(); - this.$refs.filter.focus(); + this.$refs.filterInput.focus(); } }, - createFilterRoute(params) { - this.$refs.filter.blur(); - history.pushState({}, null, `${window.location.origin}#/filter=${encodeURIComponent(params)}`); + parseFiltersFromUrl() { + const hash = window.location.hash || ""; + const match = hash.match(/filters=([^&]*)/); + if (match && match[1]) { + try { + const decoded = decodeURIComponent(match[1]); + return JSON.parse(decoded); + } catch (e) { + return []; + } + } + // Legacy single filter support + const legacyMatch = hash.match(/filter=([^&]*)/); + if (legacyMatch && legacyMatch[1]) { + const decoded = decodeURIComponent(legacyMatch[1]); + return decoded ? [decoded] : []; + } + return []; + }, + + updateUrlWithFilters() { + if (this.activeFilters.length > 0) { + const encoded = encodeURIComponent(JSON.stringify(this.activeFilters)); + history.pushState({}, null, `${window.location.origin}#/filters=${encoded}`); + } else { + history.pushState({}, null, window.location.origin); + } + }, + + applyFilter() { + const trimmed = this.filterInput.trim(); + if (trimmed && !this.activeFilters.includes(trimmed)) { + this.activeFilters.push(trimmed); + this.filterInput = ""; + this.$refs.filterInput.blur(); + this.updateUrlWithFilters(); + } + }, + + clearFilter(index) { + this.activeFilters.splice(index, 1); + this.updateUrlWithFilters(); + }, + + clearAllFilters() { + this.activeFilters = []; + this.filterInput = ""; + this.updateUrlWithFilters(); }, fetchDeals() { @@ -280,15 +331,23 @@ export default {