Frontend revamp with Vue

[frontend] Gear view implemented
[frontend] Activity view started
[frontend] Map logic separated from ActivitySummary logic
[frontend] Fixed minor UI issues
This commit is contained in:
João Vitória Silva
2024-02-23 11:13:56 +00:00
parent 7625dc441c
commit bc55e7346d
20 changed files with 938 additions and 175 deletions

View File

@@ -0,0 +1,102 @@
<template>
<!-- map zone -->
<div v-if="isLoading">
<LoadingComponent />
</div>
<div v-else>
<div ref="activityMap" class="map" style="height: 300px;" v-if="source === 'home'"></div>
<div ref="activityMap" class="map" style="height: 500px;" v-if="source === 'activity'"></div>
</div>
</template>
<script>
import { ref, onMounted, watchEffect, nextTick } from 'vue';
import { activityStreams } from '@/services/activityStreams';
import LoadingComponent from '@/components/LoadingComponent.vue';
import L from 'leaflet';
export default {
components: {
LoadingComponent,
},
props: {
activity: {
type: Object,
required: true,
},
source:{
type: String,
required: true,
}
},
setup(props) {
const isLoading = ref(true);
const activityStreamLatLng = ref(null);
const activityMap = ref(null);
const source = ref(props.source);
onMounted(async () => {
try {
activityStreamLatLng.value = await activityStreams.getActivitySteamByStreamTypeByActivityId(props.activity.id, 7);
} catch (error) {
console.error("Failed to fetch activity details:", error);
} finally {
isLoading.value = false;
nextTick(() => {
nextTick(() => {
if (activityStreamLatLng.value) {
initMap();
}
});
});
}
});
watchEffect(() => {
});
const initMap = () => {
if (!activityMap.value) return;
const waypoints = activityStreamLatLng.value.stream_waypoints;
const map = L.map(activityMap.value, {
dragging: false, // Disable panning
touchZoom: false, // Disable touch zoom
scrollWheelZoom: false, // Disable scroll wheel zoom
zoomControl: false // Remove zoom control buttons
}).fitWorld();
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
const latlngs = waypoints.map(waypoint => [waypoint.lat, waypoint.lon]);
L.polyline(latlngs, { color: 'blue' }).addTo(map);
// Fit map to polyline bounds
if (latlngs.length > 0) {
map.fitBounds(latlngs);
// Add start and end markers
L.marker(latlngs[0], {
icon: L.divIcon({ className: 'bg-success dot' })
}).addTo(map);
L.marker(latlngs[latlngs.length - 1], {
icon: L.divIcon({ className: 'bg-danger dot' })
}).addTo(map);
}
};
return {
isLoading,
activityStreamLatLng,
activityMap,
source,
};
},
};
</script>

View File

@@ -1,116 +1,193 @@
<template>
<div class="card">
<div class="card-body">
<div v-if="isLoading">
<LoadingComponent />
</div>
<div v-else>
<div class="d-flex justify-content-between">
<!-- user name and photo zone -->
<div class="d-flex align-items-center">
<img :src="userActivity.photo_path" alt="User Photo" width="55" height="55" class="rounded-circle" v-if="userActivity.photo_path">
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="55" height="55" class="rounded-circle" v-else-if="!userActivity.photo_path && userActivity.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="55" height="55" class="rounded-circle" v-else>
<div class="ms-3 me-3">
<div class="fw-bold">
<a href="#" class="link-underline-opacity-25 link-underline-opacity-100-hover">
{{ activity.name }}
</a>
</div>
<h6>
<span v-if="activity.activity_type == 1 || activity.activity_type == 2">
<font-awesome-icon :icon="['fas', 'person-running']" />
</span>
<span v-else-if="activity.activity_type == 3">
<font-awesome-icon :icon="['fas', 'person-running']" /> (Virtual)
</span>
<span v-else-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6">
<font-awesome-icon :icon="['fas', 'fa-person-biking']" />
</span>
<span v-else-if="activity.activity_type == 7">
<font-awesome-icon :icon="['fas', 'fa-person-biking']" /> (Virtual)
</span>
<span v-else-if="activity.activity_type == 8 || activity.activity_type == 9">
<font-awesome-icon :icon="['fas', 'fa-person-swimming']" />
</span>
<span v-else>
<font-awesome-icon :icon="['fas', 'fa-dumbbell']" />
</span>
<span>{{ " " + formatDate(activity.start_time) }}</span> @
<span>{{ formatTime(activity.start_time) }}</span>
<!-- Conditionally display city and country -->
<span v-if="activity.city || activity.country">
-
<span v-if="activity.town">{{ activity.town }},</span>
<span v-if="activity.country">{{ " " + activity.country }}</span>
</span>
</h6>
</div>
<div v-if="isLoading">
<LoadingComponent />
</div>
<div v-else>
<div class="d-flex justify-content-between">
<!-- user name and photo zone -->
<div class="d-flex align-items-center">
<img :src="userActivity.photo_path" alt="User Photo" width="55" height="55" class="rounded-circle" v-if="userActivity.photo_path">
<img src="/src/assets/avatar/male1.png" alt="Default Male Avatar" width="55" height="55" class="rounded-circle" v-else-if="!userActivity.photo_path && userActivity.gender == 1">
<img src="/src/assets/avatar/female1.png" alt="Default Female Avatar" width="55" height="55" class="rounded-circle" v-else>
<div class="ms-3 me-3">
<div class="fw-bold">
<router-link :to="{ name: 'activity', params: { id: activity.id }}" class="link-underline-opacity-25 link-underline-opacity-100-hover" v-if="source === 'home'">
{{ activity.name}}
</router-link>
<router-link :to="{ name: 'user', params: { id: userActivity.id }}" class="link-underline-opacity-25 link-underline-opacity-100-hover" v-if="source === 'activity'">
{{ userActivity.name}}
</router-link>
</div>
<div class="dropdown d-flex" v-if="activity.strava_activity_id">
<a class="btn btn-link btn-lg mt-1" :href="`https://www.strava.com/activities/${activity.strava_activity_id}`" role="button">
<font-awesome-icon :icon="['fab', 'fa-strava']" />
<h6>
<span v-if="activity.activity_type == 1 || activity.activity_type == 2">
<font-awesome-icon :icon="['fas', 'person-running']" />
</span>
<span v-else-if="activity.activity_type == 3">
<font-awesome-icon :icon="['fas', 'person-running']" /> (Virtual)
</span>
<span v-else-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6">
<font-awesome-icon :icon="['fas', 'fa-person-biking']" />
</span>
<span v-else-if="activity.activity_type == 7">
<font-awesome-icon :icon="['fas', 'fa-person-biking']" /> (Virtual)
</span>
<span v-else-if="activity.activity_type == 8 || activity.activity_type == 9">
<font-awesome-icon :icon="['fas', 'fa-person-swimming']" />
</span>
<span v-else>
<font-awesome-icon :icon="['fas', 'fa-dumbbell']" />
</span>
<span>{{ " " + formatDate(activity.start_time) }}</span> @
<span>{{ formatTime(activity.start_time) }}</span>
<!-- Conditionally display city and country -->
<span v-if="activity.city || activity.country">
-
<span v-if="activity.town">{{ activity.town }},</span>
<span v-if="activity.country">{{ " " + activity.country }}</span>
</span>
</h6>
</div>
</div>
<div class="dropdown d-flex">
<a class="btn btn-link btn-lg mt-1" :href="`https://www.strava.com/activities/${activity.strava_activity_id}`" role="button" v-if="activity.strava_activity_id">
<font-awesome-icon :icon="['fab', 'fa-strava']" />
</a>
<div v-if="source === 'activity'">
<button class="btn btn-link btn-lg" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<font-awesome-icon :icon="['fas', 'fa-ellipsis-vertical']" />
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" :class="{ disabled: activity.strava_activity_id }" href="#" data-bs-toggle="modal" data-bs-target="#deleteActivityModal">
{{ $t("activitySummary.buttonDeleteActivity") }}
</a>
</li>
</ul>
</div>
</div>
</div>
<!-- Modal delete gear -->
<div class="modal fade" id="deleteActivityModal" tabindex="-1" aria-labelledby="deleteActivityModal"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteActivityModal">
{{ $t("activitySummary.buttonDeleteActivity") }}
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<span>{{ $t("activitySummary.modalDeleteBody1") }}<b>{{ activity.name }}</b>?</span>
<br>
<span>{{ $t("activitySummary.modalDeleteBody2") }}</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{{ $t("generalItens.buttonClose") }}
</button>
<a type="button" class="btn btn-danger" href="#">
{{ $t("activitySummary.buttonDeleteActivity") }}
</a>
</div>
</div>
<div class="row d-flex mt-3">
<div class="col">
<span class="fw-lighter">
{{ $t("activitySummary.activityDistance") }}
</span>
<br>
<span>
<!-- Check if activity_type is not 9 -->
{{ activity.activity_type != 9
? (activity.distance / 1000).toFixed(2) + ' km' : activity.distance + ' m'
}}
</span>
</div>
<div class="col border-start border-opacity-50">
<span class="fw-lighter">
{{ $t("activitySummary.activityTime") }}
</span>
<br>
<span>{{ calculateTimeDifference(activity.start_time, activity.end_time) }}</span>
</div>
<div class="col border-start border-opacity-50">
<div v-if="activity.activity_type != 9 && activity.activity_type != 1">
<span class="fw-lighter">
{{ $t("activitySummary.activityElevationGain") }}
</span>
<br>
<span>{{ activity.elevation_gain }} m</span>
</div>
<div v-else-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3 || activity.activity_type == 9">
<span class="fw-lighter">
{{ $t("activitySummary.activityPace") }}
</span>
<br>
{{ formattedPace }}
</div>
</div>
</div>
</div>
</div>
<!-- map zone -->
<div class="mx-3 mb-3" v-if="isLoading">
<LoadingComponent />
</div>
<div class="mx-3 mb-3" v-else>
<div ref="activityMap" class="map" style="height: 300px;"></div>
<!-- Activity title -->
<h1 class="mt-3" v-if="source === 'activity'">
{{ activity.name }}
</h1>
<!-- Activity summary -->
<div class="row d-flex mt-3">
<div class="col">
<span class="fw-lighter">
{{ $t("activitySummary.activityDistance") }}
</span>
<br>
<span>
<!-- Check if activity_type is not 9 -->
{{ activity.activity_type != 9
? (activity.distance / 1000).toFixed(2) + ' km' : activity.distance + ' m'
}}
</span>
</div>
<div class="col border-start border-opacity-50">
<span class="fw-lighter">
{{ $t("activitySummary.activityTime") }}
</span>
<br>
<span>{{ calculateTimeDifference(activity.start_time, activity.end_time) }}</span>
</div>
<div class="col border-start border-opacity-50">
<div v-if="activity.activity_type != 9 && activity.activity_type != 1">
<span class="fw-lighter">
{{ $t("activitySummary.activityElevationGain") }}
</span>
<br>
<span>{{ activity.elevation_gain }} m</span>
</div>
<div v-else-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3 || activity.activity_type == 9">
<span class="fw-lighter">
{{ $t("activitySummary.activityPace") }}
</span>
<br>
{{ formattedPace }}
</div>
</div>
</div>
<div class="row d-flex mt-3" v-if="source === 'activity'">
<!-- activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3 -->
<div class="col" v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
<span class="fw-lighter">
{{ $t("activitySummary.activityAvgPower") }}
</span>
<br>
<span v-if="activity.average_power">{{ activity.average_power }} W</span>
<span v-else>{{ $t("activitySummary.activityNoData") }}</span>
</div>
<div class="col border-start border-opacity-50" v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
<span class="fw-lighter">{{ $t("activitySummary.activityEleGain") }}</span>
<br>
<span>{{ activity.elevation_gain }} m</span>
</div>
<div class="col border-start border-opacity-50" v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
<span class="fw-lighter">
{{ $t("activitySummary.activityEleLoss") }}
</span>
<br>
<span>{{ activity.elevation_loss }} m</span>
</div>
<!-- activity.activity_type != 9 || activity.activity_type != 1 -->
<div class="col">
<span class="fw-lighter">{{ $t("activitySummary.activityAvgSpeed") }}</span>
<br>
<span>{{ (activity.average_speed * 3.6).toFixed(0) }} km/h</span>
</div>
<div class="col border-start border-opacity-50">
<span class="fw-lighter">
{{ $t("activitySummary.activityAvgPower") }}
</span>
<br>
<span v-if="activity.average_power">{{ activity.average_power }} W</span>
<span v-else>{{ $t("activitySummary.activityNoData") }}</span>
</div>
<div class="col border-start border-opacity-50">
<span class="fw-lighter">{{ $t("activitySummary.activityEleLoss") }}</span>
<br>
<span>{{ activity.elevation_loss }} m</span>
</div>
</div>
</div>
<br>
</template>
<script>
import { ref, onMounted, watchEffect, computed, nextTick } from 'vue';
import { users } from '@/services/user';
import { activityStreams } from '@/services/activityStreams';
import LoadingComponent from '@/components/LoadingComponent.vue';
import { formatDate, formatTime, calculateTimeDifference } from '@/utils/dateTimeUtils';
import { formatPace } from '@/utils/activityUtils';
import L from 'leaflet';
export default {
components: {
@@ -121,29 +198,24 @@ export default {
type: Object,
required: true,
},
source:{
type: String,
required: true,
}
},
setup(props) {
const isLoading = ref(true);
const userActivity = ref(null);
const activityStreamLatLng = ref(null);
const formattedPace = computed(() => formatPace(props.activity.pace));
const activityMap = ref(null);
const source = ref(props.source);
onMounted(async () => {
try {
userActivity.value = await users.getUserById(props.activity.user_id);
activityStreamLatLng.value = await activityStreams.getActivitySteamByStreamTypeByActivityId(props.activity.id, 7);
} catch (error) {
console.error("Failed to fetch activity details:", error);
} finally {
isLoading.value = false;
nextTick(() => {
nextTick(() => {
if (activityStreamLatLng.value) {
initMap();
}
});
});
}
});
@@ -151,49 +223,14 @@ export default {
});
const initMap = () => {
if (!activityMap.value) return;
const waypoints = activityStreamLatLng.value.stream_waypoints;
const map = L.map(activityMap.value, {
dragging: false, // Disable panning
touchZoom: false, // Disable touch zoom
scrollWheelZoom: false, // Disable scroll wheel zoom
zoomControl: false // Remove zoom control buttons
}).fitWorld();
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
const latlngs = waypoints.map(waypoint => [waypoint.lat, waypoint.lon]);
L.polyline(latlngs, { color: 'blue' }).addTo(map);
// Fit map to polyline bounds
if (latlngs.length > 0) {
map.fitBounds(latlngs);
// Add start and end markers
L.marker(latlngs[0], {
icon: L.divIcon({ className: 'bg-success dot' })
}).addTo(map);
L.marker(latlngs[latlngs.length - 1], {
icon: L.divIcon({ className: 'bg-danger dot' })
}).addTo(map);
}
};
return {
isLoading,
userActivity,
activityStreamLatLng,
formatDate,
formatTime,
calculateTimeDifference,
formattedPace,
activityMap,
source,
};
},
};

