initial version

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

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

1
.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

55
README.md Normal file
View File

@ -0,0 +1,55 @@
# Augsburg Mosaic
This project blossomed from a genuine fondness for the city of Augsburg, aiming to unfold its hidden gems and notable locales to both visitors and residents alike.
![Project Screenshot](screenshot.png)
## Project Overview
Augsburg Mosaic serves as a digital guide, offering a curated list of places categorized as Attractions, Food spots, Cultural hubs, Nature retreats, and Others. Through an interactive map and a user-friendly list, one can explore, filter, and learn more about each location.
Utilizing technologies such as Vue.js, Nuxt.js, Leaflet.js for map rendering, and Pinia for state management, this project embodies a seamless blend of design and functionality, all while being a purely static site optimized for SEO. This also means, there are no cookies or any other data collection mechanisms involved, especially not concerning personal data.
This endeavor is purely a hobby, unfunded yet rich in spirit. The icons embellishing this site are courtesy of FontAwesome, while the images are captured through the lens of [WieErWill](https://wieerwill.de).
## Getting Started
### Prerequisites
- Ensure you have [Node.js](https://nodejs.org/) installed on your machine.
- This project uses [pnpm](https://pnpm.io/) as the package manager. Install it globally using `npm install -g pnpm`.
### Setup
1. Fork this repository to your GitHub account.
2. Clone your forked repository to your local machine: `git clone https://github.com/wieerwill/augsburg-mosaic.git`
3. Navigate to the project directory: `cd augsburg-mosaic`
4. Install the project dependencies: `pnpm install`
5. Start the development server: `pnpm dev`
The project should now be running on [http://localhost:3000](http://localhost:3000).
### Building
To build the static version of the project, run:
```bash
pnpm generate
```
The static files will be generated in the dist directory, ready to be deployed.
Contributing
Feel free to explore, report issues, or open pull requests. If you find this project useful, please consider giving it a star! ⭐
### License
This project is open-source and available under the MIT License.
### Acknowledgements
Icons provided by FontAwesome.
Map rendering powered by Leaflet.js.
Enjoy traversing through the mosaic of Augsburg!

13
app.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<NuxtPage />
</template>
<style>
html {
height: 100vh;
}
body {
margin: 0;
padding: 0;
}
</style>

195
assets/locations.json Normal file
View File

@ -0,0 +1,195 @@
[
{
"id": 1,
"title": "Zoo",
"description": "Home to over 1,600 animals of 300 species, Augsburg Zoo is a fun destination for families.",
"category": "attraction",
"coordinates": [
48.34879,
10.91588
],
"address": "Brehmplatz 1, 86161 Augsburg",
"rating": 4,
"image": null,
"notes": "Great for a day out with kids."
},
{
"id": 2,
"title": "Town Hall",
"description": "A Renaissance architectural masterpiece with a beautiful interior including the Golden Hall.",
"category": "cultural",
"coordinates": [
48.3688702145732,
10.89869720434145
],
"address": "Rathauspl. 1, 86150 Augsburg",
"rating": 5,
"image": null,
"notes": "The Golden Hall is a must-see."
},
{
"id": 3,
"title": "Fuggerei",
"description": "The world's oldest social housing complex still in use.",
"category": "cultural",
"coordinates": [
48.36987006188504,
10.903964160650018
],
"address": "Jakoberstraße 26, 86152 Augsburg",
"rating": 4,
"image": null,
"notes": "A quiet, peaceful place with a rich history."
},
{
"id": 4,
"title": "Perlachturm",
"description": "A historic tower offering panoramic views of Augsburg. (currently closed)",
"category": "attraction",
"coordinates": [
48.36920628764096,
10.898450278952886
],
"address": "Rathauspl. 1, 86150 Augsburg",
"rating": 4,
"image": null,
"notes": "Wear comfortable shoes for the climb."
},
{
"id": 5,
"title": "Puppenkiste",
"description": "A famous marionette theater with a museum showcasing historical puppets.",
"category": "cultural",
"coordinates": [
48.3603972116233,
10.903206140281654
],
"address": "Spitalgasse 15, 86150 Augsburg",
"rating": 5,
"image": null,
"notes": "A magical experience for all ages."
},
{
"id": 6,
"title": "Botanical Garden",
"description": "A serene botanical garden with themed areas and a butterfly house.",
"category": "nature",
"coordinates": [
48.34957863531708,
10.915033207995533
],
"address": "Dr.-Ziegenspeck-Weg 10, 86161 Augsburg",
"rating": 4,
"image": null,
"notes": "The butterfly house is a highlight."
},
{
"id": 7,
"title": "Maximilianmuseum",
"description": "A museum housing a variety of historical artifacts and artworks.",
"category": "cultural",
"coordinates": [
48.36794304122964,
10.896419926758991
],
"address": "Fuggerplatz 1, 86150 Augsburg",
"rating": 3,
"image": null,
"notes": "Rich in local history."
},
{
"id": 8,
"title": "Eiskanal",
"description": "An artificial white-water river offering canoeing and kayaking opportunities.",
"category": "other",
"coordinates": [
48.34801468191772,
10.936438664823985
],
"address": "Eiskanalstraße 30, 86161 Augsburg",
"rating": 4,
"image": null,
"notes": "A thrilling experience for adventure seekers."
},
{
"id": 9,
"title": "Schaezlerpalais",
"description": "A baroque palace with stunning interiors and an art gallery.",
"category": "cultural",
"coordinates": [
48.36515672496401,
10.899364991520201
],
"address": "Maximilianstraße 46, 86150 Augsburg",
"rating": 4,
"image": null,
"notes": "The Rococo ballroom is a masterpiece."
},
{
"id": 10,
"title": "Masericó",
"category": "food",
"coordinates": [48.37465547077278, 10.895556446113813],
"address": "Frauentorstraße 25, 86152 Augsburg",
"rating": "",
"description": "Finest Pizza selection, always freshly baken and great combinations possible",
"image": "",
"notes": ""
},
{
"id": 11,
"title": "Trattoria Pizzeria Crudo",
"category": "food",
"coordinates": [48.379074019160875, 10.891983555594491],
"address": "Am Pfannenstiel 20, 86153 Augsburg",
"rating": "",
"description": "Italian, Pizza, Seafood, Mediterranean",
"image": "",
"notes": ""
},
{
"id": 13,
"title": "Shushu Falafel",
"category": "food",
"coordinates": [48.37083274095064, 10.897619465655083],
"address": "Karlstraße 2, 86150 Augsburg",
"rating": "",
"description": "Syrian, Lebanese, Middle Eastern",
"image": "",
"notes": ""
},
{
"id": 14,
"title": "Riegele WirtsHaus",
"category": "food",
"coordinates": [48.36772460100637, 10.88493485006803],
"address": "Frölichstraße 26, 86150 Augsburg",
"rating": "",
"description": "traditional and modern bavrian, home bew beer and always up to new ideas",
"image": "",
"notes": ""
},
{
"id": 15,
"title": "Got Asia",
"category": "food",
"coordinates": [48.37044353815951, 10.89805742563597],
"address": "Leonhardsberg 2, 86150 Augsburg",
"rating": "",
"description": "Asian Cuisine, Sushi and good udon noodles",
"image": "",
"notes": ""
},
{
"id": 16,
"title": "Taverna Ikaros",
"category": "food",
"coordinates": [48.36173471336735, 10.871633911417918],
"address": "Sigmundstraße 3, 86157 Augsburg",
"rating": "",
"description": "best greek i found so far in Augsburg, friendly Service and i can suggest the Zeus plate ;)",
"image": "",
"notes": ""
}
]

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>

32
mixins/mobileMixin.js Normal file
View File

@ -0,0 +1,32 @@
import { useMainStore } from '~/store/pinia';
const mobileMixin = {
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);
}
}
};
export default mobileMixin

14
nuxt.config.ts Normal file
View File

@ -0,0 +1,14 @@
export default defineNuxtConfig({
ssr: true,
target: 'static',
head: {
title: 'Augsburg Mosaic',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'Interactive map of Augsburg with recommended locations to visit.' }
]
},
modules: ['nuxt3-leaflet', '@pinia/nuxt'],
devtools: { enabled: true }
})

