Session api token validation

This commit is contained in:
2026-06-23 11:20:01 +10:00
parent cd1538ba12
commit fcba639bae
25 changed files with 491 additions and 235 deletions
@@ -0,0 +1,33 @@
<script setup lang="ts">
import {User} from "@/Types/types";
defineProps<{
user: User
}>()
import {Link} from "@inertiajs/vue3";
</script>
<template>
<div class="avatar">
<Link :href="route('profile.view', { user: user?.name })">
{{ user?.name?.charAt(0).toUpperCase() ?? '?' }}
</Link>
</div>
</template>
<style scoped>
.avatar {
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
background: rgba(99, 102, 241, 0.2);
border: 1px solid rgba(99, 102, 241, 0.35);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
color: #818cf8;
flex-shrink: 0;
}
</style>
@@ -14,6 +14,7 @@ import FlightCancelledFeedItem from "@/Components/FlightsGoneBy/Feed/FlightCance
import FlightImportedFeedItem from "@/Components/FlightsGoneBy/Feed/FlightImportedFeedItem.vue";
import FlightMovedFeedItem from "@/Components/FlightsGoneBy/Feed/FlightMovedFeedItem.vue";
import ButtonLink from "@/Components/FlightsGoneBy/ButtonLink.vue";
import Avatar from "@/Components/FlightsGoneBy/Feed/Avatar.vue";
const props = defineProps<{
action: UserAction
@@ -56,11 +57,7 @@ function timeAgo(dateStr: string): string {
<template>
<div class="feed-item glass glass-border">
<div class="card-top">
<div class="avatar">
<Link :href="route('profile.view', { user: action.user?.name })">
{{ action.user?.name?.charAt(0).toUpperCase() ?? '?' }}
</Link>
</div>
<Avatar :user="action.user" />
<div class="meta">
<span class="name">
<Link :href="route('profile.view', { user: action.user?.name })">
@@ -110,21 +107,6 @@ function timeAgo(dateStr: string): string {
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.avatar {
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
background: rgba(99, 102, 241, 0.2);
border: 1px solid rgba(99, 102, 241, 0.35);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
color: #818cf8;
flex-shrink: 0;
}
.meta {
display: flex;
flex-direction: column;
@@ -171,7 +171,11 @@ function airportPopupHTML(airport: Airport): string {
function routePopupHTML(historical: Flight[], future: Flight[]): string {
interface AirlineEntry { html: string; count: number }
interface DirectionGroup { label: string; airlines: Map<string, AirlineEntry> }
interface DirectionGroup {
label: string
airlines: Map<string, AirlineEntry>
users: Map<number, { name: string }>
}
const groupByDirection = (flights: Flight[]): DirectionGroup[] => {
const groups = new Map<string, DirectionGroup>()
@@ -180,13 +184,19 @@ function routePopupHTML(historical: Flight[], future: Flight[]): string {
const key = `${flight.departure_airport.id}-${flight.arrival_airport.id}`
const label = `${flight.departure_airport.municipality} to ${flight.arrival_airport.municipality}`
if (!groups.has(key)) groups.set(key, { label, airlines: new Map() })
if (!groups.has(key)) groups.set(key, { label, airlines: new Map(), users: new Map() })
const group = groups.get(key)!
if (flight.user) {
group.users.set(flight.user.id, { name: flight.user.name })
}
if (!flight.airline) return
const { iata_code, logo_url, name } = flight.airline
const airlineKey = iata_code ?? ''
const airlines = groups.get(key)!.airlines
const airlines = group.airlines
if (airlines.has(airlineKey)) {
airlines.get(airlineKey)!.count++
@@ -204,10 +214,20 @@ function routePopupHTML(historical: Flight[], future: Flight[]): string {
return [...groups.values()]
}
const renderUsers = (users: Map<number, { name: string }>): string => {
if (!users.size) return ''
const badges = [...users.values()]
.map(({ name }) => `<span class="rp-user-badge">${name}</span>`)
.join('')
return `<div class="rp-users">${badges}</div>`
}
const renderSection = (title: string, flights: Flight[]): string => {
if (!flights.length) return ''
const rows = groupByDirection(flights).map(({ label, airlines }) => {
const rows = groupByDirection(flights).map(({ label, airlines, users }) => {
const airlineLines = [...airlines.values()]
.map(({ html, count }) => count > 1
? `<span style="display:inline-flex;align-items:center;gap:4px;">${html}<span style="color:#556677">(x${count})</span></span>`
@@ -218,6 +238,7 @@ function routePopupHTML(historical: Flight[], future: Flight[]): string {
<div class="rp-direction">
<div class="rp-route">${label}</div>
<div class="rp-airlines">${airlineLines || '—'}</div>
${renderUsers(users)}
</div>`
}).join('')
@@ -778,13 +799,17 @@ export default defineComponent({
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
max-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.flight-map-wrapper { aspect-ratio: 7 / 10; }
.flight-map-wrapper {
aspect-ratio: 7 / 10;
max-height: none;
}
}
.map-container { position: absolute; inset: 0; }
@@ -860,6 +885,22 @@ export default defineComponent({
.rp-airlines { font-size: 0.75rem; color: #778899; }
.rp-divider { height: 1px; background: rgba(255,255,255,0.08); margin: 2px 0; }
.rp-users {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.rp-user-badge {
font-size: 0.7rem;
color: #c8cdd8;
background: rgba(77,166,255,0.12);
border: 1px solid rgba(77,166,255,0.25);
border-radius: 999px;
padding: 1px 8px;
white-space: nowrap;
}
/* Map controls */
.maplibregl-ctrl-group { background: rgba(10,14,22,0.85) !important; border: 1px solid rgba(255,255,255,0.08) !important; }
.maplibregl-ctrl-group button { color: #a0b4c8 !important; }
@@ -7,13 +7,13 @@ type FollowStatus = 'following' | 'requested' | 'none'
const props = defineProps<{
user: User
followStatus: FollowStatus
followStatus: string
}>()
const snackbar = ref(false)
const snackbarMessage = ref('')
const status = ref<FollowStatus>(props.followStatus)
const status = ref<FollowStatus>(props.followStatus as FollowStatus)
const processing = ref(false)
const auth = usePage<SharedProps>().props.auth
@@ -11,6 +11,7 @@ const props = defineProps<{
achievementCount?: number
followStatus?: string
show: "flights" | "achievements"
showCount?: boolean
}>()
const counts = computed(() => {
@@ -39,7 +40,7 @@ const counts = computed(() => {
</div>
</div>
<div class="board-count">
<div class="board-count" v-if="showCount">
<span class="count-number">{{ counts[show] }}</span>
<span class="count-label">{{show.toUpperCase()}}</span>
</div>
@@ -13,13 +13,14 @@ defineProps<{
loading: boolean
canView: boolean
title?: string
showCount?: boolean
}>()
</script>
<template>
<div class="board-wrapper">
<Head :title="title" />
<ProfileHeader :show="achievementCount && achievementCount > 0 ? 'achievements' : 'flights'" :followStatus="followStatus" :user="user" :flightCount="flightCount" :achievementCount="achievementCount" />
<ProfileHeader :showCount="showCount" :show="achievementCount && achievementCount > 0 ? 'achievements' : 'flights'" :followStatus="followStatus" :user="user" :flightCount="flightCount" :achievementCount="achievementCount" />
<div v-if="loading" class="loading-state">
<PlaneLoader />
</div>
+175 -19
View File
@@ -1,8 +1,14 @@
<script setup lang="ts">
import MainLayout from "@/Layouts/MainLayout.vue";
import {User, UserAction} from "@/Types/types";
import { Head } from "@inertiajs/vue3";
import { User, UserAction } from "@/Types/types";
import {Head, Link} from "@inertiajs/vue3";
import FeedItem from "@/Components/FlightsGoneBy/Feed/FeedItem.vue";
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
import FlightMap from "@/Components/FlightsGoneBy/FlightMap.vue";
import PlaneLoader from "@/Components/FlightsGoneBy/PlaneLoader.vue";
import { useFlights } from "@/Composables/useFlights";
import { useApiResource } from "@/Composables/useApiResource";
import Avatar from "@/Components/FlightsGoneBy/Feed/Avatar.vue";
defineOptions({ layout: MainLayout })
@@ -10,32 +16,78 @@ const props = defineProps<{
user: User
feed: UserAction[]
}>()
// Flights belonging to people the user follows — adjust endpoint to match your API
const { flights: followingFlights, flightsLoading } = useFlights('/internal/feed/following-flights')
// People the user follows — adjust shape/endpoint to match your API
const { data: following, loading: followingLoading } = useApiResource<User[]>('/internal/feed/following')
</script>
<template>
<Head title="Feed" />
<ProfileLayout :showCount="false" :user="user" followStatus="none" :canView="true" :loading="false">
<Head title="Feed" />
<div class="feed-page">
<header class="feed-header">
<h1>Feed</h1>
<span class="feed-count">{{ feed.length }} updates</span>
</header>
<div class="feed-page">
<header class="feed-header">
<h1>Feed</h1>
<span class="feed-count">{{ feed.length }} updates</span>
</header>
<div v-if="feed.length === 0" class="empty">
<p>Nothing here yet follow someone to see their flight updates!</p>
<!-- MAP: the headline content -->
<section class="map-section">
<div class="map-caption">
<span class="caption-icon">🛫</span>
Recently logged by people you follow
</div>
<FlightMap v-if="!flightsLoading" :flights="followingFlights" :show-legend="false" />
<div v-else class="loading-state">
<PlaneLoader />
</div>
</section>
<div class="feed-grid">
<!-- FOLLOWING: secondary, supporting content -->
<aside class="following-box">
<h2>Following</h2>
<div v-if="followingLoading" class="following-loading">
<PlaneLoader />
</div>
<div v-else-if="!following?.length" class="empty-small">
<p>You're not following anyone yet.</p>
</div>
<ul v-else class="following-list">
<li v-for="person in following" :key="person.id" class="following-item">
<Avatar :user="person" />
<Link :href="route('profile.view', { user: person?.name })">
<span class="following-name">{{ person.name }}</span>
</Link>
</li>
</ul>
</aside>
<!-- FEED: condensed, de-emphasised -->
<div class="feed-condensed">
<div v-if="feed.length === 0" class="empty">
<p>Nothing here yet — follow someone to see their flight updates!</p>
</div>
<div v-else class="feed-list">
<FeedItem v-for="action in feed" :key="action.id" :action="action" compact />
</div>
</div>
</div>
</div>
<div class="feed-list">
<FeedItem v-for="action in feed" :key="action.id" :action="action" />
</div>
</div>
</ProfileLayout>
</template>
<style scoped>
.feed-page {
margin: 0 auto;
padding: 2rem 1rem;
width: 55%;
width: 70%;
display: flex;
flex-direction: column;
gap: 2rem;
}
@media (max-width: 768px) {
@@ -48,7 +100,6 @@ const props = defineProps<{
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.feed-header h1 {
@@ -62,14 +113,119 @@ const props = defineProps<{
color: #6b7280;
}
/* MAP */
.map-section {
position: relative;
overflow: hidden;
}
.map-caption {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 10;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(10, 15, 30, 0.65);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
.caption-icon {
font-size: 1rem;
line-height: 1;
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 40dvh;
}
/* GRID: following sidebar + condensed feed */
.feed-grid {
display: grid;
grid-template-columns: 240px 1fr;
gap: 2rem;
align-items: start;
}
@media (max-width: 768px) {
.feed-grid {
grid-template-columns: 1fr;
}
}
.following-box {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 1rem;
}
.following-box h2 {
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.75rem;
}
.following-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.following-item {
display: flex;
align-items: center;
gap: 0.6rem;
}
.following-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
object-fit: cover;
}
.following-name {
font-size: 0.85rem;
}
.following-loading {
display: flex;
justify-content: center;
padding: 1rem 0;
}
.empty-small p {
font-size: 0.8rem;
color: #6b7280;
margin: 0;
}
/* CONDENSED FEED — de-emphasised vs before */
.feed-condensed {
opacity: 0.92;
}
.empty p {
text-align: center;
margin-top: 3rem;
color: #6b7280;
margin-top: 1rem;
}
.feed-list {
display: flex;
flex-direction: column;
gap: 2rem;
gap: 1rem; /* tightened from 2rem */
}
</style>
@@ -56,7 +56,7 @@ const unlocked = computed(() => {
</script>
<template>
<ProfileLayout :title="`${achievement.name}`" :canView="canView" :achievementCount="achievementCount" :user="user" :followStatus="followStatus" :loading="flightsLoading">
<ProfileLayout :title="`${achievement.name}`" :canView="canView" :achievementCount="achievementCount" :user="user" :followStatus="followStatus" :loading="flightsLoading" showCount>
<div class="innerLayout">
<ButtonLink variant="flat" icon="mdi-arrow-left" :label="`Back to ${user.name}'s Achievements`" :href="`${route('profile.achievements', { user: user.name })}#${achievement.internal_name}`" />
@@ -1,30 +0,0 @@
<script setup lang="ts">
import MainLayout from "@/Layouts/MainLayout.vue";
import { Head } from '@inertiajs/vue3';
import {Flight, User} from "@/Types/types";
import ProfileViewSwitcher from "@/Components/FlightsGoneBy/ProfileViewSwitcher.vue";
import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue";
import BoardingPasses from "@/Components/FlightsGoneBy/BoardingPasses.vue";
defineOptions({
layout: MainLayout
})
defineProps<{
user: User
flights: Flight[]
canEdit: boolean
}>()
</script>
<template>
<Head :title="`${user.name}'s Flights`" />
<ProfileLayout :flights="flights" :user="user">
<ProfileViewSwitcher :user="user" active-view="passes" />
<BoardingPasses :flights="flights" :canEdit="canEdit" />
</ProfileLayout>
</template>
<style scoped>
</style>
+1
View File
@@ -71,6 +71,7 @@ const filteredUnlockedCount = computed(() =>
:loading="false"
:canView="canView"
:title="`${user.name}'s Achievements`"
showCount
>
<ProfileViewSwitcher active-view="achievements" :user="user" />
+1
View File
@@ -30,6 +30,7 @@ const props = defineProps<{
:user="user"
:followStatus="followStatus"
:flight-count="flightCount"
showCount
:loading="false">
<Head :title="`${flight.flight_number ?? user.name + '\'s Flight'}`" />
+1 -1
View File
@@ -187,7 +187,7 @@ function switchView(view: ProfileView) {
</script>
<template>
<ProfileLayout :title="`${user.name}'s Flights`" :canView="canView" :followStatus="followStatus" :flightCount="flightCount" :user="user" :loading="flightsLoading">
<ProfileLayout :title="`${user.name}'s Flights`" :canView="canView" :followStatus="followStatus" :flightCount="flightCount" :user="user" :loading="flightsLoading" showCount>
<div class="toolbar">
<ProfileViewSwitcher :user="user" :active-view="activeView" @update:active-view="switchView" />
+1
View File
@@ -300,6 +300,7 @@ export interface Flight {
range: FlightRange
region_range: RegionRange
livery_url?: string
user?: User | null
}