mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-10 08:17:59 -05:00
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:
102
frontend_vue/src/components/Activities/ActivityMapComponent.vue
Normal file
102
frontend_vue/src/components/Activities/ActivityMapComponent.vue
Normal 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>
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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">© {{ 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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
4
frontend_vue/src/i18n/en/activityView.json
Normal file
4
frontend_vue/src/i18n/en/activityView.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"labelGear": "Gear",
|
||||
"labelGearNotSet": "Not set"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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`);
|
||||
},
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
143
frontend_vue/src/views/ActivityView.vue
Normal file
143
frontend_vue/src/views/ActivityView.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
5
frontend_vue/src/views/UserView.vue
Normal file
5
frontend_vue/src/views/UserView.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>User</h1>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user