3
old_server/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@nuxt/devtools": "latest",
"nuxt": "^3.8.0",
"vue": "^3.3.7",
"vue-router": "^4.2.5"
},
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"@vue-leaflet/vue-leaflet": "^0.10.1",
"leaflet": "^1.9.4",
"nuxt3-leaflet": "^1.0.12",
"pinia": "^2.1.7"
}
}

69
pages/about.vue Normal file
View File

@ -0,0 +1,69 @@
<template>
<div class="about-container">
<h1 class="about-title">Augsburg Mosaic</h1>
<p class="about-description">
Welcome to Augsburg Mosaic, a delightful endeavor crafted by the creative mind <a href="https://wieerwill.de" class="external-link">WieErWill</a>.
This project blossomed from a genuine fondness for the city of Augsburg, aiming to unfold its hidden gems and notable locales to both visitors and residents alike.
</p>
<p class="about-detail">
Augsburg Mosaic serves as a digital guide, offering a curated list of places categorized as Attractions, Food spots, Cultural hubs, Nature retreats, and Others.
Through an interactive map and a user-friendly list, one can explore, filter, and learn more about each location.
Utilizing technologies such as Vue.js, Nuxt.js, Leaflet.js for map rendering, and Pinia for state management, this project embodies a seamless blend of design and functionality,
all while being a purely static site optimized for SEO. This also means, i don't use cookies or any other data, especially not yours in any way.
</p>
<p class="about-note">
This endeavor is purely a hobby, unfunded yet rich in spirit. Feel free to traverse through the mosaic of Augsburg,
all I hope is for it to bring joy and not to cause harm. The icons embellishing this site are courtesy of FontAwesome,
while the images are captured through my lens.
</p>
<a href="/" class="back-link"><strong>Back to Map</strong></a>
</div>
</template>
<style scoped>
.about-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
margin: auto;
max-width: 800px;
background-color: #f7f4f4;
border: 1px solid #e0e0e0;
border-radius: 10px;
box-shadow: 0 16px 16px rgba(0, 0, 0, 0.2);
margin-top: 3rem;
}
.about-title {
font-size: 36px;
margin-bottom: 20px;
}
.about-description,
.about-detail,
.about-note {
text-align: left;
margin-bottom: 20px;
line-height: 1.6;
}
.external-link,
.back-link {
color: #3498db;
text-decoration: none;
transition: color 0.3s;
}
.external-link:hover,
.back-link:hover {
color: #2c3e50;
}
.back-link {
margin-top: 20px;
padding: 10px 20px;
border: 1px solid #3498db;
border-radius: 5px;
text-align: center;
}
</style>