View File

@@ -1,5 +1,5 @@
<template>
<footer class="border-top py-3 my-4 bg-body-tertiar">
<footer class="pb-3 pt-4 mt-auto bg-body-tertiary">
<p class="text-center text-muted">&copy; {{ new Date().getFullYear() === 2023 ? '2023' : '2023 - ' + new Date().getFullYear() }} Endurain <a href="https://github.com/joaovitoriasilva/endurain" role="button"><font-awesome-icon :icon="['fab', 'fa-github']" /></a> <a href="https://fosstodon.org/@endurain"><font-awesome-icon :icon="['fab', 'fa-mastodon']" /></a> v0.1.6</p>
<p class="text-center text-muted"><img src="/src/assets/strava/api_logo_cptblWith_strava_horiz_light.png" alt="Compatible with STRAVA image" height="25" /></p>
</footer>

View File

@@ -9,7 +9,7 @@
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav me-auto mb-2 mb-lg-0">
<!-- if is logged in -->
<a class="nav-link" href="/gears" v-if="isLoggedIn">
<a class="nav-link" :class="{ active: path === '/gears' }" href="/gears" v-if="isLoggedIn">
<font-awesome-icon :icon="['fas', 'fa-bicycle']" />
<span class="ms-1">
{{ $t("navbar.gear") }}
@@ -55,22 +55,38 @@
</template>
<script>
import { watch, ref } from 'vue';
import { auth } from '@/services/auth';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
export default {
setup() {
const router = useRouter();
const route = useRoute();
const path = ref(route.path);
const isLoggedIn = ref(auth.isTokenValid(localStorage.getItem('accessToken')))
function handleLogout() {
auth.removeLoggedUser();
router.push('/login');
}
watch(() => route.path, (newPath, oldPath) => {
path.value = newPath;
// Perform actions based on newPath if needed
if (newPath === '/login' && isLoggedIn.value) {
isLoggedIn.value = auth.isTokenValid(localStorage.getItem('accessToken'));
}
if (oldPath === '/login' && !isLoggedIn.value) {
isLoggedIn.value = auth.isTokenValid(localStorage.getItem('accessToken'));
}
});
return {
isLoggedIn: auth.isTokenValid(localStorage.getItem('accessToken')),
isLoggedIn,
userMe: JSON.parse(localStorage.getItem('userMe')),
handleLogout,
path,
};
},
};

View File

@@ -0,0 +1,4 @@
{
"labelGear": "Gear",
"labelGearNotSet": "Not set"
}

