Added Notifications

This commit is contained in:
2026-05-18 14:31:53 +10:00
parent 1d5b9f340f
commit 10b5b6a5c9
18 changed files with 545 additions and 166 deletions
@@ -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 {
+15 -3
View File
@@ -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 })" />
+57 -31
View File
@@ -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;
+1
View File
@@ -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