160
pages/index.vue Normal file
View File

@ -0,0 +1,160 @@
<template>
<Header />
<div class="container">
<div class="sidebar" :class="{ 'is-mobile': isMobile }">
<Filter :locations="locations" @update-filter="updateFilteredLocations" />
<div class="location-list-container">
<LocationList :locations="filteredLocations" :selectedLocation="selectedLocation"
@location-click="handleLocationClick" />
</div>
</div>
<div class="map-container" v-if="!isMobile">
<Map :center="mapCenter" :zoom="mapZoom" :maxBounds="maxBounds" :locations="filteredLocations"
@marker-click="handleMarkerClick" :selectedLocation="selectedLocation" ref="map" />
</div>
</div>
<Footer />
</template>
<script>
import locationsData from '~/assets/locations.json';
import Header from '~/components/Header.vue';
import Footer from '~/components/Footer.vue';
import Filter from '~/components/Filter.vue';
import LocationList from '~/components/LocationList.vue';
import Map from '~/components/Map.vue';
export default {
data() {
return {
isMobile: false,
maxBounds: [
[48.2, 10.6], // Southwest coordinates
[48.6, 11.2] // Northeast coordinates
],
mapCenter: [48.3689, 10.8979],
mapZoom: 12,
selectedLocation: null,
filteredLocations: [],
locations: locationsData
}
},
components: {
Header,
Footer,
Filter,
LocationList,
Map
},
created() {
this.filteredLocations = [...this.locations].sort((a, b) => a.title.localeCompare(b.title))
},
mounted() {
this.$nextTick(this.updateHeaderFooterHeight);
window.addEventListener('resize', this.updateHeaderFooterHeight);
this.$nextTick(this.updateLocationListHeight);
window.addEventListener('resize', this.updateLocationListHeight);
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.updateHeaderFooterHeight);
window.removeEventListener('resize', this.updateLocationListHeight);
window.removeEventListener('resize', this.handleResize);
},
methods: {
updateFilteredLocations(filteredLocations) {
this.filteredLocations = filteredLocations;
},
handleLocationClick(location) {
this.selectedLocation = location;
this.mapCenter = location.coordinates;
this.mapZoom = 16;
},
handleMarkerClick(location) {
this.selectedLocation = location;
this.mapCenter = location.coordinates;
},
handleResize() {
this.isMobile = window.innerWidth <= 768;
},
updateHeaderFooterHeight() {
const headerHeight = document.querySelector('header').offsetHeight;
const footerHeight = document.querySelector('footer').offsetHeight;
const totalHeight = headerHeight + footerHeight;
document.documentElement.style.setProperty('--header-footer-height', `${totalHeight}px`);
},
updateLocationListHeight() {
const filterHeight = document.querySelector('.filter-container').offsetHeight;
const containerHeight = document.querySelector('.container').clientHeight;
const newHeight = containerHeight - filterHeight - 45
const locationListElement = document.querySelector('.location-list');
locationListElement.style.height = `${newHeight}px`;
}
}
}
</script>
<style scoped>
.header {
background-color: #e0f7fa;
padding: 20px;
margin: 0;
text-align: center;
}
.container {
display: flex;
flex-direction: row;
height: calc(100vh - var(--header-footer-height));
}
.sidebar {
flex: 1;
background-color: #f9f9f9;
display: flex;
flex-direction: column;
border: 1px solid #434343
}
.location-list-container {
overflow-y: auto;
flex-grow: 1;
}
.map-container {
flex: 1;
transition: flex 0.3s;
min-width: 10rem;
min-height: 10rem;
height: 100%;
}
.is-mobile {
flex: 3;
}
.is-mobile .map-container {
display: none;
}
.title {
color: #2c3e50;
}
.description {
color: #2c3e50;
}
/* Mosaic style background for body */
body {
background-color: #ffffff;
background-image:
linear-gradient(45deg, #f06, #f06 25%, transparent 25%, transparent 75%, #f06 75%, #f06),
linear-gradient(45deg, #f06, #f06 25%, transparent 25%, transparent 75%, #f06 75%, #f06);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
</style>

6015
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 160v96C0 379.7 100.3 480 224 480s224-100.3 224-224V160H320v96c0 53-43 96-96 96s-96-43-96-96V160H0zm0-32H128V64c0-17.7-14.3-32-32-32H32C14.3 32 0 46.3 0 64v64zm320 0H448V64c0-17.7-14.3-32-32-32H352c-17.7 0-32 14.3-32 32v64z"/></svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M240.1 4.2c9.8-5.6 21.9-5.6 31.8 0l171.8 98.1L448 104l0 .9 47.9 27.4c12.6 7.2 18.8 22 15.1 36s-16.4 23.8-30.9 23.8H32c-14.5 0-27.2-9.8-30.9-23.8s2.5-28.8 15.1-36L64 104.9V104l4.4-1.6L240.1 4.2zM64 224h64V416h40V224h64V416h48V224h64V416h40V224h64V420.3c.6 .3 1.2 .7 1.8 1.1l48 32c11.7 7.8 17 22.4 12.9 35.9S494.1 512 480 512H32c-14.1 0-26.5-9.2-30.6-22.7s1.1-28.1 12.9-35.9l48-32c.6-.4 1.2-.7 1.8-1.1V224z"/></svg>

After

Width:  |  Height:  |  Size: 666 B

1
public/icons/food.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M416 0C400 0 288 32 288 176V288c0 35.3 28.7 64 64 64h32V480c0 17.7 14.3 32 32 32s32-14.3 32-32V352 240 32c0-17.7-14.3-32-32-32zM64 16C64 7.8 57.9 1 49.7 .1S34.2 4.6 32.4 12.5L2.1 148.8C.7 155.1 0 161.5 0 167.9c0 45.9 35.1 83.6 80 87.7V480c0 17.7 14.3 32 32 32s32-14.3 32-32V255.6c44.9-4.1 80-41.8 80-87.7c0-6.4-.7-12.8-2.1-19.1L191.6 12.5c-1.8-8-9.3-13.3-17.4-12.4S160 7.8 160 16V150.2c0 5.4-4.4 9.8-9.8 9.8c-5.1 0-9.3-3.9-9.8-9L127.9 14.6C127.2 6.3 120.3 0 112 0s-15.2 6.3-15.9 14.6L83.7 151c-.5 5.1-4.7 9-9.8 9c-5.4 0-9.8-4.4-9.8-9.8V16zm48.3 152l-.3 0-.3 0 .3-.7 .3 .7z"/></svg>

After

Width:  |  Height:  |  Size: 834 B

1
public/icons/nature.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M210.6 5.9L62 169.4c-3.9 4.2-6 9.8-6 15.5C56 197.7 66.3 208 79.1 208H104L30.6 281.4c-4.2 4.2-6.6 10-6.6 16C24 309.9 34.1 320 46.6 320H80L5.4 409.5C1.9 413.7 0 419 0 424.5c0 13 10.5 23.5 23.5 23.5H192v32c0 17.7 14.3 32 32 32s32-14.3 32-32V448H424.5c13 0 23.5-10.5 23.5-23.5c0-5.5-1.9-10.8-5.4-15L368 320h33.4c12.5 0 22.6-10.1 22.6-22.6c0-6-2.4-11.8-6.6-16L344 208h24.9c12.7 0 23.1-10.3 23.1-23.1c0-5.7-2.1-11.3-6-15.5L237.4 5.9C234 2.1 229.1 0 224 0s-10 2.1-13.4 5.9z"/></svg>

After

Width:  |  Height:  |  Size: 728 B

1
public/icons/other.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 640 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M181.5 197.1l12.9 6.4c5.9 3 12.4 4.5 19.1 4.5c23.5 0 42.6-19.1 42.6-42.6V144c0-35.3-28.7-64-64-64H128c-35.3 0-64 28.7-64 64v21.4c0 23.5 19.1 42.6 42.6 42.6c6.6 0 13.1-1.5 19.1-4.5l12.9-6.4 8.4-4.2L135.1 185c-4.5-3-7.1-8-7.1-13.3V168c0-13.3 10.7-24 24-24h16c13.3 0 24 10.7 24 24v3.7c0 5.3-2.7 10.3-7.1 13.3l-11.8 7.9 8.4 4.2zm-8.6 49.4L160 240l-12.9 6.4c-12.6 6.3-26.5 9.6-40.5 9.6c-3.6 0-7.1-.2-10.6-.6v.6c0 35.3 28.7 64 64 64h64c17.7 0 32 14.3 32 32s-14.3 32-32 32H384V336 320c0-23.7 12.9-44.4 32-55.4c9.4-5.4 20.3-8.6 32-8.6V240c0-26.5 21.5-48 48-48c8.8 0 16 7.2 16 16v32 16 48c0 8.8 7.2 16 16 16s16-7.2 16-16V204.3c0-48.2-30.8-91-76.6-106.3l-8.5-2.8c-8-2.7-12.6-11.1-10.4-19.3s10.3-13.2 18.6-11.6l19.9 4C576 86.1 640 164.2 640 254.9l0 1.1h0c0 123.7-100.3 224-224 224h-1.1H256h-.6C132 480 32 380 32 256.6V256 216.8c-10.1-14.6-16-32.3-16-51.4V144l0-1.4C6.7 139.3 0 130.5 0 120c0-13.3 10.7-24 24-24h2.8C44.8 58.2 83.3 32 128 32h64c44.7 0 83.2 26.2 101.2 64H296c13.3 0 24 10.7 24 24c0 10.5-6.7 19.3-16 22.6l0 1.4v21.4c0 1.4 0 2.8-.1 4.3c12-6.2 25.7-9.6 40.1-9.6h8c17.7 0 32 14.3 32 32s-14.3 32-32 32h-8c-13.3 0-24 10.7-24 24v8h56.4c-15.2 17-24.4 39.4-24.4 64H320c-42.3 0-78.2-27.4-91-65.3c-5.1 .9-10.3 1.3-15.6 1.3c-14.1 0-27.9-3.3-40.5-9.6zM96 128a16 16 0 1 1 0 32 16 16 0 1 1 0-32zm112 16a16 16 0 1 1 32 0 16 16 0 1 1 -32 0z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
public/icons/search.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>

After

Width:  |  Height:  |  Size: 494 B

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

21
store/pinia.js Normal file
View File

@ -0,0 +1,21 @@
import { defineStore } from 'pinia';
export const useMainStore = defineStore({
id: 'main',
state: () => ({
isMobile: false,
selectedLocation: null,
mapCenter: [48.371, 10.898],
mapZoom: 13,
}),
actions: {
setIsMobile(value) {
this.isMobile = value;
},
selectLocation(location) {
this.selectedLocation = location;
this.mapCenter = location.coordinates;
this.mapZoom = 15;
},
}
});

3
tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}