initial version

This commit is contained in:
2023-11-01 20:27:34 +01:00
commit a5f25d1cd7
27 changed files with 7072 additions and 0 deletions

134
components/Filter.vue Normal file
View 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
View File

@@ -0,0 +1,18 @@
<template>
<footer>
<a href="/about">Learn more about <em>&lt;Augsburg Mosaik/&gt;</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
View 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
View 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 }">&#9733;</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
View 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="&amp;copy; <a href=&quot;https://www.openstreetmap.org/&quot;>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>