Added achievement data
This commit is contained in:
@@ -26,7 +26,7 @@ const props = defineProps<{
|
||||
dep_time: string
|
||||
arr_time: string
|
||||
duration: string
|
||||
airline_options: { value: number, title: string }[]
|
||||
airline_options: { value: number, title: string, logo_url: string }[]
|
||||
from_options: { value: number, title: string, country_code: string}[]
|
||||
to_options: { value: number, title: string, country_code: string }[]
|
||||
aircraft_options: { value: number, title: string }[]
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue"
|
||||
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue"
|
||||
import ProfileViewSwitcher from "@/Components/FlightsGoneBy/ProfileViewSwitcher.vue"
|
||||
import AchievementCard from "@/Components/FlightsGoneBy/AchievementCard.vue"
|
||||
import {Achievement, User, UserAchievement} from "@/Types/types"
|
||||
import { Head } from "@inertiajs/vue3";
|
||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
|
||||
const props = defineProps<{
|
||||
user: User
|
||||
canEdit: boolean
|
||||
isFollowing: boolean
|
||||
achievements: Record<string, Achievement[]>
|
||||
userAchievements: Record<number, UserAchievement>
|
||||
}>()
|
||||
|
||||
const totalAchievements = computed(() =>
|
||||
Object.values(props.achievements).reduce((sum, group) => sum + group.length, 0)
|
||||
)
|
||||
|
||||
const unlockedCount = computed(() =>
|
||||
Object.values(props.achievements)
|
||||
.flat()
|
||||
.filter(a => {
|
||||
const ua = props.userAchievements[a.id]
|
||||
if (!ua) return false
|
||||
if (!a.progressive || !a.threshold) return true
|
||||
return (ua.progress ?? 0) >= a.threshold
|
||||
}).length
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="`${user.name}'s Achievements`" />
|
||||
<ProfileLayout
|
||||
:user="user"
|
||||
:achievementCount="unlockedCount"
|
||||
:is-following="isFollowing"
|
||||
:loading="false"
|
||||
>
|
||||
<ProfileViewSwitcher active-view="achievements" :user="user" />
|
||||
|
||||
<div class="achievements-page">
|
||||
<div class="achievements-summary">
|
||||
<span class="summary-text">
|
||||
{{ unlockedCount }} / {{ totalAchievements }} achievements unlocked
|
||||
</span>
|
||||
<v-progress-linear
|
||||
:model-value="Math.round((unlockedCount / totalAchievements) * 100)"
|
||||
color="amber"
|
||||
rounded
|
||||
height="6"
|
||||
bg-color="rgba(255,255,255,0.1)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(categoryAchievements, categoryName) in achievements"
|
||||
:key="categoryName"
|
||||
class="achievement-category"
|
||||
>
|
||||
<div class="category-header">
|
||||
<h2 class="category-name">{{ categoryName }}</h2>
|
||||
<span class="category-count">
|
||||
{{ categoryAchievements.filter(a => userAchievements[a.id]).length }}
|
||||
/ {{ categoryAchievements.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="achievement-grid">
|
||||
<AchievementCard
|
||||
v-for="achievement in categoryAchievements"
|
||||
:key="achievement.id"
|
||||
:achievement="achievement"
|
||||
:user-achievement="userAchievements[achievement.id]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProfileLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.achievements-page {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.achievements-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.achievement-category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.achievement-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.achievement-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||
import { Head } from '@inertiajs/vue3'
|
||||
import {ref, computed, watchEffect} from 'vue'
|
||||
import { Flight, ProfileView, User } from "@/Types/types"
|
||||
import { router } from '@inertiajs/vue3'
|
||||
import { useFlightStats } from "@/Composables/useFlightStats"
|
||||
import {Head, router} from '@inertiajs/vue3'
|
||||
import {computed, onMounted, ref, watchEffect} from 'vue'
|
||||
import axios from 'axios'
|
||||
import {Flight, ProfileView, User} from "@/Types/types"
|
||||
import {useFlightStats} from "@/Composables/useFlightStats"
|
||||
import ProfileViewSwitcher from "@/Components/FlightsGoneBy/ProfileViewSwitcher.vue"
|
||||
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue"
|
||||
import DepartureBoard from "@/Components/FlightsGoneBy/DepartureBoard.vue"
|
||||
@@ -15,13 +15,27 @@ defineOptions({ layout: MainLayout })
|
||||
|
||||
const props = defineProps<{
|
||||
user: User
|
||||
flights: Flight[]
|
||||
canEdit: boolean
|
||||
selectedFlightId?: number | null
|
||||
initialView?: ProfileView
|
||||
isFollowing: boolean
|
||||
flight_api_url: string
|
||||
}>()
|
||||
|
||||
// ── Flights state ─────────────────────────────────────────────────────────────
|
||||
const flights = ref<Flight[]>([])
|
||||
const flightsLoading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await axios.get(props.flight_api_url)
|
||||
|
||||
flights.value = response.data
|
||||
} finally {
|
||||
flightsLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const localSelectedFlightId = ref(props.selectedFlightId ?? null)
|
||||
|
||||
// ── Filter state ──────────────────────────────────────────────────────────────
|
||||
@@ -70,8 +84,7 @@ function matchesFilters(f: Flight): boolean {
|
||||
}
|
||||
|
||||
const filteredFlights = computed(() => {
|
||||
const result = props.flights.filter(matchesFilters)
|
||||
return result
|
||||
return flights.value.filter(matchesFilters)
|
||||
})
|
||||
|
||||
const stats = useFlightStats(filteredFlights)
|
||||
@@ -92,13 +105,18 @@ watchEffect(() => {
|
||||
// ── View switching ────────────────────────────────────────────────────────────
|
||||
const activeView = ref<ProfileView>(props.initialView ?? 'board')
|
||||
|
||||
const routeNames = {
|
||||
const routeNames: Record<Exclude<ProfileView, 'achievements'>, string> = {
|
||||
map: 'profile.map',
|
||||
board: 'profile.departure-board',
|
||||
passes: 'profile.boarding-passes',
|
||||
} as const
|
||||
|
||||
function switchView(view: ProfileView) {
|
||||
if (view === 'achievements') {
|
||||
router.visit(route('profile.achievements', { user: props.user.name }))
|
||||
return
|
||||
}
|
||||
|
||||
const flightId = view === 'board' ? localSelectedFlightId.value : null
|
||||
localSelectedFlightId.value = null
|
||||
activeView.value = view
|
||||
@@ -115,8 +133,8 @@ function switchView(view: ProfileView) {
|
||||
|
||||
<template>
|
||||
<Head :title="`${user.name}'s Flights`" />
|
||||
<ProfileLayout :is-following="isFollowing" :flights="flights" :user="user">
|
||||
<ProfileViewSwitcher :active-view="activeView" @update:active-view="switchView" />
|
||||
<ProfileLayout :is-following="isFollowing" :flightCount="flights.length" :user="user" :loading="flightsLoading">
|
||||
<ProfileViewSwitcher :user="user" :active-view="activeView" @update:active-view="switchView" />
|
||||
<DepartureBoard v-if="activeView === 'board'" :flight-id="localSelectedFlightId" :flight-stats="stats" :canEdit="canEdit" />
|
||||
<BoardingPasses v-if="activeView === 'passes'" :flight-stats="stats" :canEdit="canEdit" />
|
||||
<FlightMapAndCharts
|
||||
|
||||
Reference in New Issue
Block a user