View File

@@ -1,6 +1,14 @@
{
"buttonDeleteActivity": "Delete Activity",
"modalDeleteBody1": "Are you sure you want to delete activity ",
"modalDeleteBody2": "This action cannot be undone.",
"activityDistance": "Distance",
"activityTime": "Time",
"activityElevationGain": "Elevation Gain",
"activityPace": "Pace"
"activityPace": "Pace",
"activityAvgPower": "Avg Power",
"activityAvgSpeed": "Avg Speed",
"activityEleGain": "Elevation gain",
"activityEleLoss": "Elevation loss",
"activityNoData": "No data"
}

View File

@@ -1,3 +1,14 @@
{
"buttonEditGear": "Edit Gear",
"buttonDeleteGear": "Delete Gear",
"modalEditGearIsActiveLabel": "Is Active",
"modalEditGearIsActiveOption1": "Yes",
"modalEditGearIsActiveOption0": "No",
"modalDeleteGearBody1": "Are you sure you want to delete gear",
"modalDeleteGearBody2": "This action cannot be undone.",
"labelDistance": "Distance",
"title": "Gear activities",
"subtitle": "(last 10 activities)",
"labelDate": "Date",
"successGearEdited": "Gear edited successfully"
}

View File

@@ -21,5 +21,8 @@
"gearTypeOption3": "Wetsuit",
"activeState": "Active",
"inactiveState": "Inactive",
"successGearAdded": "Gear added successfully"
"gearFromStrava": "Strava",
"successGearAdded": "Gear added successfully",
"successGearDeleted": "Gear deleted successfully",
"errorGearNotFound": "Gear not found"
}

View File

@@ -3,5 +3,7 @@
"buttonClose": "Close",
"buttonlistAll": "List all",
"requiredField": "Required fields",
"errorFetchingInfo": "Error fetching info"
"errorFetchingInfo": "Error fetching info",
"errorEditingInfo": "Error editing info",
"errorDeletingInfo": "Error deleting info"
}

View File

@@ -5,5 +5,6 @@
"radioUserActivities": "My activities",
"radioFollowerActivities": "Followers activities",
"successActivityAdded": "Activity added successfully",
"errorActivityAdded": "Error adding activity"
"errorActivityAdded": "Error adding activity",
"errorActivityNotFound": "Activity not found"
}

View File

@@ -15,6 +15,7 @@ import enHomeView from './en/homeView.json';
import enLoginView from './en/loginView.json';
import enGearsView from './en/gears/gearsView.json';
import enGearView from './en/gears/gearView.json';
import enActivityView from './en/activityView.json';
import enNotFoundView from './en/notFoundView.json';
// Constructing the messages structure
@@ -29,6 +30,7 @@ const messages = {
login: enLoginView,
gears: enGearsView,
gear: enGearView,
activity: enActivityView,
notFound: enNotFoundView,
},
};

View File

@@ -4,6 +4,8 @@ import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import GearsView from '../views/Gears/GearsView.vue'
import GearView from '../views/Gears/GearView.vue'
import ActivityView from '../views/ActivityView.vue'
import UserView from '../views/UserView.vue'
import NotFoundView from '../views/NotFoundView.vue';
import { auth } from '@/services/auth';
@@ -33,6 +35,16 @@ const router = createRouter({
name: 'gear',
component: GearView
},
{
path: '/activity/:id',
name: 'activity',
component: ActivityView
},
{
path: '/user/:id',
name: 'user',
component: UserView
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',

View File

@@ -7,6 +7,9 @@ export const activities = {
getUserThisMonthStats(user_id) {
return fetchGetRequest(`activities/user/${user_id}/thismonth/distances`);
},
getUserActivitiesByGearId(user_id, gear_id) {
return fetchGetRequest(`activities/user/${user_id}/gear/${gear_id}`);
},
getUserNumberOfActivities(user_id) {
return fetchGetRequest(`activities/user/${user_id}/number`);
},

View File

@@ -1,4 +1,4 @@
import { fetchGetRequest, fetchPostRequest } from '@/utils/serviceUtils';
import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest } from '@/utils/serviceUtils';
export const gears = {
getGearById(gearId) {
@@ -15,5 +15,11 @@ export const gears = {
},
createGear(data) {
return fetchPostRequest('gear/create', data)
},
editGear(gearId, data) {
return fetchPutRequest(`gear/${gearId}/edit`, data);
},
deleteGear(gearId) {
return fetchDeleteRequest(`gear/${gearId}/delete`);
}
};

View File

@@ -139,4 +139,58 @@ export async function fetchPostRequest(url, data, headers = {}) {
}
// Return the JSON response
return response.json();
}
/**
* Sends a PUT request to the specified URL with the provided data.
* @param {string} url - The URL to send the request to.
* @param {Object} data - The data to be sent in the request body.
* @param {Object} headers - Optional headers to be included in the request.
* @returns {Promise<Object>} - A promise that resolves to the JSON response from the server.
* @throws {Error} - If the response status is not ok.
*/
export async function fetchPutRequest(url, data, headers = {}) {
// Create the full URL by combining the API URL with the provided URL
const fullUrl = `${API_URL}${url}`;
// Send the PUT request
const response = await fetch(fullUrl, {
method: 'PUT',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
},
});
// If the response status is not ok, throw an error
if (!response.ok) {
throw new Error('' + response.status);
}
// Return the JSON response
return response.json();
}
/**
* Sends a DELETE request to the specified URL with optional headers.
* @param {string} url - The URL to send the DELETE request to.
* @param {Object} headers - Optional headers to include in the request.
* @returns {Promise<Object>} - A promise that resolves to the JSON response from the server.
* @throws {Error} - If the response status is not ok.
*/
export async function fetchDeleteRequest(url, headers = {}) {
// Create the full URL by combining the API URL with the provided URL
const fullUrl = `${API_URL}${url}`;
// Send the DELETE request
const response = await fetch(fullUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
},
});
// If the response status is not ok, throw an error
if (!response.ok) {
throw new Error('' + response.status);
}
// Return the JSON response
return response.json();
}

