Implement global search

This commit is contained in:
2024-10-11 12:59:21 +02:00
parent 49b436d588
commit cfb8fb9d7d
24 changed files with 262 additions and 209 deletions

View File

@@ -59,7 +59,7 @@ band.get("/", (req: Request, res: Response) => {
})
// Get all information about one band
band.get("/:name", (req: Request, res: Response) => {
band.get("/band/:name", (req: Request, res: Response) => {
Band.findOne({
where: {
name: { [Op.like]: req.params.name }
@@ -109,4 +109,19 @@ band.get("/:name", (req: Request, res: Response) => {
.then(band => {
res.status(200).json(band)
})
})
// Band search
band.get("/search", (req: Request, res: Response) => {
Band.findAll({
where: {
name: {
[Op.substring]: req.query.value
}
}
})
.then(bands => {
res.status(200).json(bands)
})
})

View File

@@ -8,10 +8,11 @@ import { SeatRow } from "../models/locations/seatRow.model";
import { Seat } from "../models/locations/seat.model";
import { Ticket } from "../models/ordering/ticket.model";
import { Band } from "../models/acts/band.model";
import { Op } from "sequelize";
export const concert = Router()
concert.get("/:id", (req: Request, res: Response) => {
concert.get("/concert/:id", (req: Request, res: Response) => {
Concert.findByPk(req.params.id, {
include: [
{

View File

@@ -5,6 +5,7 @@ import { Request, Response, Router } from "express";
import { Location } from "../models/locations/location.model";
import { Genre } from "../models/acts/genre.model";
import { City } from "../models/locations/city.model";
import { Op } from "sequelize";
export const events = Router()
@@ -80,5 +81,33 @@ events.get("/", async (req: Request, res: Response) => {
res.status(200).json(events)
})
})
// Event search
events.get("/search", (req: Request, res: Response) => {
Event.findAll({
where: {
name: {
[Op.substring]: req.query.value
}
},
include: [
{
model: Concert,
required: true,
include: [
{
model: Location,
}
],
},
{
model: Band,
}
]
})
.then(events => {
res.status(200).json(events)
})
})

View File

@@ -7,6 +7,7 @@ import { Band } from "../models/acts/band.model";
import { SeatGroup } from "../models/locations/seatGroup.model";
import { Seat } from "../models/locations/seat.model";
import { SeatRow } from "../models/locations/seatRow.model";
import { Op } from "sequelize";
export const location = Router()
@@ -69,7 +70,7 @@ location.get("/", (req: Request, res: Response) => {
})
})
location.get("/:urlName", (req: Request, res: Response) => {
location.get("/location/:urlName", (req: Request, res: Response) => {
Location.findOne({
where: { urlName: req.params.urlName },
include: [
@@ -117,4 +118,19 @@ location.get("/:urlName", (req: Request, res: Response) => {
res.status(200).json(location)
})
})
// Location search
location.get("/search", (req: Request, res: Response) => {
Location.findAll({
where: {
name: {
[Op.substring]: req.query.value
}
}
})
.then(locations => {
res.status(200).json(locations)
})
})

View File

@@ -6,38 +6,21 @@ import navigationAppendItems from './components/navigation/navigationAppendItems
import navigationPrependItems from './components/navigation/navigationPrependItems.vue';
import { usePreferencesStore } from './data/stores/preferencesStore';
import { useFeedbackStore } from './data/stores/feedbackStore';
import { useConcertStore } from './data/stores/concertStore';
import { LocationModel } from './data/models/locations/locationModel';
import { useShoppingStore } from './data/stores/shoppingStore';
import footerItems from './components/navigation/footerItems.vue';
const preferencesStore = usePreferencesStore()
const concertStore = useConcertStore()
const feedbackStore = useFeedbackStore()
const shoppingStore = useShoppingStore()
const theme = useTheme()
theme.global.name.value = preferencesStore.theme
concertStore.fetchAllTours()
// Global watcher
watch(() => preferencesStore.language, () => {
i18n.global.locale = preferencesStore.language
}, { immediate: true })
watch(() => concertStore.cityFilter, () => {
concertStore.locationFilter = new LocationModel()
concertStore.filterTours()
})
watch(() => concertStore.locationFilter, () => {
concertStore.filterTours()
})
watch(() => concertStore.genreFilter, () => {
concertStore.filterTours()
})
</script>
<template>

View File

@@ -5,22 +5,10 @@ import router from '@/plugins/router';
const accountStore = useAccountStore()
const basketStore = useBasketStore()
function startSearch() {
// todo
router.push("/shows/search")
}
</script>
<template>
<v-text-field
variant="outlined"
append-inner-icon="mdi-magnify"
density="compact"
class="mt-5 mr-5"
@click:append-inner="startSearch"
width="400"
/>
<v-btn variant="plain" icon="mdi-magnify" to="/search" />
<v-btn v-if="accountStore.userAccount.id == 0" variant="plain" icon="mdi-account" to="/account/login" />
<v-btn v-else variant="plain" icon="mdi-account" to="/account/home" />

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import cardViewTopImage from '../basics/cardViewTopImage.vue';
import { LocationModel } from '@/data/models/locations/locationModel';
import { useRouter } from 'vue-router';
const router = useRouter()
defineProps({
location: LocationModel
})
</script>
<template>
<card-view-top-image
:image="location.imageOutdoor"
:title="location.name"
@click="router.push('locations/' + location.name.replaceAll(' ', '-').toLowerCase())"
>
<div>
{{ location.concerts.length }} {{ $t('concert', location.concerts.length) }}
</div>
</card-view-top-image>
</template>

View File

@@ -7,5 +7,9 @@ export async function getAllBands() {
}
export async function getBand(bandName: string) {
return await axios.get(BASE_URL + '/' + bandName)
return await axios.get(BASE_URL + '/band/' + bandName)
}
export async function searchBand(searchTerm: string) {
return await axios.get(BASE_URL + '/search?value=' + searchTerm)
}

View File

@@ -2,13 +2,9 @@ import axios from "axios"
let BASE_URL = "http://localhost:3000/concerts"
export async function getAllConcerts() {
return await axios.get(BASE_URL)
}
export async function getConcert(id: number) {
if (id != undefined) {
return await axios.get(BASE_URL + "/" + id)
return await axios.get(BASE_URL + "/concert/" + id)
} else {
return null
}

View File

@@ -14,4 +14,8 @@ export async function getTopEvents(nrOfEvents) {
let url = BASE_URL + "?sort=desc&count=" + nrOfEvents
return await axios.get(url)
}
export async function searchEvent(searchTerm: string) {
return await axios.get(BASE_URL + "/search?value=" + searchTerm)
}

View File

@@ -7,11 +7,15 @@ export async function getAllLocations() {
}
export async function getLocation(locationName: string) {
return await axios.get(BASE_URL + "/" + locationName)
return await axios.get(BASE_URL + "/location/" + locationName)
}
export async function getTopLocations(nrOfLocations: number) {
let url = BASE_URL + "?sort=desc&count=" + nrOfLocations
return await axios.get(url)
}
export async function searchLocation(searchTerm: string) {
return await axios.get(BASE_URL + "/search?value=" + searchTerm)
}

View File

@@ -1,10 +0,0 @@
import axios from "axios"
let BASE_URL = "http://localhost:3000/tours"
/**
* Fetch all tours from API
*/
export async function getAllTours() {
//return await axios.get(BASE_URL)
}

View File

@@ -1,122 +0,0 @@
import { useLocalStorage } from "@vueuse/core";
import { defineStore } from "pinia";
import { TourModel } from "../models/acts/tourModel";
import { getAllTours } from "../api/tourApi";
import { GenreModel } from "../models/acts/genreModel";
import { getAllBands } from "../api/bandApi";
import { BandModel } from "../models/acts/bandModel";
import { LocationModel } from "../models/locations/locationModel";
import { getAllLocations } from "../api/locationApi";
import { getAllGenres } from "../api/genreApi";
import { CityModel } from "../models/locations/cityModel";
import { getAllCities } from "../api/cityApi";
export const useConcertStore = defineStore("concertStore", {
state: () => ({
tours: useLocalStorage<Array<TourModel>>("hackmycart/concertStore/tours", []),
filteredTours: useLocalStorage<Array<TourModel>>("hackmycart/concertStore/filteredTours", []),
bands: useLocalStorage<Array<BandModel>>("hackmycart/concertStore/bands", []),
locations: useLocalStorage<Array<LocationModel>>("hackmycart/concertStore/locations", []),
filteredLocations: useLocalStorage<Array<LocationModel>>("hackmycart/concertStore/filteredLocations", []),
cities: useLocalStorage<Array<CityModel>>("hackmycart/concertStore/cities", []),
genres: useLocalStorage<Array<GenreModel>>("hackmycart/concertStore/genres", []),
cityFilter: useLocalStorage<CityModel>("hackmycart/concertStore/cityFilter", new CityModel()),
locationFilter: useLocalStorage<LocationModel>("hackmycart/concertStore/locationFilter", new LocationModel),
genreFilter: useLocalStorage<GenreModel>("hackmycart/concertStore/genreFilter", new GenreModel())
}),
actions: {
async fetchAllTours() {
await getAllTours()
.then(result => {
// this.tours = result.data
// this.tours.sort((a, b) => {
// return new Date(a.concerts[0].date) < new Date(b.concerts[0].date) ? -1 : 1
// })
// this.filteredTours = this.tours
// this.filterTours()
})
await getAllBands()
.then(result => {
this.bands = result.data
})
await getAllLocations()
.then(result => {
this.locations = result.data
})
await getAllGenres()
.then(result => {
this.genres = result.data
this.genres.sort((a, b) => {
return a.name > b.name
})
})
await getAllCities()
.then(result => {
this.cities = result.data
})
},
filterTours() {
this.filteredTours = []
// Filter tours by city, location and genre
for (let tour of this.tours) {
let rightGenre = false
let rightCity = false
let rightLocation = false
// Genre filter
if (this.genreFilter == null || this.genreFilter.id == undefined) {
rightGenre = true
} else {
for (let genre of tour.band.genres) {
if (genre.name == this.genreFilter.name) {
rightGenre = true
}
}
}
// City filter
if (this.cityFilter == null || this.cityFilter.id == undefined) {
rightCity = true
} else {
for (let concert of tour.concerts) {
if (concert.location.city.name == this.cityFilter.name) {
rightCity = true
}
}
// Filter locations by city
this.filteredLocations = this.cities.find(city =>
city.id == this.cityFilter.id
).locations
}
// Location filter
if (this.locationFilter == null || this.locationFilter.id == undefined) {
rightLocation = true
} else {
for (let concert of tour.concerts) {
if (concert.location.id == this.locationFilter.id) {
rightLocation = true
}
}
}
if (rightGenre && rightCity && rightLocation) {
this.filteredTours.push(tour)
}
}
}
}
})

View File

@@ -1,11 +0,0 @@
import { useLocalStorage } from "@vueuse/core";
import { defineStore } from "pinia";
const useScoreStore = defineStore("scoreStore", {
state: () => ({
progressGroup0: useLocalStorage("hackmycart/scoreStore/progressGroup0", 0),
progressGroup1: useLocalStorage("hackmycart/scoreStore/progressGroup1", 0),
progressGroup2: useLocalStorage("hackmycart/scoreStore/progressGroup2", 0),
progressGroup3: useLocalStorage("hackmycart/scoreStore/progressGroup3", 0),
})
})

View File

@@ -0,0 +1,43 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { searchBand } from "../api/bandApi";
import { searchLocation } from "../api/locationApi";
import { searchEvent } from "../api/eventApi";
export const useSearchStore = defineStore("searchStore", {
state: () => ({
searchTerm: ref(""),
bands: ref(),
locations: ref(),
events: ref(),
alreadySearched: ref(false),
searchInProgress: ref(false)
}),
actions: {
/**
* Search for the termin in all bands, locations, events
*/
async startSearch() {
this.alreadySearched = true
this.searchInProgress = true
await searchBand(this.searchTerm)
.then(result => {
this.bands = result.data
})
await searchLocation(this.searchTerm)
.then(result => {
this.locations = result.data
})
await searchEvent(this.searchTerm)
.then(result => {
this.events = result.data
})
this.searchInProgress = false
}
}
})

View File

@@ -170,5 +170,6 @@
"description": "Soll der Bearbeitungsfortschritt der Übungen wirklich zurückgesetzt werden? Dies kann nicht rückgänig gemacht werden!"
},
"goToTheConcert": "Zum Konzert",
"selectedConcert": "Ausgewähltes Konzert"
"selectedConcert": "Ausgewähltes Konzert",
"enterSomeKeywords": "Füge Schlagworte ein um nach Bands, Events, Konzerten und Veranstaltungsorten zu suchen"
}

View File

@@ -170,5 +170,6 @@
"description": "Do you really want to reset the exercise progress? This can't be undone!"
},
"goToTheConcert": "To the concert",
"selectedConcert": "Selected Concert"
"selectedConcert": "Selected Concert",
"enterSomeKeywords": "Enter keywords to search for bands, events, concerts and locations"
}

View File

@@ -1,6 +0,0 @@
<script setup lang="ts">
</script>
<template>
Search
</template>

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
import OutlinedButton from '@/components/basics/outlinedButton.vue';
import { useConcertStore } from '@/data/stores/concertStore';
import { useShoppingStore } from '@/data/stores/shoppingStore';
import { useRouter } from 'vue-router';
const concertStore = useConcertStore()
const shoppingStore = useShoppingStore()
const router = useRouter()
shoppingStore.getEvents()
</script>
<template>
@@ -31,27 +33,27 @@ const router = useRouter()
</template>
<v-carousel-item
v-for="band in concertStore.bands"
:src="'http://localhost:3000/static/' + band.imageMembers"
v-for="event in shoppingStore.events"
:src="'http://localhost:3000/static/' + event.band.imageMembers"
cover
>
<v-card
class="position-absolute bottom-0"
:title="band.name"
:title="event.name"
width="100%"
:rounded="false"
background-opacity="50%"
>
<v-card-text>
<div>
{{ band.descriptionDe }}
{{ event.band.descriptionDe }}
</div>
<outlined-button
append-icon="mdi-arrow-right"
class="mt-2"
color="primary"
@click="router.push('bands/' + band.name.replaceAll(' ', '-').toLowerCase())"
@click="router.push('bands/' + event.name.replaceAll(' ', '-').toLowerCase())"
>
{{ $t('tickets', 2) }}
</outlined-button>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { useConcertStore } from '@/data/stores/concertStore';
import highlightCarousel from './highlightCarousel.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import cardWithTopImage from '@/components/basics/cardViewTopImage.vue';

View File

@@ -4,6 +4,7 @@ import cardWithTopImage from '@/components/basics/cardViewTopImage.vue';
import { useRouter } from 'vue-router';
import { useShoppingStore } from '@/data/stores/shoppingStore';
import { useFeedbackStore } from '@/data/stores/feedbackStore';
import locationListItem from '@/components/pageParts/locationListItem.vue';
const shoppingStore = useShoppingStore()
const feedbackStore = useFeedbackStore()
@@ -51,15 +52,7 @@ shoppingStore.getCities()
<v-row>
<v-col v-for="location in city.locations" cols="3">
<card-with-top-image
:image="location.imageOutdoor"
:title="location.name"
@click="router.push('locations/' + location.name.replaceAll(' ', '-').toLowerCase())"
>
<div>
{{ location.nrOfConcerts }} {{ $t('concert', location.nrOfConcerts) }}
</div>
</card-with-top-image>
<location-list-item :location="location" />
</v-col>
</v-row>
</div>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import searchBar from './searchBar.vue';
import eventListItem from '@/components/pageParts/eventListItem.vue';
import sectionDivider from '@/components/basics/sectionDivider.vue';
import { useSearchStore } from '@/data/stores/searchStore';
const searchStore = useSearchStore()
</script>
<template>
<v-container>
<v-row>
<v-spacer />
<v-col cols="10">
<v-row>
<v-col>
<search-bar />
</v-col>
</v-row>
<v-row>
<v-col>
{{ searchStore.bands }}
</v-col>
</v-row>
<v-row>
<v-col>
{{ searchStore.locations }}
</v-col>
</v-row>
<v-row>
<v-col>
<section-divider
v-if="searchStore.alreadySearched"
:title="$t('event', 2)"
/>
</v-col>
</v-row>
<v-row
v-if="searchStore.alreadySearched && !searchStore.searchInProgress"
v-for="event in searchStore.events"
>
<v-col>
<event-list-item :event="event" :loading="searchStore.searchInProgress" />
</v-col>
</v-row>
<v-row
v-else-if="searchStore.alreadySearched && searchStore.searchInProgress"
v-for="i in 3"
>
<v-col>
<event-list-item :loading="searchStore.searchInProgress" />
</v-col>
</v-row>
<v-row>
<v-col>
<v-empty-state
:title="$t('noEventsFound')"
icon="mdi-magnify"
/>
</v-col>
</v-row>
</v-col>
<v-spacer />
</v-row>
</v-container>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import cardView from '@/components/basics/cardView.vue';
import { useSearchStore } from '@/data/stores/searchStore';
const searchStore = useSearchStore()
</script>
<template>
<card-view >
<v-text-field
variant="outlined"
hide-details
v-model="searchStore.searchTerm"
:placeholder="$t('enterSomeKeywords')"
>
<template #append-inner>
<v-btn
icon="mdi-magnify"
variant="plain"
@click="searchStore.startSearch"
/>
</template>
</v-text-field>
</card-view>
</template>

View File

@@ -5,7 +5,7 @@ import { accountRoutes } from "./account.routes";
import { systemRoutes } from "./system.routes";
import EventsPage from "@/pages/events/eventsPage/index.vue";
import LocationsPage from "@/pages/locations/locationsPage/index.vue"
import SearchPage from "@/pages/events/searchPage/index.vue"
import SearchPage from "@/pages/searchPage/index.vue"
import BandDetailPage from "@/pages/events/bandDetailPage/index.vue"
import LocationDetailPage from "@/pages/locations/locationDetailPage/index.vue"
import TicketOrderPage from "@/pages/events/ticketOrderPage/index.vue"