Added Notifications
This commit is contained in:
@@ -11,6 +11,7 @@ import Distance from "@/Components/Distance.vue";
|
||||
const distanceAchievements = [
|
||||
'general_flying.circumference_of_the_earth',
|
||||
'general_flying.to_the_moon',
|
||||
'general_flying.gigametre'
|
||||
];
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -29,6 +30,10 @@ const progress = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const inProgress = computed(() =>
|
||||
!unlocked.value && (props.userAchievement?.progress ?? 0) > 0
|
||||
)
|
||||
|
||||
const unlocked = computed(() => {
|
||||
if (!props.userAchievement) return false
|
||||
if (props.achievement.progressive) return (progress.value?.percentage ?? 0) >= 100
|
||||
@@ -52,11 +57,11 @@ const difficultyVariant = computed(() => {
|
||||
<template>
|
||||
<v-card
|
||||
class="achievement-card"
|
||||
:class="{ locked: !unlocked }"
|
||||
:class="{ locked: !unlocked && !inProgress }"
|
||||
rounded="lg"
|
||||
elevation="2"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-card-text class="cardLayout">
|
||||
<div class="achievement-inner">
|
||||
<div class="achievement-icon-wrap" :class="{ unlocked }">
|
||||
<v-icon
|
||||
@@ -110,7 +115,7 @@ const difficultyVariant = computed(() => {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
<div class="button-spacer" />
|
||||
<ButtonLink
|
||||
variant="outlined"
|
||||
label="View Details"
|
||||
@@ -122,15 +127,27 @@ const difficultyVariant = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cardLayout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.achievement-card {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.button-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.achievement-card.locked .achievement-inner {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
|
||||
.achievement-card:hover .achievement-inner,
|
||||
.achievement-inner {
|
||||
display: flex;
|
||||
|
||||
@@ -33,7 +33,6 @@ defineProps<{
|
||||
</div>
|
||||
|
||||
<div class="pass-centre">
|
||||
<div class="pass-plane-icon">✈</div>
|
||||
<AirlineLogo :airline="flight.airline" size="44" class="pass-logo" />
|
||||
<div class="pass-flight-number">{{ flight.flight_number }}</div>
|
||||
<div class="pass-airline-name">{{ flight.airline?.name }}</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function directedKey(dep: string, arr: string): string {
|
||||
}
|
||||
|
||||
export function labelFor(a: string, b: string): string {
|
||||
return `${a} ↔ ${b}`
|
||||
return `${a}↔${b}`
|
||||
}
|
||||
|
||||
export function continentNameOf(flight: Flight, side: 'departure' | 'arrival'): string | null {
|
||||
|
||||
@@ -86,7 +86,7 @@ async function lookupFlight() {
|
||||
}
|
||||
if (data.aircraft_options?.length) {
|
||||
aircraftOptionsData.value = data.aircraft_options
|
||||
if (data.aircraft_options.length === 1 && !form.aircraft) form.aircraft = data.aircraft_options[0]
|
||||
if (!form.aircraft) form.aircraft = data.aircraft_options[0]
|
||||
}
|
||||
|
||||
lookupKey.value++
|
||||
@@ -143,6 +143,18 @@ const submitForm = useForm({
|
||||
auto_update: false,
|
||||
})
|
||||
|
||||
|
||||
const departureIsFuture = computed(() => {
|
||||
if (!form.departure_date) return false
|
||||
return new Date(form.departure_date) > new Date()
|
||||
})
|
||||
|
||||
|
||||
watch(departureIsFuture, (isFuture) => {
|
||||
form.auto_update = isFuture
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
function submit() {
|
||||
submitForm.flight_number = flightNumber.value
|
||||
submitForm.departure_date = form.departure_date
|
||||
@@ -411,11 +423,11 @@ const arrivalMax = computed(() => {
|
||||
</v-row>
|
||||
|
||||
<!-- ── Auto update ────────────────────────────────────────── -->
|
||||
<v-row>
|
||||
<v-row v-if="departureIsFuture">
|
||||
<v-col cols="12">
|
||||
<v-checkbox
|
||||
v-model="form.auto_update"
|
||||
label="Automatically update aircraft details within 24 hours of flight departure."
|
||||
label="Automatically update flight details within 24 hours of flight departure"
|
||||
:disabled="!lookupComplete"
|
||||
hide-details
|
||||
density="compact"
|
||||
|
||||
@@ -27,6 +27,7 @@ const props = defineProps<{
|
||||
airlines: Airline[]
|
||||
continents: Continent[]
|
||||
aircraft_families: Record<string, string[]>
|
||||
achievementCount: number
|
||||
}>()
|
||||
|
||||
|
||||
@@ -66,7 +67,7 @@ const difficultyVariant = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProfileLayout :user="user" :isFollowing="isFollowing" :loading="flightsLoading">
|
||||
<ProfileLayout :achievementCount="achievementCount" :user="user" :isFollowing="isFollowing" :loading="flightsLoading">
|
||||
<Head :title="`${achievement.name}`" />
|
||||
<div class="innerLayout">
|
||||
<ButtonLink variant="flat" icon="mdi-arrow-left" :label="`Back to ${user.name}'s Achievements`" :href="route('profile.achievements', { user: user.name })" />
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue"
|
||||
import { ref, 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";
|
||||
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
|
||||
import PanelHeader from "@/Components/FlightsGoneBy/Panels/PanelHeader.vue";
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
|
||||
@@ -15,21 +17,39 @@ const props = defineProps<{
|
||||
isFollowing: boolean
|
||||
achievements: Record<string, Achievement[]>
|
||||
userAchievements: Record<number, UserAchievement>
|
||||
unlockedCount: number
|
||||
totalAchievements: number
|
||||
unlockedByCategory: Record<string, number>
|
||||
}>()
|
||||
|
||||
const totalAchievements = computed(() =>
|
||||
Object.values(props.achievements).reduce((sum, group) => sum + group.length, 0)
|
||||
const hideImpossible = ref(false)
|
||||
|
||||
const filteredAchievements = computed(() => {
|
||||
if (!hideImpossible.value) return props.achievements
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(props.achievements).map(([category, achievements]) => [
|
||||
category,
|
||||
achievements.filter(a => a.difficulty?.internal_name !== 'impossible')
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const filteredUnlockedByCategory = computed(() =>
|
||||
Object.fromEntries(
|
||||
Object.entries(filteredAchievements.value).map(([category, achievements]) => [
|
||||
category,
|
||||
achievements.filter(a => props.userAchievements[a.id]?.unlocked).length
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
const filteredTotal = computed(() =>
|
||||
Object.values(filteredAchievements.value).reduce((sum, group) => sum + group.length, 0)
|
||||
)
|
||||
|
||||
const filteredUnlockedCount = computed(() =>
|
||||
Object.values(filteredUnlockedByCategory.value).reduce((sum, count) => sum + count, 0)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -45,11 +65,20 @@ const unlockedCount = computed(() =>
|
||||
|
||||
<div class="achievements-page">
|
||||
<div class="achievements-summary">
|
||||
<span class="summary-text">
|
||||
{{ unlockedCount }} / {{ totalAchievements }} achievements unlocked
|
||||
</span>
|
||||
<div class="summary-controls">
|
||||
<span class="summary-text">
|
||||
{{ filteredUnlockedCount }} / {{ filteredTotal }} achievements unlocked
|
||||
</span>
|
||||
<v-checkbox
|
||||
v-model="hideImpossible"
|
||||
label="Hide Impossible Achievements"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="amber"
|
||||
/>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="Math.round((unlockedCount / totalAchievements) * 100)"
|
||||
:model-value="Math.round((filteredUnlockedCount / filteredTotal) * 100)"
|
||||
color="amber"
|
||||
rounded
|
||||
height="6"
|
||||
@@ -57,18 +86,17 @@ const unlockedCount = computed(() =>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(categoryAchievements, categoryName) in achievements"
|
||||
<Panel
|
||||
v-for="(categoryAchievements, categoryName) in filteredAchievements"
|
||||
:key="categoryName"
|
||||
class="achievement-category"
|
||||
>
|
||||
<div class="category-header">
|
||||
<h2 class="category-name">{{ categoryName }}</h2>
|
||||
<PanelHeader class="category-header">{{ categoryName }}
|
||||
<span class="category-count">
|
||||
{{ categoryAchievements.filter(a => userAchievements[a.id]).length }}
|
||||
{{ filteredUnlockedByCategory[categoryName] ?? 0 }}
|
||||
/ {{ categoryAchievements.length }}
|
||||
</span>
|
||||
</div>
|
||||
</PanelHeader>
|
||||
|
||||
<div class="achievement-grid">
|
||||
<AchievementCard
|
||||
@@ -79,7 +107,7 @@ const unlockedCount = computed(() =>
|
||||
:user-achievement="userAchievements[achievement.id]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</ProfileLayout>
|
||||
</template>
|
||||
@@ -101,6 +129,12 @@ const unlockedCount = computed(() =>
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.7;
|
||||
@@ -120,14 +154,6 @@ const unlockedCount = computed(() =>
|
||||
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;
|
||||
|
||||
Vendored
+1
@@ -106,6 +106,7 @@ export interface UserAchievement {
|
||||
id: number
|
||||
user_id: number
|
||||
achievement_id: number
|
||||
unlocked: boolean
|
||||
progress: number | null
|
||||
achievement?: Achievement
|
||||
created_at: string | null
|
||||
|
||||
Reference in New Issue
Block a user