View File

@@ -0,0 +1,143 @@
<template>
<LoadingComponent v-if="isLoading"/>
<div v-else>
<ActivitySummaryComponent v-if="activity" :activity="activity" :source="'activity'" />
</div>
<!-- map zone -->
<div class="mt-3 mb-3" v-if="isLoading">
<LoadingComponent />
</div>
<div class="mt-3 mb-3" v-else>
<ActivityMapComponent :activity="activity" :source="'activity'"/>
</div>
<!-- gear zone -->
<hr class="mb-2 mt-2">
<div class="mt-3 mb-3" v-if="isLoading">
<LoadingComponent />
</div>
<div class="d-flex justify-content-between align-items-center" v-else>
<p class="pt-2">
<span class="fw-lighter">
{{ $t("activity.labelGear") }}
</span>
<br>
<span v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
<font-awesome-icon :icon="['fas', 'person-running']" />
</span>
<span v-else-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6 || activity.activity_type == 7">
<font-awesome-icon :icon="['fas', 'fa-person-biking']" />
</span>
<span v-else-if="activity.activity_type == 8 || activity.activity_type == 9">
<font-awesome-icon :icon="['fas', 'fa-person-swimming']" />
</span>
<span class="ms-2" v-if="activity.gear_id">{{ gear.nickname }}</span>
<span class="ms-2" v-else>{{ $t("activity.labelGearNotSet") }}</span>
</p>
<div class="justify-content-end">
<!-- add gear button -->
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal" data-bs-target="#addGearToActivityModal" v-if="!activity.gear_id">
<font-awesome-icon :icon="['fas', 'fa-plus']" />
</a>
<!-- edit gear button -->
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal" data-bs-target="#editGearActivityModal" v-if="activity.gear_id">
<font-awesome-icon :icon="['far', 'fa-pen-to-square']" />
</a>
<!-- Delete zone -->
<a class="btn btn-link btn-lg" href="#" role="button" data-bs-toggle="modal" data-bs-target="#deleteGearActivityModal" v-if="activity.gear_id">
<font-awesome-icon :icon="['fas', 'fa-trash']" />
</a>
</div>
</div>
<!-- graphs -->
<hr class="mb-2 mt-2">
<div>
<br>
<button @click="goBack" type="button" class="w-100 btn btn-primary d-lg-none">{{ $t("generalItens.buttonBack") }}</button>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted, watchEffect, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
// Importing the stores
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
// Importing the components
import ActivitySummaryComponent from '@/components/Activities/ActivitySummaryComponent.vue';
import ActivityMapComponent from '@/components/Activities/ActivityMapComponent.vue';
import NoItemsFoundComponent from '@/components/NoItemsFoundComponents.vue';
import ErrorAlertComponent from '@/components/Alerts/ErrorAlertComponent.vue';
import SuccessAlertComponent from '@/components/Alerts/SuccessAlertComponent.vue';
import LoadingComponent from '@/components/LoadingComponent.vue';
// Importing the services
import { gears } from '@/services/gears';
import { activities } from '@/services/activities';
export default {
components: {
NoItemsFoundComponent,
ActivitySummaryComponent,
ActivityMapComponent,
LoadingComponent,
ErrorAlertComponent,
SuccessAlertComponent,
},
setup (){
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const errorAlertStore = useErrorAlertStore();
const successAlertStore = useSuccessAlertStore();
const isLoading = ref(true);
const errorMessage = ref('');
const successMessage = ref('');
const activity = ref(null);
const gear = ref(null);
/**
* Function to navigate back to the previous page.
*/
function goBack() {
route.go(-1);
}
onMounted(async () => {
try{
activity.value = await activities.getActivityById(route.params.id);
if (!activity.value) {
router.push({ path: '/', query: { activityFound: 'false' } });
}
if (activity.value.gear_id) {
gear.value = await gears.getGearById(activity.value.gear_id);
}
} catch (error) {
if (error.toString().includes('422')) {
router.push({ path: '/', query: { activityFound: 'false' } });
}
// If there is an error, set the error message and show the error alert.
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
errorAlertStore.setAlertMessage(errorMessage.value);
}
isLoading.value = false;
});
return {
isLoading,
activity,
gear,
errorMessage,
successMessage,
goBack,
};
}
};
</script>

View File

