Many more changes for btter experience.
This commit is contained in:
parent
87059a62e2
commit
e2a95252bb
16 changed files with 385 additions and 72 deletions
|
|
@ -3,6 +3,7 @@ import { ref, onMounted, computed } from 'vue';
|
|||
import { RouterLink } from 'vue-router';
|
||||
import type { Game } from '../types';
|
||||
import { gameService } from '../services/api';
|
||||
import { alert, confirm } from '../composables/useModal';
|
||||
|
||||
const games = ref<Game[]>([]);
|
||||
const loading = ref(false);
|
||||
|
|
@ -31,24 +32,24 @@ async function loadGames() {
|
|||
}
|
||||
|
||||
async function archiveGame(gameId: string) {
|
||||
if (!confirm('Are you sure you want to archive this game?')) return;
|
||||
if (!await confirm('Are you sure you want to archive this game?')) return;
|
||||
|
||||
try {
|
||||
await gameService.archive(gameId);
|
||||
await loadGames();
|
||||
} catch (err) {
|
||||
alert('Failed to archive game');
|
||||
await alert('Failed to archive game');
|
||||
}
|
||||
}
|
||||
|
||||
async function unarchiveGame(gameId: string) {
|
||||
if (!confirm('Are you sure you want to unarchive this game?')) return;
|
||||
if (!await confirm('Are you sure you want to unarchive this game?')) return;
|
||||
|
||||
try {
|
||||
await gameService.unarchive(gameId);
|
||||
await loadGames();
|
||||
} catch (err) {
|
||||
alert('Failed to unarchive game');
|
||||
await alert('Failed to unarchive game');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import L from 'leaflet';
|
|||
import 'leaflet/dist/leaflet.css';
|
||||
import type { Game, Route } from '../types';
|
||||
import { gameService, routeService } from '../services/api';
|
||||
import { alert, confirm } from '../composables/useModal';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { formatDistance } from '../utils/units';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const game = ref<Game | null>(null);
|
||||
const routes = ref<Route[]>([]);
|
||||
|
|
@ -20,6 +24,8 @@ const selectedRoute = computed(() =>
|
|||
routes.value.find(r => r.id === selectedRouteId.value) || null
|
||||
);
|
||||
|
||||
const unitPreference = computed(() => authStore.user?.unitPreference || 'METRIC');
|
||||
|
||||
const mapContainer = ref<HTMLDivElement | null>(null);
|
||||
let map: L.Map | null = null;
|
||||
let routeMarkers: { [routeId: string]: L.Marker[] } = {};
|
||||
|
|
@ -76,7 +82,7 @@ async function saveRoute() {
|
|||
|
||||
updateMapMarkers();
|
||||
} catch (err) {
|
||||
alert('Failed to update route');
|
||||
await alert('Failed to update route');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
|
@ -162,7 +168,7 @@ function updateMapMarkers() {
|
|||
|
||||
async function createRoute() {
|
||||
if (!newRoute.value.name.trim()) {
|
||||
alert('Please enter a route name');
|
||||
await alert('Please enter a route name');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +187,7 @@ async function createRoute() {
|
|||
newRoute.value = { name: '', description: '', color: '#3498db' };
|
||||
updateMapMarkers();
|
||||
} catch (err) {
|
||||
alert('Failed to create route');
|
||||
await alert('Failed to create route');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
|
@ -194,12 +200,12 @@ async function copyRoute(routeId: string) {
|
|||
selectedRouteId.value = response.data.id;
|
||||
updateMapMarkers();
|
||||
} catch (err) {
|
||||
alert('Failed to copy route');
|
||||
await alert('Failed to copy route');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRoute(routeId: string) {
|
||||
if (!confirm('Are you sure you want to delete this route and all its legs?')) return;
|
||||
if (!await confirm('Are you sure you want to delete this route and all its legs?')) return;
|
||||
|
||||
try {
|
||||
await routeService.delete(routeId);
|
||||
|
|
@ -211,13 +217,13 @@ async function deleteRoute(routeId: string) {
|
|||
|
||||
updateMapMarkers();
|
||||
} catch (err) {
|
||||
alert('Failed to delete route');
|
||||
await alert('Failed to delete route');
|
||||
}
|
||||
}
|
||||
|
||||
async function addLeg() {
|
||||
if (!selectedRouteId.value || !newLeg.value.description) {
|
||||
alert('Please enter a description');
|
||||
await alert('Please enter a description');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -249,14 +255,14 @@ async function addLeg() {
|
|||
|
||||
updateMapMarkers();
|
||||
} catch (err) {
|
||||
alert('Failed to add leg');
|
||||
await alert('Failed to add leg');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLeg(legId: string) {
|
||||
if (!confirm('Are you sure you want to delete this leg?')) return;
|
||||
if (!await confirm('Are you sure you want to delete this leg?')) return;
|
||||
|
||||
try {
|
||||
await routeService.deleteLeg(selectedRouteId.value!, legId);
|
||||
|
|
@ -268,7 +274,7 @@ async function deleteLeg(legId: string) {
|
|||
|
||||
updateMapMarkers();
|
||||
} catch (err) {
|
||||
alert('Failed to delete leg');
|
||||
await alert('Failed to delete leg');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +380,7 @@ onUnmounted(() => {
|
|||
<footer>
|
||||
<small>
|
||||
{{ routeItem.routeLegs?.length || 0 }} legs
|
||||
· {{ getRouteDistance(routeItem).toFixed(2) }} km
|
||||
· {{ formatDistance(getRouteDistance(routeItem), unitPreference) }}
|
||||
</small>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'leaflet/dist/leaflet.css';
|
|||
import { useAuthStore } from '../stores/auth';
|
||||
import type { Game, Team, ChatMessage, Route } from '../types';
|
||||
import { gameService, teamService, routeService } from '../services/api';
|
||||
import { alert, confirm } from '../composables/useModal';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
|
|
@ -109,10 +110,22 @@ function connectSocket() {
|
|||
team.lat = data.lat;
|
||||
team.lng = data.lng;
|
||||
|
||||
const teamRoute = team.teamRoutes?.[0];
|
||||
const teamRouteData = routes.value.find(r => r.id === teamRoute?.routeId);
|
||||
const color = teamRouteData?.color || '#666';
|
||||
|
||||
if (teamMarkers[data.teamId]) {
|
||||
teamMarkers[data.teamId].setLatLng([data.lat, data.lng]);
|
||||
} else if (map) {
|
||||
const marker = L.marker([data.lat, data.lng])
|
||||
teamMarkers[data.teamId].remove();
|
||||
}
|
||||
|
||||
if (map) {
|
||||
const teamIcon = L.divIcon({
|
||||
className: 'team-marker',
|
||||
html: `<div style="background:${color}; width:24px; height:24px; border-radius:50%; border:2px solid white; box-shadow:0 2px 4px rgba(0,0,0,0.3);"></div>`,
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12]
|
||||
});
|
||||
const marker = L.marker([data.lat, data.lng], { icon: teamIcon })
|
||||
.addTo(map)
|
||||
.bindPopup(team.name);
|
||||
teamMarkers[data.teamId] = marker;
|
||||
|
|
@ -148,30 +161,30 @@ async function advanceTeam(teamId: string) {
|
|||
await loadGame();
|
||||
socket?.emit('team-advanced', { gameId: gameId.value, teamId });
|
||||
} catch (err) {
|
||||
alert('Failed to advance team');
|
||||
await alert('Failed to advance team');
|
||||
}
|
||||
}
|
||||
|
||||
async function deductTime(teamId: string) {
|
||||
const seconds = prompt('Enter deduction in seconds:', '60');
|
||||
if (!seconds) return;
|
||||
const secondsStr = window.prompt('Enter deduction in seconds:', '60');
|
||||
if (!secondsStr) return;
|
||||
|
||||
try {
|
||||
await teamService.deduct(teamId, parseInt(seconds));
|
||||
await teamService.deduct(teamId, parseInt(secondsStr));
|
||||
await loadGame();
|
||||
} catch (err) {
|
||||
alert('Failed to deduct time');
|
||||
await alert('Failed to deduct time');
|
||||
}
|
||||
}
|
||||
|
||||
async function disqualifyTeam(teamId: string) {
|
||||
if (!confirm('Are you sure you want to disqualify this team?')) return;
|
||||
if (!await confirm('Are you sure you want to disqualify this team?')) return;
|
||||
|
||||
try {
|
||||
await teamService.disqualify(teamId);
|
||||
await loadGame();
|
||||
} catch (err) {
|
||||
alert('Failed to disqualify team');
|
||||
await alert('Failed to disqualify team');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { useRoute, RouterLink } from 'vue-router';
|
|||
import { useAuthStore } from '../stores/auth';
|
||||
import type { Game } from '../types';
|
||||
import { gameService } from '../services/api';
|
||||
import { alert, confirm } from '../composables/useModal';
|
||||
import { formatRadius } from '../utils/units';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
|
|
@ -14,6 +16,7 @@ const error = ref('');
|
|||
|
||||
const gameId = computed(() => route.params.id as string);
|
||||
const isGameMaster = computed(() => game.value?.gameMasterId === authStore.user?.id);
|
||||
const unitPreference = computed(() => authStore.user?.unitPreference || 'METRIC');
|
||||
|
||||
async function loadGame() {
|
||||
loading.value = true;
|
||||
|
|
@ -29,46 +32,46 @@ async function loadGame() {
|
|||
}
|
||||
|
||||
async function publishGame() {
|
||||
if (!confirm('Are you sure you want to publish this game?')) return;
|
||||
if (!await confirm('Are you sure you want to publish this game?')) return;
|
||||
|
||||
try {
|
||||
await gameService.publish(gameId.value);
|
||||
await loadGame();
|
||||
} catch (err) {
|
||||
alert('Failed to publish game');
|
||||
await alert('Failed to publish game');
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveGame() {
|
||||
if (!confirm('Are you sure you want to archive this game?')) return;
|
||||
if (!await confirm('Are you sure you want to archive this game?')) return;
|
||||
|
||||
try {
|
||||
await gameService.archive(gameId.value);
|
||||
await loadGame();
|
||||
} catch (err) {
|
||||
alert('Failed to archive game');
|
||||
await alert('Failed to archive game');
|
||||
}
|
||||
}
|
||||
|
||||
async function unarchiveGame() {
|
||||
if (!confirm('Are you sure you want to unarchive this game?')) return;
|
||||
if (!await confirm('Are you sure you want to unarchive this game?')) return;
|
||||
|
||||
try {
|
||||
await gameService.unarchive(gameId.value);
|
||||
await loadGame();
|
||||
} catch (err) {
|
||||
alert('Failed to unarchive game');
|
||||
await alert('Failed to unarchive game');
|
||||
}
|
||||
}
|
||||
|
||||
async function endGame() {
|
||||
if (!confirm('Are you sure you want to end this game?')) return;
|
||||
if (!await confirm('Are you sure you want to end this game?')) return;
|
||||
|
||||
try {
|
||||
await gameService.end(gameId.value);
|
||||
await loadGame();
|
||||
} catch (err) {
|
||||
alert('Failed to end game');
|
||||
await alert('Failed to end game');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,9 +80,9 @@ async function copyInviteLink() {
|
|||
const response = await gameService.getInvite(gameId.value);
|
||||
const link = `${window.location.origin}/invite/${response.data.inviteCode}`;
|
||||
await navigator.clipboard.writeText(link);
|
||||
alert('Invite link copied to clipboard!');
|
||||
await alert('Invite link copied to clipboard!');
|
||||
} catch (err) {
|
||||
alert('Failed to get invite link');
|
||||
await alert('Failed to get invite link');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +147,7 @@ onMounted(() => {
|
|||
<dd v-else>Not set</dd>
|
||||
|
||||
<dt>Search Radius</dt>
|
||||
<dd>{{ game.searchRadius || 500 }} meters</dd>
|
||||
<dd>{{ formatRadius(game.searchRadius || 500, unitPreference) }}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
import { useAuthStore } from '../stores/auth';
|
||||
import type { Game, Team } from '../types';
|
||||
import { gameService, teamService } from '../services/api';
|
||||
import { alert } from '../composables/useModal';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
|
@ -44,7 +45,7 @@ async function loadGame() {
|
|||
|
||||
async function createTeam() {
|
||||
if (!newTeamName.value.trim()) {
|
||||
alert('Please enter a team name');
|
||||
await alert('Please enter a team name');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ async function createTeam() {
|
|||
await loadGame();
|
||||
router.push(`/games/${gameId.value}/play`);
|
||||
} catch (err) {
|
||||
alert('Failed to create team');
|
||||
await alert('Failed to create team');
|
||||
} finally {
|
||||
creating.value = false;
|
||||
}
|
||||
|
|
@ -68,7 +69,7 @@ async function joinTeam(teamId: string) {
|
|||
await loadGame();
|
||||
router.push(`/games/${gameId.value}/play`);
|
||||
} catch (err) {
|
||||
alert('Failed to join team');
|
||||
await alert('Failed to join team');
|
||||
} finally {
|
||||
joining.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'leaflet/dist/leaflet.css';
|
|||
import { useAuthStore } from '../stores/auth';
|
||||
import type { Game, Team, ChatMessage, Route, RouteLeg } from '../types';
|
||||
import { teamService, uploadService } from '../services/api';
|
||||
import { alert } from '../composables/useModal';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
|
|
@ -164,7 +165,7 @@ function handlePhotoSelect(event: Event) {
|
|||
|
||||
async function submitPhoto() {
|
||||
if (!photoFile.value || !currentLeg.value || !team.value) {
|
||||
alert('Please select a photo');
|
||||
await alert('Please select a photo');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -183,11 +184,11 @@ async function submitPhoto() {
|
|||
})
|
||||
});
|
||||
|
||||
alert('Photo submitted! Wait for Game Master approval.');
|
||||
await alert('Photo submitted! Wait for Game Master approval.');
|
||||
showPhotoUpload.value = false;
|
||||
photoFile.value = null;
|
||||
} catch (err) {
|
||||
alert('Failed to submit photo');
|
||||
await alert('Failed to submit photo');
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { userService, uploadService } from '../services/api';
|
||||
import type { User, UserGameHistory, LocationHistory } from '../types';
|
||||
import { alert, confirm } from '../composables/useModal';
|
||||
import { formatDistance } from '../utils/units';
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
|
@ -22,6 +24,10 @@ const uploadingAvatar = ref(false);
|
|||
const locationHistory = ref<{ totalLocations: number; byGame: { game: { id: string; name: string }; locations: LocationHistory[]; locationCount: number }[] }>({ totalLocations: 0, byGame: [] });
|
||||
const gamesHistory = ref<UserGameHistory[]>([]);
|
||||
|
||||
const unitPreference = ref<'METRIC' | 'IMPERIAL'>('METRIC');
|
||||
|
||||
const displayUnitPreference = computed(() => unitPreference.value);
|
||||
|
||||
async function loadProfile() {
|
||||
loading.value = true;
|
||||
try {
|
||||
|
|
@ -31,6 +37,7 @@ async function loadProfile() {
|
|||
screenName.value = response.data.screenName || '';
|
||||
avatarUrl.value = response.data.avatarUrl || '';
|
||||
avatarPreview.value = response.data.avatarUrl || '';
|
||||
unitPreference.value = response.data.unitPreference || 'METRIC';
|
||||
} catch (err) {
|
||||
console.error('Failed to load profile:', err);
|
||||
} finally {
|
||||
|
|
@ -65,7 +72,7 @@ async function handleAvatarSelect(event: Event) {
|
|||
avatarUrl.value = uploadRes.data.url;
|
||||
avatarPreview.value = uploadRes.data.url;
|
||||
} catch (err) {
|
||||
alert('Failed to upload avatar');
|
||||
await alert('Failed to upload avatar');
|
||||
} finally {
|
||||
uploadingAvatar.value = false;
|
||||
}
|
||||
|
|
@ -78,40 +85,41 @@ async function saveProfile() {
|
|||
const response = await userService.updateProfile({
|
||||
name: name.value,
|
||||
screenName: screenName.value || undefined,
|
||||
avatarUrl: avatarUrl.value || undefined
|
||||
avatarUrl: avatarUrl.value || undefined,
|
||||
unitPreference: unitPreference.value
|
||||
});
|
||||
user.value = response.data;
|
||||
authStore.user = response.data;
|
||||
alert('Profile updated successfully!');
|
||||
await alert('Profile updated successfully!');
|
||||
} catch (err) {
|
||||
alert('Failed to update profile');
|
||||
await alert('Failed to update profile');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLocationData() {
|
||||
if (!confirm('Are you sure you want to delete all your location data? This cannot be undone.')) return;
|
||||
if (!await confirm('Are you sure you want to delete all your location data? This cannot be undone.')) return;
|
||||
|
||||
try {
|
||||
await userService.deleteLocationData();
|
||||
alert('Location data deleted.');
|
||||
await alert('Location data deleted.');
|
||||
loadLocationHistory();
|
||||
} catch (err) {
|
||||
alert('Failed to delete location data');
|
||||
await alert('Failed to delete location data');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
if (!confirm('Are you sure you want to delete your account? This will remove all your data and cannot be undone.')) return;
|
||||
if (!confirm('This is your final warning. Type "DELETE" to confirm.')) return;
|
||||
if (!await confirm('Are you sure you want to delete your account? This will remove all your data and cannot be undone.')) return;
|
||||
if (!await confirm('This is your final warning. This action cannot be undone.')) return;
|
||||
|
||||
try {
|
||||
await userService.deleteAccount();
|
||||
authStore.logout();
|
||||
router.push('/');
|
||||
} catch (err) {
|
||||
alert('Failed to delete account');
|
||||
await alert('Failed to delete account');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +186,14 @@ onMounted(() => {
|
|||
<input v-model="screenName" type="text" placeholder="Optional alias" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Distance Units
|
||||
<select v-model="unitPreference">
|
||||
<option value="METRIC">Metric (km, m)</option>
|
||||
<option value="IMPERIAL">Imperial (miles, feet)</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button @click="saveProfile" :disabled="saving">
|
||||
{{ saving ? 'Saving...' : 'Save Profile' }}
|
||||
</button>
|
||||
|
|
@ -274,7 +290,7 @@ onMounted(() => {
|
|||
<small>Legs</small>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<strong>{{ game.totalDistance }} km</strong>
|
||||
<strong>{{ formatDistance(game.totalDistance, displayUnitPreference) }}</strong>
|
||||
<small>Distance</small>
|
||||
</div>
|
||||
<div class="stat">
|
||||
|
|
|
|||
|
|
@ -114,10 +114,22 @@ function connectSocket() {
|
|||
team.lat = data.lat;
|
||||
team.lng = data.lng;
|
||||
|
||||
const teamRoute = team.teamRoutes?.[0];
|
||||
const teamRouteData = routes.value.find(r => r.id === teamRoute?.routeId);
|
||||
const color = teamRouteData?.color || '#666';
|
||||
|
||||
if (teamMarkers[data.teamId]) {
|
||||
teamMarkers[data.teamId].setLatLng([data.lat, data.lng]);
|
||||
} else if (map) {
|
||||
const marker = L.marker([data.lat, data.lng])
|
||||
teamMarkers[data.teamId].remove();
|
||||
}
|
||||
|
||||
if (map) {
|
||||
const teamIcon = L.divIcon({
|
||||
className: 'team-marker',
|
||||
html: `<div style="background:${color}; width:16px; height:16px; border-radius:50%; border:2px solid white; box-shadow:0 2px 4px rgba(0,0,0,0.3);"></div>`,
|
||||
iconSize: [16, 16],
|
||||
iconAnchor: [8, 8]
|
||||
});
|
||||
const marker = L.marker([data.lat, data.lng], { icon: teamIcon })
|
||||
.addTo(map)
|
||||
.bindPopup(team.name);
|
||||
teamMarkers[data.teamId] = marker;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue