initial version
This commit is contained in:
134
components/Filter.vue
Normal file
134
components/Filter.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="filter-container">
|
||||
<div class="categories">
|
||||
<button v-for="category in categories" :key="category.value"
|
||||
:class="{ 'selected': selectedCategories.includes(category.value) }" @click="toggleCategory(category.value)"
|
||||
:title="category.label">
|
||||
<img :src="`/icons/${category.value}.svg`" :alt="category.label">
|
||||
</button>
|
||||
</div>
|
||||
<div class="search-container">
|
||||
<img src="/icons/search.svg" alt="Search Icon" class="search-icon">
|
||||
<input type="text" v-model="searchText" @input="updateFilter" placeholder="Search by title or address"
|
||||
class="search-input">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
locations: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
categories: [
|
||||
{ value: 'attraction', label: 'Attraction' },
|
||||
{ value: 'food', label: 'Food' },
|
||||
{ value: 'cultural', label: 'Cultural' },
|
||||
{ value: 'nature', label: 'Nature' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
],
|
||||
selectedCategories: ['attraction', 'food', 'cultural', 'nature', 'other'],
|
||||
searchText: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredLocations() {
|
||||
const lowerCaseSearchText = this.searchText.toLowerCase();
|
||||
const filtered = this.locations.filter(location =>
|
||||
this.selectedCategories.includes(location.category) &&
|
||||
(location.title.toLowerCase().includes(lowerCaseSearchText) ||
|
||||
location.address.toLowerCase().includes(lowerCaseSearchText))
|
||||
);
|
||||
return filtered.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleCategory(category) {
|
||||
const index = this.selectedCategories.indexOf(category);
|
||||
if (index > -1) {
|
||||
this.selectedCategories.splice(index, 1);
|
||||
} else {
|
||||
this.selectedCategories.push(category);
|
||||
}
|
||||
this.updateFilter();
|
||||
},
|
||||
updateFilter() {
|
||||
this.$emit('update-filter', this.filteredLocations);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 2px solid #454545;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.categories {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.categories button {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
margin: 0 5px;
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.categories button img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
padding: 3px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.categories button.selected {
|
||||
background-color: #2dff1ac6;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
width: 90%;
|
||||
margin: 0 auto 3px auto;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 10px;
|
||||
padding-left: 35px;
|
||||
/* Adjusted padding to account for the icon */
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
18
components/Footer.vue
Normal file
18
components/Footer.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<footer>
|
||||
<a href="/about">Learn more about <em><Augsburg Mosaik/></em></a>
|
||||
</footer>
|
||||
</template>
|
||||
<style scoped>
|
||||
footer {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
74
components/Header.vue
Normal file
74
components/Header.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<h1 class="title">Augsburg Mosaic</h1>
|
||||
<p class="description">
|
||||
Discover Augsburg through my curated list of notable locations, events, and eateries. Whether you're a visitor
|
||||
exploring the city for the first time or a local seeking new experiences, this guide aims to enrich your journey
|
||||
through Augsburg.
|
||||
</p>
|
||||
<p class="hint" v-if="isMobile">
|
||||
For an enhanced experience, view this website on a larger screen.
|
||||
</p>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useMainStore } from '~/store/pinia';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const store = useMainStore();
|
||||
const isMobile = ref(store.isMobile);
|
||||
|
||||
const updateIsMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
store.setIsMobile(isMobile.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (process.client) {
|
||||
updateIsMobile();
|
||||
window.addEventListener('resize', updateIsMobile);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
updateIsMobile
|
||||
};
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (process.client) {
|
||||
window.removeEventListener('resize', this.updateIsMobile);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #f0f0f0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #555;
|
||||
line-height: 1.3;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
179
components/LocationList.vue
Normal file
179
components/LocationList.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="location-list">
|
||||
<div v-for="location in locations" :key="location.id" class="location-card"
|
||||
:class="{ highlighted: selectedLocation && selectedLocation.id === location.id }" :data-id="location.id"
|
||||
ref="locationCards" @click="handleLocationClick(location)">
|
||||
<div class="card-header">
|
||||
<img v-if="location.image" :src="location.image" alt="location image" class="location-image" />
|
||||
<div class="location-info">
|
||||
<h3 class="location-title">{{ location.title }}</h3>
|
||||
<p class="location-category">{{ location.category }}</p>
|
||||
<!--<div class="location-rating">
|
||||
<span v-for="n in 5" :key="n" class="star" :class="{ filled: n <= location.rating }">★</span>
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
<p class="location-description">{{ location.description }}</p>
|
||||
<p class="location-address">{{ location.address }}</p>
|
||||
<!--<div class="location-notes">
|
||||
<p><strong>Notes:</strong> {{ location.notes }}</p>
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { nextTick, defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
locations: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectedLocation: {
|
||||
require: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
locationCards: []
|
||||
};
|
||||
},
|
||||
updated() {
|
||||
if (this.selectedLocation) {
|
||||
const highlightedCard = this.locationCards.find(card => card.dataset.id === this.selectedLocation.id.toString());
|
||||
if (highlightedCard) {
|
||||
highlightedCard.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedLocation: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (newValue) {
|
||||
this.scrollToLocation(newValue.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['location-click'],
|
||||
methods: {
|
||||
handleLocationClick(location) {
|
||||
this.$emit('location-click', location);
|
||||
},
|
||||
scrollToLocation(id) {
|
||||
nextTick(() => {
|
||||
const highlightedCard = Array.from(this.$refs.locationCards).find(card => card.dataset.id === id.toString());
|
||||
if (highlightedCard) {
|
||||
highlightedCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.location-list {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
/* Makes the location list scrollable */
|
||||
padding: 20px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.location-card {
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #3e3c3c81;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.location-card:hover {
|
||||
transform: scaleY(1.05) scaleX(1.01);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.location-image {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.location-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.location-title {
|
||||
font-size: 1.5em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.location-category {
|
||||
font-size: 1em;
|
||||
margin: 5px 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.location-rating .star {
|
||||
font-size: 1.2em;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.location-rating .star.filled {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.location-description,
|
||||
.location-address {
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.location-notes {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Media query for larger screens */
|
||||
@media (min-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.location-image {
|
||||
margin-bottom: 0;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.location-info,
|
||||
.location-description,
|
||||
.location-address,
|
||||
.location-notes {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background-color: rgba(157, 221, 221, 0.551);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
</style>
|
||||
31
components/Map.vue
Normal file
31
components/Map.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="map-component">
|
||||
<LMap ref="map" :zoom="zoom" :center="center" :max-bounds="maxBounds">
|
||||
<LTileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
attribution="&copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors"
|
||||
layer-type="base" name="OpenStreetMap" />
|
||||
<LMarker v-for="location in locations" :key="location.id" :lat-lng="location.coordinates"
|
||||
@click="handleMarkerClick(location)" />
|
||||
</LMap>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
const props = defineProps(['center', 'zoom', 'maxBounds', 'locations', 'selectedLocation']);
|
||||
|
||||
const emit = defineEmits(['marker-click']);
|
||||
|
||||
const handleMarkerClick = (location) => {
|
||||
emit('marker-click', location);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-component {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid #434343
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user