@@ -1,3 +1,330 @@
<template>
<h1>Gear View</h1>
</template>
<!-- Error alerts -->
<ErrorAlertComponent v-if="errorMessage"/>
<!-- Success banners -->
<SuccessAlertComponent v-if="successMessage"/>
<div v-if="isLoading">
<LoadingComponent />
</div>
<h1 v-else>{{ gear?.nickname }}</h1>
<div class="row row-gap-3 mt-4">
<!-- left column -->
<div class="col-lg-3 col-md-12">
<!-- Gear photo -->
<div v-if="isLoading">
<LoadingComponent />
</div>
<div v-else>
<div class="justify-content-center align-items-center d-flex">
<img src="/src/assets/avatar/bicycle1.png" alt="Bycicle avatar" width="180" height="180" v-if="gear?.gear_type == 1">
<img src="/src/assets/avatar/running_shoe1.png" alt="Bycicle avatar" width="180" height="180" v-else-if="gear?.gear_type == 2">
<img src="/src/assets/avatar/wetsuit1.png" alt="Bycicle avatar" width="180" height="180" v-else>
</div>
<br>
<div class="vstack justify-content-center align-items-center d-flex">
<!-- badges -->
<div class="hstack justify-content-center">
<span class="badge bg-success-subtle border border-success-subtle text-success-emphasis align-middle" v-if="gear?.is_active == 1">
{{ $t("gears.activeState") }}
</span>
<span class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis align-middle" v-else>
{{ $t("gears.inactiveState") }}
</span>
<span class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-emphasis align-middle" v-if="gear?.gear_type == 1">
{{ $t("gears.gearTypeOption1") }}
</span>
<span class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-emphasis align-middle" v-else-if="gear?.gear_type == 2">
{{ $t("gears.gearTypeOption2") }}
</span>
<span class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-emphasis align-middle" v-else>
{{ $t("gears.gearTypeOption3") }}
</span>
<span class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-emphasis align-middle" v-if="gear?.strava_gear_id">
{{ $t("gears.gearFromStrava") }}
</span>
</div>
</div>
<!-- edit gear zone -->
<button type="button" class="mt-2 w-100 btn btn-primary" :disabled="gear && gear?.strava_gear_id" data-bs-toggle="modal" data-bs-target="#editGearModal">
{{ $t("gear.buttonEditGear") }}
</button>
<!-- Modal edit gear -->
<!-- Modal edit gear -->
<div class="modal fade" id="editGearModal" tabindex="-1" aria-labelledby="editGearModal"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="editGearModal">
{{ $t("gear.buttonEditGear") }}
</h1>
</div>
<form @submit.prevent="submitEditGearForm">
<div class="modal-body">
<!-- brand fields -->
<label for="gearBrandEdit"><b>{{ $t("gears.modalBrand") }}:</b></label>
<input class="form-control" type="text" name="gearBrandEdit" :placeholder='$t("gears.modalBrand")' v-model="brand" maxlength="250">
<!-- model fields -->
<label for="gearModelEdit"><b>{{ $t("gears.modalModel") }}:</b></label>
<input class="form-control" type="text" name="gearModelEdit" :placeholder='$t("gears.modalModel")' v-model="model" maxlength="250">
<!-- nickname fields -->
<label for="gearNicknameEdit"><b>* {{ $t("gears.modalNickname") }}:</b></label>
<input class="form-control" type="text" name="gearNicknameEdit" :placeholder='$t("gears.modalNickname")' v-model="nickname" maxlength="250" required>
<!-- gear type fields -->
<label for="gearTypeEdit"><b>* {{ $t("gears.modalGearTypeLabel") }}:</b></label>
<select class="form-control" name="gearTypeEdit" v-model="gearType" required>
<option value="1">{{ $t("gears.modalGearTypeOption1Bike") }}</option>
<option value="2">{{ $t("gears.modalGearTypeOption2Shoes") }}</option>
<option value="3">{{ $t("gears.modalGearTypeOption3Wetsuit") }}</option>
</select>
<!-- date fields -->
<label for="gearDateEdit"><b>* {{ $t("gears.modalDateLabel") }}:</b></label>
<input class="form-control" type="date" name="gearDateEdit" :placeholder='$t("gears.modalDatePlaceholder")' v-model="date" required>
<!-- gear is_active fields -->
<label for="gearIsActiveEdit"><b>* {{ $t("gear.modalEditGearIsActiveLabel") }}:</b></label>
<select class="form-control" name="gearIsActiveEdit" v-model="isActive" required>
<option value="1">{{ $t("gear.modalEditGearIsActiveOption1") }}</option>
<option value="0">{{ $t("gear.modalEditGearIsActiveOption0") }}</option>
</select>
<p>* {{ $t("generalItens.requiredField") }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
<button type="submit" class="btn btn-success" name="editGear" data-bs-dismiss="modal">{{ $t("gear.buttonEditGear") }}</button>
</div>
</form>
</div>
</div>
</div>
<button type="button" class="mt-2 w-100 btn btn-danger" :disabled="(gear && gear?.strava_gear_id) || (gearActivities && gearActivities.length != 0)" data-bs-toggle="modal" data-bs-target="#deleteGearModal" >
{{ $t("gear.buttonDeleteGear") }}
</button>
<!--<a class="mt-2 w-100 btn btn-danger" :class="{ 'disabled': gear && gear.strava_gear_id }" href="#" role="button" data-bs-toggle="modal" data-bs-target="#deleteGearModal" :aria-disabled="gearActivities && gearActivities.length != 0 ? 'true' : 'false'" @click.prevent="gear && gear.strava_gear_id || (gearActivities && gearActivities.length != 0) ? null : openDeleteModal()">
{{ $t("gear.buttonDeleteGear") }}
</a>-->
<!-- Modal delete gear -->
<div class="modal fade" id="deleteGearModal" tabindex="-1" aria-labelledby="deleteGearModal"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteGearModal">
{{ $t("gear.buttonDeleteGear") }}
</h1>
</div>
<div class="modal-body">
<span>{{ $t("gear.modalDeleteGearBody1") }} <b>
{{ gear?.nickname }}
</b>?</span>
<br>
<span>{{ $t("gear.modalDeleteGearBody2") }}</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
<button @click="submitDeleteGear" type="button" class="btn btn-danger" data-bs-dismiss="modal">
{{ $t("gear.buttonDeleteGear") }}
</button>
</div>
</div>
</div>
</div>
<!-- details -->
<div class="vstack align-items-center">
<span class="mt-2"><strong>{{ $t("gear.labelDistance") }}:</strong> {{ gearDistance }} km</span>
<span class="mt-2" v-if="gear?.brand"><strong>{{ $t("gears.modalBrand") }}:</strong> {{ gear?.brand }}</span>
<span class="mt-2" v-if="gear?.model"><strong>{{ $t("gears.modalModel") }}:</strong> {{ gear?.model }}</span>
</div>
</div>
</div>
<div class="col">
<hr class="mb-2 mt-2 d-sm-none d-block">
<div class="hstack align-items-baseline">
<h5>
{{ $t("gear.title") }}
</h5>
<h6 class="ms-1">
{{ $t("gear.subtitle") }}
</h6>
</div>
<NoItemsFoundComponent v-if="!gearActivities || (gearActivities && gearActivities.length == 0)"/>
<div v-else>
<ul class="list-group list-group-flush" v-for="activity in gearActivities" :key="activity.id" :activity="activity">
<li class="vstack list-group-item d-flex justify-content-between">
<router-link :to="{ name: 'activity', params: { id: activity.id }}">
{{ activity.name}}
</router-link>
<span><strong>{{ $t("gear.labelDate") }}:</strong> {{ formatDate(activity.start_time) }} @ {{ formatTime(activity.start_time) }}</span>
</li>
</ul>
</div>
</div>
</div>
<div>
<br>
<button @click="goBack" type="button" class="w-100 btn btn-primary d-lg-none">{{ $t("generalItens.buttonBack") }}</button>
</div>
</template>
<script>
// Importing the vue composition API
import { ref, onMounted, onUnmounted, watchEffect, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
// Importing the stores
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
// Importing the components
import NoItemsFoundComponent from '@/components/NoItemsFoundComponents.vue';
import ErrorAlertComponent from '@/components/Alerts/ErrorAlertComponent.vue';
import SuccessAlertComponent from '@/components/Alerts/SuccessAlertComponent.vue';
import LoadingComponent from '@/components/LoadingComponent.vue';
// Importing the services
import { gears } from '@/services/gears';
import { activities } from '@/services/activities';
import { formatDate, formatTime } from '@/utils/dateTimeUtils';
export default {
components: {
NoItemsFoundComponent,
LoadingComponent,
ErrorAlertComponent,
SuccessAlertComponent,
},
setup() {
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const errorAlertStore = useErrorAlertStore();
const successAlertStore = useSuccessAlertStore();
const isLoading = ref(true);
const errorMessage = ref('');
const successMessage = ref('');
const gear = ref(null);
const gearActivities = ref([]);
const gearDistance = ref(0);
const brand = ref('');
const model = ref('');
const nickname = ref('');
const gearType = ref(1);
const date = ref(null);
const isActive = ref(1);
/**
* Function to navigate back to the previous page.
*/
function goBack() {
route.go(-1);
}
async function submitEditGearForm() {
try {
const data = {
brand: brand.value,
model: model.value,
nickname: nickname.value,
gear_type: gearType.value,
created_at: date.value,
is_active: isActive.value,
};
await gears.editGear(route.params.id, data);
gear.value.brand = brand.value;
gear.value.model = model.value;
gear.value.nickname = nickname.value;
gear.value.gear_type = gearType.value;
gear.value.created_at = date.value;
gear.value.is_active = isActive.value;
successMessage.value = t('gear.successGearEdited');
successAlertStore.setAlertMessage(successMessage.value);
successAlertStore.setClosableState(true);
} catch {
// If there is an error, set the error message and show the error alert.
errorMessage.value = t('generalItens.errorEditingInfo') + " - " + error.toString();
errorAlertStore.setAlertMessage(errorMessage.value);
}
}
async function submitDeleteGear() {
try {
gear.value = await gears.deleteGear(route.params.id);
router.push({ path: '/gears', query: { gearDeleted: 'true' } });
} catch (error) {
errorMessage.value = t('generalItens.errorDeletingInfo') + " - " + error.toString();
errorAlertStore.setAlertMessage(errorMessage.value);
}
}
/**
* Initializes the component and fetches user gears with pagination.
* Attaches a scroll event listener to the window.
*
* @returns {void}
*/
onMounted(async () => {
try {
// Fetch the gear by its id.
gear.value = await gears.getGearById(route.params.id);
if (!gear.value) {
router.push({ path: '/gears', query: { gearFound: 'false' } });
}
gearActivities.value = await activities.getUserActivitiesByGearId(JSON.parse(localStorage.getItem('userMe')).id, route.params.id);
if (gearActivities.value) {
for (const activity of gearActivities.value) {
gearDistance.value += activity.distance;
}
gearDistance.value = (gearDistance.value / 1000).toFixed(2)
}
brand.value = gear.value.brand;
model.value = gear.value.model;
nickname.value = gear.value.nickname;
gearType.value = gear.value.gear_type;
date.value = gear.value.created_at.split(' ')[0];;
isActive.value = gear.value.is_active;
} catch (error) {
if (error.toString().includes('422')) {
router.push({ path: '/gears', query: { gearFound: 'false' } });
}
// If there is an error, set the error message and show the error alert.
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
errorAlertStore.setAlertMessage(errorMessage.value);
}
isLoading.value = false;
});
return{
brand,
model,
nickname,
gearType,
date,
isActive,
isLoading,
errorMessage,
successMessage,
gear,
gearActivities,
gearDistance,
goBack,
t,
submitEditGearForm,
submitDeleteGear,
formatDate,
formatTime,
}
},
};
</script>

View File

@@ -41,7 +41,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("generalItens.buttonClose") }}</button>
<button type="submit" class="btn btn-success" name="addGear">{{ $t("gears.buttonAddGear") }}</button>
<button type="submit" class="btn btn-success" name="addGear" data-bs-dismiss="modal">{{ $t("gears.buttonAddGear") }}</button>
</div>
</form>
</div>
@@ -59,10 +59,10 @@
</div>
<div class="col">
<!-- Error alerts -->
<ErrorAlertComponent v-if="errorMessage"/>
<ErrorAlertComponent v-if="errorMessage || !gearFound"/>
<!-- Success banners -->
<SuccessAlertComponent v-if="successMessage"/>
<SuccessAlertComponent v-if="successMessage || gearDeleted"/>
<div v-if="isLoading">
<LoadingComponent />
@@ -94,6 +94,7 @@
<div>
<span class="badge bg-success-subtle border border-success-subtle text-success-emphasis align-middle" v-if="gear.is_active == 1">{{ $t("gears.activeState") }}</span>
<span class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis align-middle" v-else>{{ $t("gears.inactiveState") }}</span>
<span class="badge bg-primary-subtle border border-primary-subtle text-primary-emphasis align-middle" v-if="gear.strava_gear_id">{{ $t("gears.gearFromStrava") }}</span>
</div>
</li>
</ul>
@@ -114,7 +115,7 @@
// Importing the vue composition API
import { ref, onMounted, onUnmounted, watchEffect, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
// Importing the stores
import { useSuccessAlertStore } from '@/stores/Alerts/successAlert';
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
@@ -137,9 +138,11 @@ export default {
},
setup() {
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const errorAlertStore = useErrorAlertStore();
const successAlertStore = useSuccessAlertStore();
const gearDeleted = ref(false);
const gearFound = ref(true);
const brand = ref('');
const model = ref('');
const nickname = ref('');
@@ -159,7 +162,7 @@ export default {
* Function to navigate back to the previous page.
*/
function goBack() {
router.go(-1);
route.go(-1);
}
/**
@@ -256,12 +259,6 @@ export default {
successMessage.value = t('gears.successGearAdded');
successAlertStore.setAlertMessage(successMessage.value);
successAlertStore.setClosableState(true);
/*const modalElement = document.getElementById('addGearModal');
const modalInstance = Modal.getInstance(modalElement);
if (modalInstance) {
modalInstance.hide();
}*/
} catch (error) {
// If there is an error, set the error message and show the error alert.
errorMessage.value = t('generalItens.errorFetchingInfo') + " - " + error.toString();
@@ -269,10 +266,6 @@ export default {
}
}
async function submitSearchGearByNickname() {
console.log('submitSearchGearByNickname');
}
/**
* Initializes the component and fetches user gears with pagination.
* Attaches a scroll event listener to the window.
@@ -280,6 +273,20 @@ export default {
* @returns {void}
*/
onMounted(async () => {
if (route.query.gearDeleted === 'true') {
// Set the gearDeleted value to true and show the success alert.
gearDeleted.value = true;
successAlertStore.setAlertMessage(t("gears.successGearDeleted"));
successAlertStore.setClosableState(true);
}
if (route.query.gearFound === 'false') {
// Set the gearFound value to false and show the error alert.
gearFound.value = false;
errorAlertStore.setAlertMessage(t("gears.errorGearNotFound"));
errorAlertStore.setClosableState(true);
}
// Add the event listener for scroll event.
window.addEventListener('scroll', handleScroll);
@@ -345,7 +352,8 @@ export default {
goBack,
t,
submitAddGearForm,
submitSearchGearByNickname,
gearDeleted,
gearFound,
};
},
};

View File

@@ -21,7 +21,7 @@
</div>
<UserDistanceStatsComponent v-else />
</div>
<a class="w-100 btn btn-primary" href="#" role="button" data-bs-toggle="modal" data-bs-target="#addActivityModal">
<a class="w-100 btn btn-primary mb-4" href="#" role="button" data-bs-toggle="modal" data-bs-target="#addActivityModal">
{{ $t("home.buttonAddActivity") }}
</a>
@@ -46,7 +46,7 @@
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{{ $t("generalItens.buttonClose") }}
</button>
<button type="submit" class="btn btn-success">
<button type="submit" class="btn btn-success" data-bs-dismiss="modal">
{{ $t("home.buttonAddActivity") }}
</button>
</div>
@@ -58,7 +58,7 @@
<!-- activities zone -->
<div class="col">
<!-- Error alerts -->
<ErrorAlertComponent v-if="errorMessage"/>
<ErrorAlertComponent v-if="errorMessage || !activityFound"/>
<!-- Success banners -->
<SuccessAlertComponent v-if="successMessage"/>
@@ -82,7 +82,12 @@
<!-- Checking if userActivities is loaded and has length -->
<div v-if="userActivities && userActivities.length">
<!-- Iterating over userActivities to display them -->
<ActivitySummaryComponent v-for="activity in userActivities" :key="activity.id" :activity="activity" />
<div class="card mb-3" v-for="activity in userActivities" :key="activity.id">
<div class="card-body">
<ActivitySummaryComponent :activity="activity" :source="'home'"/>
</div>
<ActivityMapComponent class="mx-3 mb-3" :activity="activity" :source="'home'"/>
</div>
</div>
<!-- Displaying a message or component when there are no activities -->
<NoItemsFoundComponent v-else />
@@ -109,6 +114,7 @@
<script>
import { ref, onMounted, onUnmounted, watchEffect, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { activities } from '@/services/activities';
// Importing the stores
@@ -118,6 +124,7 @@ import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
import UserDistanceStatsComponent from '@/components/Activities/UserDistanceStatsComponent.vue';
import NoItemsFoundComponent from '@/components/NoItemsFoundComponents.vue';
import ActivitySummaryComponent from '@/components/Activities/ActivitySummaryComponent.vue';
import ActivityMapComponent from '@/components/Activities/ActivityMapComponent.vue';
import LoadingComponent from '@/components/LoadingComponent.vue';
import ErrorAlertComponent from '@/components/Alerts/ErrorAlertComponent.vue';
import SuccessAlertComponent from '@/components/Alerts/SuccessAlertComponent.vue';
@@ -129,16 +136,20 @@ export default {
UserDistanceStatsComponent,
NoItemsFoundComponent,
ActivitySummaryComponent,
ActivityMapComponent,
LoadingComponent,
ErrorAlertComponent,
SuccessAlertComponent,
},
setup() {
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const successAlertStore = useSuccessAlertStore();
const errorAlertStore = useErrorAlertStore();
const selectedActivityView = ref('userActivities');
const isLoading = ref(true);
const activityFound = ref(true);
const userMe = computed(() => userStore.userMe);
const thisWeekDistances = computed(() => userStore.thisWeekDistances);
const thisMonthDistances = computed(() => userStore.thisMonthDistances);
@@ -218,6 +229,13 @@ export default {
};
onMounted(async () => {
if (route.query.activityFound === 'false') {
// Set the activityFound value to false and show the error alert.
activityFound.value = false;
errorAlertStore.setAlertMessage(t("home.errorActivityNotFound"));
errorAlertStore.setClosableState(true);
}
// Add the scroll event listener
window.addEventListener('scroll', handleScroll);
@@ -263,6 +281,7 @@ export default {
successMessage,
submitUploadFileForm,
t,
activityFound,
};
},
};

View File

@@ -0,0 +1,5 @@
<template>
<div>
<h1>User</h1>
</div>
</template>