Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor code to split logic into multiple components #5

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


https://github.com/friuns2/travelmaps/assets/16543239/3fc07810-ec7a-4c82-93c5-e9d5e8550002


Expand Down Expand Up @@ -62,6 +60,18 @@ Created with [GPTCall App Creator](https://app.gptcall.net/)
5. Toggle between list and map views on mobile devices.
6. Use the "Show Itinerary" button to view your planned route.

## Component Structure and Usage

The application's logic has been refactored to split into multiple Vue components for better maintainability and scalability. Here's an overview of the new components and how they are used:

- `AttractionList.vue`: This component is responsible for rendering the list of attractions. It receives the list of attractions as a prop and handles user interactions such as focusing on an attraction or toggling it in the itinerary.

- `MapView.vue`: This component handles the display of the interactive map. It receives the home location and the list of attractions as props, and it is responsible for initializing the map, creating markers, and displaying directions.

- `Filters.vue`: This component provides the UI for filtering attractions based on the minimum rating count. It receives the minimum rating count as a prop and emits an event when the value changes.

These components are imported and used in the `pages/index.vue` file, which acts as the main entry point for the application's UI.

## Future Plans

- Implement AI support to generate travel recommendations and itineraries based on user preferences and travel style.
Expand Down
40 changes: 40 additions & 0 deletions components/AttractionList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6" id="attraction-grid">
<TransitionGroup name="attraction-list">
<div v-for="attraction in attractions" :key="attraction.name"
class="bg-white bg-opacity-10 rounded-lg overflow-hidden shadow-lg transform hover:scale-105 transition-transform duration-300 cursor-pointer"
@click="focusAttraction(attraction)">
<img :src="attraction.photoUrl" class="w-full h-48 object-cover" alt="Attraction photo">
<div class="p-4">
<h3 class="text-xl font-bold mb-2">{{ attraction.name }}</h3>
<p class="text-sm mb-1">
<i class="material-icons text-yellow-400 align-middle mr-1">star</i> {{ attraction.rating }} / 5
</p>
<p class="text-sm mb-1">
<i class="material-icons align-middle mr-1">people</i> {{ attraction.user_ratings_total }}
</p>
<p class="text-sm mb-1">
<i class="material-icons align-middle mr-1">location_on</i> {{ attraction.address }}
</p>
<p class="text-sm mb-1">
<i class="material-icons align-middle mr-1">directions_car</i> {{ attraction.distance.toFixed(2) }} km
</p>
<button @click.stop="toggleAttractionInItinerary(attraction)"
:class="{ 'bg-green-500': attraction.inItinerary, 'bg-blue-500': !attraction.inItinerary }"
class="mt-2 px-3 py-1 rounded-full text-white text-sm">
<i class="material-icons align-middle mr-1">{{ attraction.inItinerary ? 'remove_circle' : 'add_circle' }}</i> {{ attraction.inItinerary ? 'Remove from Itinerary' : 'Add to Itinerary' }} </button>
</div>
</div>
</TransitionGroup>
</div>
</template>

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
attractions: Array,
focusAttraction: Function,
toggleAttractionInItinerary: Function
});
</script>
18 changes: 18 additions & 0 deletions components/Filters.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<div class="mb-8 bg-white bg-opacity-20 rounded-lg p-4 backdrop-blur-md">
<label for="minRatingsCount" class="block text-lg font-medium mb-2">Minimum Ratings Count:</label>
<div class="flex items-center">
<input type="range" id="minRatingsCount" v-model="minRatingsCount" min="0" max="1000" step="10"
class="w-full mr-4 accent-teal-500">
<span class="text-xl font-semibold">{{ minRatingsCount }}</span>
</div>
</div>
</template>

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
minRatingsCount: Number
});
</script>
53 changes: 53 additions & 0 deletions components/MapView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<template>
<div id="map" class="h-[calc(100vh-200px)] rounded-lg shadow-lg"></div>
</template>

<script setup>
import { defineProps, onMounted } from 'vue';

const props = defineProps({
homeLocation: Object,
attractions: Array,
focusAttraction: Function,
calculateDirections: Function
});

let map, infowindow;

onMounted(() => {
initMap();
});

function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: props.homeLocation,
zoom: 12,
});

infowindow = new google.maps.InfoWindow();

props.attractions.forEach(attraction => {
createMarker(attraction);
});
}

function createMarker(attraction) {
const marker = new google.maps.Marker({
map: map,
position: attraction.location,
title: attraction.name,
animation: google.maps.Animation.DROP,
icon: {
path: google.maps.SymbolPath.CIRCLE,
scale: 8,
fillColor: attraction.inItinerary ? '#4CAF50' : '#FFA500',
fillOpacity: 0.9,
strokeWeight: 0
}
});

marker.addListener('click', function () {
props.focusAttraction(attraction);
});
}
</script>
71 changes: 6 additions & 65 deletions pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<template>

