Finalized codebase
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
<script>
|
||||
import { defineNuxtComponent } from 'nuxt/app';
|
||||
|
||||
import SlideDetails from './WeatherModals/SlideDetails.vue';
|
||||
import AddLocation from './WeatherModals/AddLocation.vue';
|
||||
import DeleteLocation from './WeatherModals/DeleteLocation.vue';
|
||||
|
||||
export default defineNuxtComponent({
|
||||
name: 'WeatherDashboard',
|
||||
components: {
|
||||
SlideDetails,
|
||||
AddLocation,
|
||||
DeleteLocation,
|
||||
},
|
||||
data: () => ({
|
||||
apiBaseURL: 'http://10.1.1.2:8880',
|
||||
apiURL: '',
|
||||
addModalOpen: false,
|
||||
addConfirmModalOpen: false,
|
||||
deleteModalOpen: false,
|
||||
detailsModalOpen: false,
|
||||
selectedLocationID: false,
|
||||
userId: 1,
|
||||
runtimeCode: 0,
|
||||
locID: null,
|
||||
locations: [],
|
||||
databaseLocations: [],
|
||||
|
||||
weatherData: [],
|
||||
weatherIcons: {
|
||||
1: [0],
|
||||
2: [57, 63, 65, 66, 67, 73, 75, 77, 80, 81, 82, 85, 86, 95, 96, 99],
|
||||
3: [1, 2, 3],
|
||||
4: [45, 48],
|
||||
5: [51, 53, 61, 66, 71, 55, 56],
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Asynchronously fetches weather data for each location on mount.
|
||||
*
|
||||
* @return {Promise<void>} A promise that resolves once weather data is fetched.
|
||||
*/
|
||||
async mounted() {
|
||||
this.locations = [];
|
||||
await this.getDatabaseLocations(true);
|
||||
if (this.locations.length > 0) {
|
||||
for (let i = 0; i < this.locations.length; i++) {
|
||||
await this.fetchWeatherData(false, this.locations[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Adds a new location ID to the locations array.
|
||||
*
|
||||
* @param {type} idx - The index of the new location ID to add.
|
||||
* @return {type}
|
||||
*/
|
||||
async addNewLocID(idx) {
|
||||
await this.locations.push(idx);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the input location ID from the locations array.
|
||||
*
|
||||
* @param {type} input - description of parameter
|
||||
* @return {type} description of return value
|
||||
*/
|
||||
removeLocID(input) {
|
||||
this.locations = this.locations.filter((l) => l !== input);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the specified modal type to open.
|
||||
*
|
||||
* @param {type} modalType - The type of modal to open
|
||||
* @return {type}
|
||||
*/
|
||||
openModal(modalType) {
|
||||
this[`${modalType}ModalOpen`] = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes a specific modal and performs additional actions if needed.
|
||||
*
|
||||
* @param {type} modalType - The type of modal to close
|
||||
*/
|
||||
closeModal(modalType) {
|
||||
this[`${modalType}ModalOpen`] = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* A description of the entire function.
|
||||
*
|
||||
* @param {type} bulk - description of parameter
|
||||
* @param {type} location - description of parameter
|
||||
* @return {type} description of return value
|
||||
*/
|
||||
async fetchWeatherData(bulk = false, location = null) {
|
||||
await this.getDatabaseLocations(true);
|
||||
this.apiURL = '';
|
||||
if (bulk) {
|
||||
this.apiURL = `${this.apiBaseURL}/locations?places=${this.locations}`;
|
||||
} else {
|
||||
this.apiURL = `${this.apiBaseURL}/locations/${location}`;
|
||||
}
|
||||
|
||||
await fetch(this.apiURL)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
for (let d in json) {
|
||||
if (!(d in this.locations.values())) {
|
||||
const mergedJSON = Object.assign({}, this.weatherData, json);
|
||||
this.weatherData = mergedJSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the weather icon for the specified WMO code.
|
||||
*
|
||||
* @param {type} wmoCode - description of parameter
|
||||
* @return {type} description of return value
|
||||
*/
|
||||
getWeatherIcon(wmoCode) {
|
||||
for (let idx in this.weatherIcons) {
|
||||
if (this.weatherIcons[idx].includes(wmoCode)) {
|
||||
return '/' + idx + '.svg';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the locations from the database and the configured locations by the user.
|
||||
*
|
||||
* @param {type} configured - description of parameter
|
||||
* @return {type} description of return value
|
||||
*/
|
||||
async getDatabaseLocations(configured = false) {
|
||||
if (configured == true) {
|
||||
this.locations = [];
|
||||
this.apiURL = `${this.apiBaseURL}/locations?places=configured&user=${this.userId}`;
|
||||
await fetch(this.apiURL)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
for (let idx in json) {
|
||||
this.locations.push(json[idx].location_id);
|
||||
}
|
||||
});
|
||||
this.apiURL = '';
|
||||
} else {
|
||||
this.apiURL = `${this.apiBaseURL}/locations?places=database_locations`;
|
||||
await fetch(this.apiURL)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
this.databaseLocations = json;
|
||||
});
|
||||
this.apiURL = '';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A function that takes a date in the format 'dd/mm/yyyy' and returns the day of the week.
|
||||
*
|
||||
* @param {type} date - description of parameter
|
||||
* @return {type} description of return value
|
||||
*/
|
||||
getDay(date) {
|
||||
let isoDate = date.split('/');
|
||||
date = isoDate[2] + '-' + isoDate[1] + '-' + isoDate[0];
|
||||
const names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const day = new Date(date).getDay();
|
||||
return names[day];
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a notification with the specified type and input.
|
||||
*
|
||||
* @param {type} type - description of parameter
|
||||
* @param {type} input - description of parameter
|
||||
* @return {type} description of return value
|
||||
*/
|
||||
displayNotification(type, input) {
|
||||
if (type == 'add') {
|
||||
var info = input.name + ', ' + input.country + ' was added succesfully to the list.';
|
||||
}
|
||||
|
||||
useToast().add({
|
||||
id: 'location_add',
|
||||
title: 'Locations updated.',
|
||||
description: info,
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-lg',
|
||||
padding: 'p-4',
|
||||
gap: 'gap-3',
|
||||
timeout: 5000,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-screen mx-auto pt-8">
|
||||
<div class="max-w-5xl mx-auto container">
|
||||
<div class="section">
|
||||
<div>
|
||||
<img class="float-left w-10 mr-2" src="/icon.svg" alt="dashboard icon" />
|
||||
<div class="text-container">
|
||||
<p class="text-md pt-1.5 font-semibold font-sans align-middle">Weather App</p>
|
||||
</div>
|
||||
<div class="min-h-10 mt-14 container">
|
||||
<div>
|
||||
<p class="float-left font-semibold font-sans w-8 mr-2 text-left text-3xl">Locations</p>
|
||||
<div>
|
||||
<button
|
||||
class="float-right text-justify text-xs bg-linkwater-100 text-gray-900 hover:bg-linkwater-300 font-bold py-2 px-4 rounded-md"
|
||||
@click="openModal('add')"
|
||||
>
|
||||
+ Add Location
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 container overflow-visible">
|
||||
<!--
|
||||
Component: AddLocation
|
||||
Description: This component renders a modal for adding a location to the dashboard.
|
||||
Props:
|
||||
- addModalOpen: boolean. Determines if the modal is open or not.
|
||||
- locations: Array. List of locations from the database.
|
||||
- weatherData: Array. List of locations from the API.
|
||||
- databaseLocations: Array. List of locations from the database.
|
||||
- locID: String|Null. ID of the location from the API.
|
||||
Events:
|
||||
- close-modal: Emitted when the modal is closed.
|
||||
- close-prevented: Emitted when the modal is closed by preventing the default action.
|
||||
-->
|
||||
<div>
|
||||
<div v-if="this.addModalOpen">
|
||||
<AddLocation
|
||||
@close-modal="closeModal('add')"
|
||||
@close-prevented="closeModal('add')"
|
||||
:modalState="this.addModalOpen"
|
||||
:locations="this.locations"
|
||||
:weatherData="this.weatherData"
|
||||
:databaseLocations="this.databaseLocations"
|
||||
:locID="this.locID"
|
||||
:apiBaseURL="this.apiBaseURL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Component: DeleteLocation
|
||||
Description: This component renders a modal for deleting a location from the dashboard.
|
||||
Props:
|
||||
- deleteModalOpen: boolean. Determines if the modal is open or not.
|
||||
- weatherData: Array. List of locations from the API.
|
||||
- locations: Array. List of locations from the database.
|
||||
- locID: String|Null. ID of the location from the API.
|
||||
Events:
|
||||
- close-modal: Emitted when the modal is closed.
|
||||
- close-prevented: Emitted when the modal is closed by preventing the default action.
|
||||
-->
|
||||
<div>
|
||||
<div v-if="this.deleteModalOpen">
|
||||
<DeleteLocation
|
||||
@close-modal="closeModal('delete')"
|
||||
@close-prevented="closeModal('delete')"
|
||||
:modalState="this.deleteModalOpen"
|
||||
:weatherData="this.weatherData"
|
||||
:locations="this.locations"
|
||||
:locID="this.locID"
|
||||
:apiBaseURL="this.apiBaseURL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
Component: SlideDetails
|
||||
Description: This component renders a modal for viewing the details of a specific location.
|
||||
Props:
|
||||
- detailsModalOpen: boolean. Determines if the modal is open or not.
|
||||
- weatherData: Array. List of locations from the API.
|
||||
- locID: String. ID of the location from the API.
|
||||
Events:
|
||||
- close-modal: Emitted when the modal is closed.
|
||||
- close-prevented: Emitted when the modal is closed by preventing the default action.
|
||||
-->
|
||||
<div>
|
||||
<div v-if="this.detailsModalOpen">
|
||||
<SlideDetails
|
||||
@close-modal="closeModal('details')"
|
||||
@close-prevented="closeModal('details')"
|
||||
:modalState="this.detailsModalOpen"
|
||||
:weatherData="this.weatherData"
|
||||
:locID="this.locID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Table: Weather Data Table
|
||||
Description: This table displays the weather data for each location.
|
||||
Props:
|
||||
- weatherData: Object. List of locations from the API.
|
||||
-->
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<!-- Table Header -->
|
||||
<thead class="text-md text-gray-100 bg-mineshaft-900">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">Location</th>
|
||||
<th scope="col" class="px-6 py-3">Temperature</th>
|
||||
<th scope="col" class="px-6 py-3">Rainfall</th>
|
||||
<th scope="col" class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- Table Body -->
|
||||
<tbody>
|
||||
<!-- Check if weatherData is not empty -->
|
||||
<template v-if="Object.keys(this.weatherData) != 0">
|
||||
<!-- Loop through each location in weatherData -->
|
||||
<tr
|
||||
v-for="loc in this.weatherData"
|
||||
class="bg-woodsmoke-950 border-tuatara-900 border-b-2 text-gray-300"
|
||||
>
|
||||
<!-- Location Name -->
|
||||
<td class="px-6 py-4">
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
@click="
|
||||
openModal('details');
|
||||
this.locID = loc.id;
|
||||
"
|
||||
>
|
||||
<img
|
||||
class="float-left w-8 mr-3"
|
||||
:src="getWeatherIcon(loc.data.current.weather_code)"
|
||||
alt="weather icon"
|
||||
/>
|
||||
<div class="text-container">
|
||||
<p class="text-sm pt-1.5 font-semibold font-sans align-middle">
|
||||
{{ loc.data.location.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Temperature -->
|
||||
<td class="px-6 py-4">{{ Math.ceil(loc.data.current.temperature_2m) }} °C</td>
|
||||
<!-- Rainfall -->
|
||||
<td class="px-6 py-4">
|
||||
{{ Math.round(loc.data.current.rain * 100) / 100 }} mm
|
||||
</td>
|
||||
<!-- Delete Button -->
|
||||
<td class="float-right px-6 py-4">
|
||||
<UButton
|
||||
icon="i-heroicons-trash"
|
||||
size="sm"
|
||||
color="mineshaft"
|
||||
square
|
||||
variant="ghost"
|
||||
@click="
|
||||
openModal('delete');
|
||||
this.locID = loc.id;
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Show message if weatherData is empty -->
|
||||
<template v-else>
|
||||
<tr class="bg-woodsmoke-950 border-tuatara-900 border-b-2 text-gray-300">
|
||||
<td colspan="4" class="px-6 pt-10 pb-8">
|
||||
<p class="text-pretty text-center">Please add a location</p>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Notification Component -->
|
||||
<UNotifications />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <strong>DEBUG</strong>
|
||||
<div class="pt-10">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-md text-gray-100 bg-mineshaft-900">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">Location</th>
|
||||
<th scope="col" class="px-6 py-3">Temperature</th>
|
||||
<th scope="col" class="px-6 py-3">Rainfall</th>
|
||||
<th scope="col" class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-if="locations.length > 0" v-for="loc in this.weatherData">
|
||||
<tr colspan="4" class="w-full bg-woodsmoke-950 border-tuatara-900 border-b-2 text-gray-300">
|
||||
<td class="w-full">
|
||||
<pre>{{ loc }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<strong>DEBUG</strong> -->
|
||||
</template>
|
||||
@@ -0,0 +1,313 @@
|
||||
<script>
|
||||
import { defineNuxtComponent } from 'nuxt/app';
|
||||
import utils from '~/assets/utils';
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modalState: {
|
||||
type: Boolean,
|
||||
},
|
||||
weatherData: {
|
||||
type: Object,
|
||||
},
|
||||
databaseLocations: {
|
||||
type: Object,
|
||||
},
|
||||
locations: {
|
||||
type: Object,
|
||||
},
|
||||
apiBaseURL: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cmpDatabaseLocations() {
|
||||
return this.databaseLocations;
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
weatherIcons: utils.weatherIcons,
|
||||
apiURL: '',
|
||||
searchInput: '',
|
||||
selectedIndex: '',
|
||||
searchVisible: true,
|
||||
userId: 1,
|
||||
runtimeCode: 0,
|
||||
locationData: [],
|
||||
}),
|
||||
methods: {
|
||||
getDay(date) {
|
||||
return utils.getDay(date);
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close-modal');
|
||||
},
|
||||
async addNewLocID(idx) {
|
||||
await this.$parent.addNewLocID(idx);
|
||||
},
|
||||
getWeatherIcon(wmoCode) {
|
||||
return utils.getWeatherIcon(wmoCode);
|
||||
},
|
||||
async addLocation(input) {
|
||||
this.apiURL = `${this.apiBaseURL}/locations`;
|
||||
if (input) {
|
||||
await this.getDatabaseLocations(true);
|
||||
let idx = 0;
|
||||
for (let i = 0; i < this.cmpDatabaseLocations.length; i++) {
|
||||
if (
|
||||
this.databaseLocations[i]['name'].includes(input.name) &&
|
||||
this.databaseLocations[i]['country'].includes(input.country)
|
||||
) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx > 0) {
|
||||
if (!this.locations.includes(idx + 1)) {
|
||||
await this.addNewLocID(idx + 1);
|
||||
await this.fetchWeatherData(false, idx + 1);
|
||||
}
|
||||
|
||||
await $fetch(this.apiURL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: idx + 1,
|
||||
name: 'existing',
|
||||
country: '',
|
||||
longitude: 0,
|
||||
latitude: 0,
|
||||
user: this.userId,
|
||||
}),
|
||||
});
|
||||
this.closeModal('add');
|
||||
} else {
|
||||
idx = this.databaseLocations.length + 1;
|
||||
await $fetch(this.apiURL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: idx,
|
||||
name: input.name,
|
||||
country: input.country,
|
||||
longitude: input.longitude,
|
||||
latitude: input.latitude,
|
||||
user: this.userId,
|
||||
}),
|
||||
});
|
||||
this.fetchWeatherData(false, idx);
|
||||
this.displayNotification('add', input);
|
||||
this.closeModal('add');
|
||||
}
|
||||
}
|
||||
},
|
||||
async searchLocations() {
|
||||
this.apiURL = '';
|
||||
if (this.searchInput.length > 1) {
|
||||
this.locationData = [];
|
||||
|
||||
this.apiURL = this.apiBaseURL + '/location/search?query=' + this.searchInput;
|
||||
console.log(this.apiURL);
|
||||
await fetch(this.apiURL)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
this.locationData = json;
|
||||
this.searchVisible = true;
|
||||
});
|
||||
} else {
|
||||
this.locationData = [];
|
||||
this.searchVisible = false;
|
||||
}
|
||||
},
|
||||
getCountryFlag(countryCode) {
|
||||
this.runtimeCode = 0;
|
||||
try {
|
||||
countryCode = countryCode.toLowerCase();
|
||||
} catch (error) {
|
||||
countryCode = 'xx';
|
||||
this.runtimeCode = 1;
|
||||
}
|
||||
|
||||
return `/flags/4x3/${countryCode}.svg`;
|
||||
},
|
||||
toggleSelection(input) {
|
||||
if (this.locationData) {
|
||||
if (this.selectedIndex) {
|
||||
this.locationData[this.selectedIndex].selected = !this.locationData[this.selectedIndex].selected;
|
||||
this.selectedIndex = '';
|
||||
}
|
||||
if (input != 'deselect') {
|
||||
this.selectedIndex = input.index;
|
||||
this.locationData[input.index].selected = !this.locationData[input.index].selected;
|
||||
}
|
||||
}
|
||||
},
|
||||
async getDatabaseLocations() {
|
||||
await this.$parent.getDatabaseLocations();
|
||||
},
|
||||
async fetchWeatherData(bulk = false, location = null) {
|
||||
await this.$parent.fetchWeatherData(bulk, location);
|
||||
},
|
||||
displayNotification(type, input) {
|
||||
this.$parent.displayNotification(type, input);
|
||||
},
|
||||
},
|
||||
name: 'Slideover Details',
|
||||
emits: ['close-modal'],
|
||||
});
|
||||
|
||||
const addOpen = ref(false);
|
||||
|
||||
defineShortcuts({
|
||||
escape: {
|
||||
usingInput: true,
|
||||
whenever: [addOpen],
|
||||
handler: () => {
|
||||
addOpen.value = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UModal
|
||||
:ui="{
|
||||
base: 'bg-opacity-0 overflow-visible min-w-80 min-h-50 ',
|
||||
overlay: {
|
||||
base: 'fixed inset-0 transition-opacity',
|
||||
ring: '',
|
||||
background: 'bg-black bg-opacity-80',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0',
|
||||
},
|
||||
},
|
||||
}"
|
||||
v-model="modalState"
|
||||
prevent-close
|
||||
:overlay="true"
|
||||
@close-prevented="closeModal"
|
||||
@close="closeModal"
|
||||
>
|
||||
<UCard
|
||||
:ui="{
|
||||
background: 'bg-woodsmoke-950',
|
||||
divide: '',
|
||||
ring: 'ring-0',
|
||||
header: {
|
||||
base: '',
|
||||
padding: 'pb-3',
|
||||
},
|
||||
body: {
|
||||
base: 'pt-2 px-8 pb-4',
|
||||
padding: '',
|
||||
},
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base px-4 font-semibold text-gray-200">Add Location</h3>
|
||||
<button
|
||||
class="float-right text-justify text-md bg-shark-950 text-gray-200 hover:bg-linkwater-300 px-1 rounded-md"
|
||||
@click="closeModal"
|
||||
>
|
||||
<UIcon name="i-heroicons-x-mark-16-solid" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center">
|
||||
<p class="text-base mx-auto text-center text-gray-200">Add Location</p>
|
||||
</div>
|
||||
|
||||
<UInput
|
||||
class="bg-mineshaft-950 border-mineshaft-950 text-gray-200 rounded-md"
|
||||
v-model="searchInput"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
icon="i-heroicons-magnifying-glass-20-solid"
|
||||
autocomplete="off"
|
||||
variant="none"
|
||||
@keyup="searchLocations(this.searchInput)"
|
||||
:ui="{ icon: { trailing: { pointer: '' } } }"
|
||||
>
|
||||
<template #trailing>
|
||||
<UButton
|
||||
v-show="this.searchInput !== ''"
|
||||
color="gray"
|
||||
variant="link"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
:padded="false"
|
||||
@click="
|
||||
this.searchInput = '';
|
||||
this.locationData = [];
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</UInput>
|
||||
<div class="grid grid-rows-1">
|
||||
<div
|
||||
v-if="searchInput && this.runtimeCode == 0 && this.locationData"
|
||||
class="text-center bg-mineshaft-950 z-50 border-mineshaft-950 text-gray-200"
|
||||
v-for="(loc, index) in this.locationData"
|
||||
:key="loc"
|
||||
>
|
||||
<div
|
||||
@click="
|
||||
toggleSelection({
|
||||
index: index,
|
||||
id: loc.id,
|
||||
name: loc.name,
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
})
|
||||
"
|
||||
:class="{
|
||||
'border-x-0 border-y-0 bg-linkwater-100 text-gray-800': loc.selected,
|
||||
}"
|
||||
class="p-2 border-b-2 columns-3 cursor-pointer hover:bg-linkwater-900"
|
||||
v-if="this.searchVisible"
|
||||
>
|
||||
<div class="column w-1/2">
|
||||
<img class="w-14" :src="getCountryFlag(loc.country_code)" alt="country flag icon" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>{{ loc.name }}</p>
|
||||
<p>{{ loc.country }}</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p v-if="loc.latitude" class="text-sm">Lat: {{ loc.latitude.toFixed(4) }}</p>
|
||||
<p v-if="loc.longitude" class="text-sm">Lon: {{ loc.longitude.toFixed(4) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-center bg-mineshaft-950 z-50 border-mineshaft-950 text-gray-200"
|
||||
v-if="searchInput && !this.searchVisible"
|
||||
>
|
||||
<p class="my-2">No location found!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="this.selectedIndex" class="flex items-center mt-3">
|
||||
<button
|
||||
class="w-full text-center text-sm bg-linkwater-100 text-gray-900 hover:bg-linkwater-300 font-bold py-2 rounded-md"
|
||||
@click="this.addLocation(this.locationData[this.selectedIndex])"
|
||||
>
|
||||
Add Location
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex items-center mt-3">
|
||||
<UTooltip class="w-full" text="Select a location first" :popper="{ arrow: true }">
|
||||
<button
|
||||
class="cursor-not-allowed w-full text-center text-sm bg-linkwater-100 text-gray-900 hover:bg-shark-900 font-bold py-2 rounded-md"
|
||||
>
|
||||
Add Location
|
||||
</button>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,167 @@
|
||||
<script>
|
||||
import { defineNuxtComponent } from 'nuxt/app';
|
||||
import utils from '~/assets/utils';
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modalState: {
|
||||
type: Boolean,
|
||||
},
|
||||
weatherData: {
|
||||
type: Object,
|
||||
},
|
||||
databaseLocations: {
|
||||
type: Object,
|
||||
},
|
||||
locID: {
|
||||
type: Number,
|
||||
},
|
||||
locations: {
|
||||
type: Object,
|
||||
},
|
||||
apiBaseURL: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
apiURL: '',
|
||||
userId: 1,
|
||||
deleteBootstrapData: false,
|
||||
}),
|
||||
methods: {
|
||||
getDay(date) {
|
||||
return utils.getDay(date);
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close-modal');
|
||||
},
|
||||
getWeatherIcon(wmoCode) {
|
||||
return utils.getWeatherIcon(wmoCode);
|
||||
},
|
||||
async removeLocID(input) {
|
||||
await this.$parent.removeLocID(input);
|
||||
},
|
||||
async deleteLocation(input) {
|
||||
this.apiURL = '';
|
||||
this.apiURL = `${this.apiBaseURL}/locations/${input}`;
|
||||
await this.$emit('close-modal');
|
||||
|
||||
await this.removeLocID(input);
|
||||
await delete this.weatherData[input];
|
||||
|
||||
if (this.deleteBootstrapData == true) {
|
||||
await fetch(`${this.apiBaseURL}/locations/${input}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({
|
||||
id: input,
|
||||
user: this.userId,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(async (json) => {
|
||||
console.log('Deleted: ' + input);
|
||||
});
|
||||
}
|
||||
this.apiURL = '';
|
||||
await this.getDatabaseLocations();
|
||||
},
|
||||
async getDatabaseLocations() {
|
||||
await this.$parent.getDatabaseLocations();
|
||||
},
|
||||
async fetchWeatherData(bulk = false, location = null) {
|
||||
await this.$parent.fetchWeatherData(bulk, location);
|
||||
},
|
||||
displayNotification(type, input) {
|
||||
this.$parent.displayNotification(type, input);
|
||||
},
|
||||
},
|
||||
name: 'Delete Location',
|
||||
emits: ['close-modal'],
|
||||
});
|
||||
|
||||
const delOpen = ref(false);
|
||||
|
||||
defineShortcuts({
|
||||
escape: {
|
||||
usingInput: true,
|
||||
whenever: [delOpen],
|
||||
handler: () => {
|
||||
delOpen.value = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UModal
|
||||
:ui="{
|
||||
base: 'w-80 min-w-20 h-50 bg-opacity-0',
|
||||
overlay: {
|
||||
base: 'fixed inset-0 transition-opacity',
|
||||
background: 'bg-black bg-opacity-80',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0',
|
||||
},
|
||||
},
|
||||
}"
|
||||
v-model="modalState"
|
||||
prevent-close
|
||||
:overlay="true"
|
||||
@close-prevented="closeModal"
|
||||
@close="closeModal"
|
||||
>
|
||||
<UCard
|
||||
:ui="{
|
||||
background: 'bg-woodsmoke-950',
|
||||
divide: '',
|
||||
ring: '',
|
||||
header: {
|
||||
base: '',
|
||||
padding: 'pb-3 px-4',
|
||||
},
|
||||
body: {
|
||||
base: 'py-2 px-4',
|
||||
padding: '',
|
||||
},
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold text-gray-200">Delete Location</h3>
|
||||
<button
|
||||
class="float-right text-justify text-md bg-shark-950 text-gray-200 hover:bg-linkwater-300 px-1 rounded-md"
|
||||
@click="closeModal"
|
||||
>
|
||||
<UIcon name="i-heroicons-x-mark-16-solid" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center">
|
||||
<p class="text-base mx-auto text-center text-gray-200">
|
||||
Remove the selected location ? ({{ this.weatherData[this.locID].data.location.name }})
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center m-6">
|
||||
<button
|
||||
class="mx-auto text-center text-xs bg-linkwater-100 text-gray-900 hover:bg-linkwater-300 font-bold py-2 px-4 rounded-md"
|
||||
@click="deleteLocation(this.locID)"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
class="mx-auto text-center text-xs bg-linkwater-100 text-gray-900 hover:bg-linkwater-300 font-bold py-2 px-4 rounded-md"
|
||||
@click="closeModal"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script>
|
||||
import { defineNuxtComponent } from 'nuxt/app';
|
||||
import utils from '~/assets/utils';
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modalState: {
|
||||
type: Boolean,
|
||||
},
|
||||
weatherData: {
|
||||
type: Object,
|
||||
},
|
||||
locID: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
weatherIcons: utils.weatherIcons,
|
||||
}),
|
||||
methods: {
|
||||
getDay(date) {
|
||||
return utils.getDay(date);
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close-modal');
|
||||
},
|
||||
getWeatherIcon(wmoCode) {
|
||||
return utils.getWeatherIcon(wmoCode);
|
||||
},
|
||||
},
|
||||
name: 'Slideover Details',
|
||||
emits: ['close-modal'],
|
||||
});
|
||||
|
||||
const detailsOpen = ref(false);
|
||||
|
||||
defineShortcuts({
|
||||
escape: {
|
||||
usingInput: true,
|
||||
whenever: [detailsOpen],
|
||||
handler: () => {
|
||||
detailsOpen.value = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<USlideover
|
||||
:ui="{
|
||||
base: 'min-w-100 min-h-70 h-full',
|
||||
background: 'bg-opacity-0',
|
||||
overlay: {
|
||||
base: 'fixed inset-0 transition-opacity',
|
||||
background: 'bg-black bg-opacity-80',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0',
|
||||
},
|
||||
},
|
||||
}"
|
||||
prevent-close
|
||||
v-model="modalState"
|
||||
:overlay="true"
|
||||
@close-prevented="closeModal"
|
||||
@close="closeModal"
|
||||
>
|
||||
<UCard
|
||||
:ui="{
|
||||
background: 'bg-woodsmoke-950 h-full',
|
||||
divide: '',
|
||||
ring: '',
|
||||
preventClose: True,
|
||||
header: {
|
||||
base: '',
|
||||
padding: 'pb-3',
|
||||
},
|
||||
body: {
|
||||
base: 'pt-2 px-6 pb-4',
|
||||
padding: '',
|
||||
},
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-2xl px-2 font-semibold text-gray-200">
|
||||
{{ this.weatherData[this.locID].data.location.name }}
|
||||
</p>
|
||||
<button
|
||||
class="float-right rounded-sm text-justify text-md bg-shark-950 text-gray-200 hover:bg-linkwater-300 px-1"
|
||||
@click="closeModal"
|
||||
>
|
||||
<UIcon class="mt-1" name="i-heroicons-x-mark-16-solid" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p class="text-sm text-silver-400 pb-4">This Week</p>
|
||||
<div class="mb-4 mx-1" v-for="index in 7" :key="index">
|
||||
<div class="container grid grid-cols-3 py-5 bg-mineshaft-900 text-gray-200 px-5 rounded-3xl">
|
||||
<div class="mt-2 col-span-2 text-left items-start">
|
||||
<img
|
||||
class="float-left w-10 mr-3"
|
||||
:src="getWeatherIcon(this.weatherData[this.locID].data.daily.weather_code[index - 1])"
|
||||
alt="weather icon"
|
||||
/>
|
||||
<div class="items-start text-left py-1 align-middle">
|
||||
<strong class="text-left text-3xl font-semibold font-sans align-middle">
|
||||
{{ getDay(this.weatherData[this.locID].data.daily.date[index - 1]) }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<div class="float-right">
|
||||
<span class="text-md text-silver-500 mx-4">Min.</span>
|
||||
<span class="text-md"
|
||||
><strong class="mr-1 text-silver-200">
|
||||
{{
|
||||
Math.ceil(
|
||||
this.weatherData[this.locID].data.daily.temperature_2m_min[index - 1]
|
||||
)
|
||||
}}°C
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="float-right mt-2">
|
||||
<span class="text-md text-silver-500 mx-4">Max.</span>
|
||||
<span class="text-md"
|
||||
><strong class="mr-1 text-silver-200">
|
||||
{{
|
||||
Math.ceil(
|
||||
this.weatherData[this.locID].data.daily.temperature_2m_max[index - 1]
|
||||
)
|
||||
}}°C
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</USlideover>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user