<div class="min-h-screen bg-gradient-to-br from-teal-400 to-indigo-600 text-white p-4 md:p-8 overflow-hidden">
<h1 @click="toggleModal" class="text-4xl md:text-5xl font-bold mb-6 text-center animate-fade-in-down cursor-pointer">Bali Attractions</h1>
<div :class="{ 'hidden': !showModal }" class="fixed inset-0 flex items-center justify-center z-50">
Expand All @@ -15,14 +14,7 @@
<button @click="setNewLocation" class="btn btn-primary w-full text-white"> Set Location </button>
</div>
</div>
<div class="mb-8 bg-white bg-opacity-20 rounded-lg p-4 backdrop-blur-md">
<label for="minRatingsCount" class="block text-lg font-medium mb-2">Minimum Ratings Count:</label>
<div class="flex items-center">
<input type="range" id="minRatingsCount" v-model="minRatingsCount" min="0" max="1000" step="10"
class="w-full mr-4 accent-teal-500">
<span class="text-xl font-semibold">{{ minRatingsCount }}</span>
</div>
</div>
<Filters :minRatingsCount="minRatingsCount" />
<div class="md:hidden">
<button @click="ActiveView('list')" :class="{ 'bg-teal-500': activeView === 'list' }"
class="px-4 py-2 rounded-l-lg">
Expand All @@ -35,72 +27,21 @@
</div>
<div class="md:flex">
<div :class="{ 'hidden md:block': activeView === 'map' }" class="md:w-1/2 md:pr-4">
<div class="mb-4 flex justify-between items-center">
<div>
<label for="sortOrder" class="mr-2 text-white">Sort by:</label>
<select id="sortOrder" v-model="sortOrder" class="bg-white bg-opacity-20 rounded-lg px-2 py-1 text-white">
<option value="relativity" class="text-black">Relativity</option>
<option value="popularity" class="text-black">Popularity</option>
<option value="distance" class="text-black">Distance</option>
</select>
</div>
<button v-if="hasItineraryItems" @click="toggleItinerary" class="btn btn-primary btn-sm text-white glass">
<i class="material-icons mr-2 text-xl">{{ showItinerary ? 'view_list' : 'directions' }}</i>
<span>{{ showItinerary ? 'Show All' : 'Show Itinerary' }}</span>
</button>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6" id="attraction-grid">
<TransitionGroup name="attraction-list">
<div v-for="attraction in sortedAttractions" :key="attraction.name"
class="bg-white bg-opacity-10 rounded-lg overflow-hidden shadow-lg transform hover:scale-105 transition-transform duration-300 cursor-pointer"
@click="focusAttraction(attraction)">
<img :src="attraction.photoUrl" class="w-full h-48 object-cover" alt="Attraction photo">
<div class="p-4">
<h3 class="text-xl font-bold mb-2">{{ attraction.name }}</h3>
<p class="text-sm mb-1">
<i class="material-icons text-yellow-400 align-middle mr-1">star</i> {{ attraction.rating }} / 5
</p>
<p class="text-sm mb-1">
<i class="material-icons align-middle mr-1">people</i> {{ attraction.user_ratings_total }}
</p>
<p class="text-sm mb-1">
<i class="material-icons align-middle mr-1">location_on</i> {{ attraction.address }}
</p>
<p class="text-sm mb-1">
<i class="material-icons align-middle mr-1">directions_car</i> {{ attraction.distance.toFixed(2) }} km
</p>
<button @click.stop="toggleAttractionInItinerary(attraction)"
:class="{ 'bg-green-500': attraction.inItinerary, 'bg-blue-500': !attraction.inItinerary }"
class="mt-2 px-3 py-1 rounded-full text-white text-sm">
<i class="material-icons align-middle mr-1">{{ attraction.inItinerary ? 'remove_circle' : 'add_circle' }}</i> {{ attraction.inItinerary ? 'Remove from Itinerary' : 'Add to Itinerary' }} </button>
</div>
</div>
</TransitionGroup>
</div>
<AttractionList :attractions="sortedAttractions" :focusAttraction="focusAttraction" :toggleAttractionInItinerary="toggleAttractionInItinerary" />
</div>
<div :class="{ 'hidden md:block': activeView === 'list' }" class="md:w-1/2 md:pl-4">
<div id="map" class="h-[calc(100vh-200px)] rounded-lg shadow-lg"></div>
<MapView :homeLocation="homeLocation" :attractions="attractions" :focusAttraction="focusAttraction" :calculateDirections="calculateDirections" />
</div>
</div>
</div>
</template>
<style scoped>
@import './styles.css';

.attraction-list-enter-active,
.attraction-list-leave-active {
transition: all 0.5s ease;
}
.attraction-list-enter-from,
.attraction-list-leave-to {
opacity: 0;
transform: translateY(30px);
}

</style>
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { getCurrentInstance } from 'vue';
import AttractionList from '@/components/AttractionList.vue';
import MapView from '@/components/MapView.vue';
import Filters from '@/components/Filters.vue';

const attractions = ref([]);
const minRatingsCount = ref(0);
Expand Down