Added Notifications
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string;
|
||||
count: number;
|
||||
weeklyGrowth: number;
|
||||
icon: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="pa-5 d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">{{ label }}</div>
|
||||
<div class="text-h5 font-weight-medium">{{ count }}</div>
|
||||
</div>
|
||||
<div class="text-caption "><slot/></div>
|
||||
<div class="text-right">
|
||||
<div class="d-flex align-center justify-end ga-1">
|
||||
<v-icon :icon="icon" color="success" size="20" />
|
||||
<span class="text-h6 font-weight-medium text-success">+{{ weeklyGrowth }}</span>
|
||||
</div>
|
||||
<div class="text-caption text-disabled">new this week </div>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { MissingLivery } from "@/Types/types";
|
||||
import {router} from "@inertiajs/vue3";
|
||||
import CopyButton from "@/Components/FlightsGoneBy/CopyButton.vue";
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
livery: MissingLivery;
|
||||
}>();
|
||||
|
||||
const ignore = () => {
|
||||
router.post(route('admin.ignore-missing-livery'), { filename: props.livery.filename });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card flat rounded="lg" class="mb-2 px-4 py-3">
|
||||
<v-row align="center" no-gutters>
|
||||
|
||||
<v-col cols="12" sm="4">
|
||||
<div class="d-flex align-center ga-1 text-medium-emphasis" style="font-size: 11px;">
|
||||
Airline
|
||||
</div>
|
||||
<div style="font-size: 12px;" class="font-weight-medium">{{ livery.airline_name }}</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="4">
|
||||
<div class="d-flex align-center ga-1 text-medium-emphasis" style="font-size: 11px;">
|
||||
Aircraft
|
||||
<CopyButton :text="livery.clipboard_text" />
|
||||
</div>
|
||||
<div style="font-size: 12px;" class="font-weight-medium">{{ livery.aircraft_display_name }}</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="4">
|
||||
<div class="d-flex align-center ga-1 text-medium-emphasis" style="font-size: 11px;">
|
||||
Filename
|
||||
<CopyButton :text="livery.filename" />
|
||||
</div>
|
||||
<div style="font-size: 12px;" class="font-weight-medium">{{ livery.filename }}</div>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
<v-row no-gutters class="mt-2" justify="end">
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
color="error"
|
||||
prepend-icon="mdi-eye-off"
|
||||
@click="ignore"
|
||||
>
|
||||
Ignore
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import type { CodeType } from '@/Composables/useAlphabetAirlines'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
|
||||
import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue";
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
letters: string[]
|
||||
@@ -50,8 +51,8 @@ function isHighlighted({ firstYear }: AirlineEntry): boolean {
|
||||
return props.selectedYear !== null && firstYear === props.selectedYear
|
||||
}
|
||||
|
||||
function toBBCode(): string {
|
||||
return props.letters
|
||||
const bbCode = computed(() =>
|
||||
props.letters
|
||||
.map(letter => {
|
||||
const entries = airlineEntriesForLetter(letter)
|
||||
if (!entries.length) return letter
|
||||
@@ -65,9 +66,9 @@ function toBBCode(): string {
|
||||
.join(', ')
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({ toBBCode })
|
||||
defineExpose({ bbCode })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Airport, Flight} from '@/Types/types'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import InlineBadge from '@/Components/FlightsGoneBy/InlineBadge.vue'
|
||||
import AirportToolTip from '@/Components/FlightsGoneBy/AirportToolTip.vue'
|
||||
import {computed} from "vue";
|
||||
|
||||
type CodeType = 'iata' | 'icao'
|
||||
|
||||
@@ -50,8 +51,8 @@ function isHighlighted({ firstYear }: AirportEntry): boolean {
|
||||
return props.selectedYear !== null && firstYear === props.selectedYear
|
||||
}
|
||||
|
||||
function toBBCode(): string {
|
||||
return props.letters
|
||||
const bbCode = computed(() =>
|
||||
props.letters
|
||||
.map(letter => {
|
||||
const entries = airportEntriesForLetter(letter)
|
||||
if (!entries.length) return letter
|
||||
@@ -64,9 +65,9 @@ function toBBCode(): string {
|
||||
.join(', ')
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({ toBBCode })
|
||||
defineExpose({ bbCode })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { copyToClipboard } from "@/Composables/useClipboard";
|
||||
|
||||
const props = defineProps<{
|
||||
text: string;
|
||||
title?: string;
|
||||
size?: string;
|
||||
}>();
|
||||
|
||||
const copied = ref(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await copyToClipboard(props.text);
|
||||
copied.value = true;
|
||||
setTimeout(() => copied.value = false, 1500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
:title="title"
|
||||
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
|
||||
:size="size ?? 'x-small'"
|
||||
:variant="copied ? 'tonal' : 'plain'"
|
||||
:color="copied ? 'success' : undefined"
|
||||
density="compact"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
</template>
|
||||
@@ -38,7 +38,8 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||
<template v-else>
|
||||
<Link :href="route('flights.add')" class="nav-link">Add Flight</Link>
|
||||
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link">Profile</Link>
|
||||
<Link href="/feed" class="nav-link">Feed</Link>
|
||||
<Link :href="route('feed')" class="nav-link">Feed</Link>
|
||||
<Link v-if="props.auth.roles.includes('admin')" :href="route('admin.dashboard')" class="nav-link">Admin</Link>
|
||||
|
||||
<div class="dropdown" ref="dropdownRef">
|
||||
<button class="nav-link dropdown-trigger" @click.stop="dropdownOpen = !dropdownOpen">
|
||||
@@ -73,7 +74,7 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||
<span class="nav-greeting">Welcome, {{ props.auth.user.name }}</span>
|
||||
<Link :href="route('flights.add')" class="nav-link" @click="menuOpen = false">Add Flight</Link>
|
||||
<Link :href="route('profile.view', { user: props.auth.user.name })" class="nav-link" @click="menuOpen = false">Profile</Link>
|
||||
<Link href="/feed" class="nav-link nav-link--highlight" @click="menuOpen = false">Feed</Link>
|
||||
<Link :href="route('feed')" class="nav-link nav-link" @click="menuOpen = false">Feed</Link>
|
||||
<Link :href="route('import.fr24')" class="nav-link" @click="menuOpen = false">Import from FR24</Link>
|
||||
<div class="dropdown-divider" />
|
||||
<button class="nav-link nav-link--danger" @click="logout">Log Out</button>
|
||||
@@ -115,9 +116,10 @@ header {
|
||||
|
||||
/* Shared nav link base */
|
||||
.nav-link {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
height: 100%;
|
||||
padding: 0.3em 0.75em;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const copyToClipboard = (text: string): Promise<void> => {
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = text;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
@@ -1,198 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
||||
import Dropdown from '@/Components/Dropdown.vue';
|
||||
import DropdownLink from '@/Components/DropdownLink.vue';
|
||||
import NavLink from '@/Components/NavLink.vue';
|
||||
import ResponsiveNavLink from '@/Components/ResponsiveNavLink.vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const showingNavigationDropdown = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
<nav
|
||||
class="border-b border-gray-100 bg-white"
|
||||
>
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 justify-between">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="flex shrink-0 items-center">
|
||||
<Link :href="route('dashboard')">
|
||||
<ApplicationLogo
|
||||
class="block h-9 w-auto fill-current text-gray-800"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div
|
||||
class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"
|
||||
>
|
||||
<NavLink
|
||||
:href="route('dashboard')"
|
||||
:active="route().current('dashboard')"
|
||||
>
|
||||
Dashboard
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:ms-6 sm:flex sm:items-center">
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="relative ms-3">
|
||||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<span class="inline-flex rounded-md">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-transparent bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-500 transition duration-150 ease-in-out hover:text-gray-700 focus:outline-none"
|
||||
>
|
||||
{{ $page.props.auth.user.name }}
|
||||
|
||||
<svg
|
||||
class="-me-0.5 ms-2 h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<DropdownLink
|
||||
:href="route('profile.edit')"
|
||||
>
|
||||
Profile
|
||||
</DropdownLink>
|
||||
<DropdownLink
|
||||
:href="route('logout')"
|
||||
method="post"
|
||||
as="button"
|
||||
>
|
||||
Log Out
|
||||
</DropdownLink>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button
|
||||
@click="
|
||||
showingNavigationDropdown =
|
||||
!showingNavigationDropdown
|
||||
"
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 transition duration-150 ease-in-out hover:bg-gray-100 hover:text-gray-500 focus:bg-gray-100 focus:text-gray-500 focus:outline-none"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
:class="{
|
||||
hidden: showingNavigationDropdown,
|
||||
'inline-flex':
|
||||
!showingNavigationDropdown,
|
||||
}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
<path
|
||||
:class="{
|
||||
hidden: !showingNavigationDropdown,
|
||||
'inline-flex':
|
||||
showingNavigationDropdown,
|
||||
}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div
|
||||
:class="{
|
||||
block: showingNavigationDropdown,
|
||||
hidden: !showingNavigationDropdown,
|
||||
}"
|
||||
class="sm:hidden"
|
||||
>
|
||||
<div class="space-y-1 pb-3 pt-2">
|
||||
<ResponsiveNavLink
|
||||
:href="route('dashboard')"
|
||||
:active="route().current('dashboard')"
|
||||
>
|
||||
Dashboard
|
||||
</ResponsiveNavLink>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div
|
||||
class="border-t border-gray-200 pb-1 pt-4"
|
||||
>
|
||||
<div class="px-4">
|
||||
<div
|
||||
class="text-base font-medium text-gray-800"
|
||||
>
|
||||
{{ $page.props.auth.user.name }}
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-500">
|
||||
{{ $page.props.auth.user.email }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<ResponsiveNavLink :href="route('profile.edit')">
|
||||
Profile
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
:href="route('logout')"
|
||||
method="post"
|
||||
as="button"
|
||||
>
|
||||
Log Out
|
||||
</ResponsiveNavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Heading -->
|
||||
<header
|
||||
class="bg-white shadow"
|
||||
v-if="$slots.header"
|
||||
>
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,22 +0,0 @@
|
||||
<script setup>
|
||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex min-h-screen flex-col items-center bg-gray-100 pt-6 sm:justify-center sm:pt-0"
|
||||
>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<ApplicationLogo class="h-20 w-20 fill-current text-gray-500" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-6 w-full overflow-hidden bg-white px-6 py-4 shadow-md sm:max-w-md sm:rounded-lg"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,15 +16,21 @@ const achievementSound = new Audio('/sounds/seatBelt.wav')
|
||||
|
||||
function handleNewNotifications(notifications: Notification[]) {
|
||||
if (!notifications?.length) return
|
||||
const unseen = notifications.filter(n => !seenNotificationIds.value.has(n.id))
|
||||
if (!unseen.length) return
|
||||
unseen.forEach(n => seenNotificationIds.value.add(n.id))
|
||||
activeToasts.value.push(...unseen)
|
||||
|
||||
const newToasts: Notification[] = []
|
||||
for (const n of notifications) {
|
||||
if (!seenNotificationIds.value.has(n.id)) {
|
||||
seenNotificationIds.value.add(n.id)
|
||||
newToasts.push(n)
|
||||
}
|
||||
}
|
||||
|
||||
if (!newToasts.length) return
|
||||
activeToasts.value.push(...newToasts)
|
||||
achievementSound.play().catch(() => {})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── Toasts ────────────────────────────────────────────────────────────────────
|
||||
const activeToasts = ref<Notification[]>([])
|
||||
|
||||
@@ -47,13 +53,13 @@ router.on('success', (event) => {
|
||||
<template>
|
||||
<Radar>
|
||||
<div class="layoutContainer">
|
||||
<MainHeader :key="transitionKey" />
|
||||
<MainHeader :key="`header-${transitionKey}`" />
|
||||
<Transition name="fade" mode="out-in">
|
||||
<main id="pageContainer" :key="transitionKey">
|
||||
<main id="pageContainer" :key="`main-${transitionKey}`">
|
||||
<slot />
|
||||
</main>
|
||||
</Transition>
|
||||
<MainFooter :key="transitionKey" />
|
||||
<MainFooter :key="`footer-${transitionKey}`" />
|
||||
</div>
|
||||
|
||||
<div class="toast-stack">
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import MainLayout from "@/Layouts/MainLayout.vue";
|
||||
import AdminSidebar from "@/Pages/Admin/AdminSidebar.vue";
|
||||
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
|
||||
import {Head} from "@inertiajs/vue3";
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
missingLiveryCount: number;
|
||||
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MainLayout>
|
||||
<Head :title="title" />
|
||||
<div class="admin-container">
|
||||
<AdminSidebar :missingLiveryCount="missingLiveryCount" />
|
||||
<div class="admin-content">
|
||||
<GlassBox class="admin-page" :title="title">
|
||||
<slot />
|
||||
</GlassBox>
|
||||
</div>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
min-height: 90dvh;
|
||||
/* Override MainLayout's centred main — fill the full height */
|
||||
align-self: stretch;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;;
|
||||
flex: 1 1 auto;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-page {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
min-height: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import {Link} from "@inertiajs/vue3";
|
||||
import {number} from "echarts";
|
||||
defineProps<{
|
||||
missingLiveryCount: number;
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="glass admin-sidebar">
|
||||
<div class="sidebar-title">Admin</div>
|
||||
<Link :href="route('admin.dashboard')" class="sidebar-link">
|
||||
<v-icon icon="mdi-chart-line" size="18" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link :href="route('admin.reconcile-missing-liveries')" class="sidebar-link">
|
||||
<v-icon icon="mdi-airplane-takeoff" size="18" />
|
||||
Reconcile Missing Liveries
|
||||
<v-chip size="x-small" color="error" class="ml-1">{{ missingLiveryCount }}</v-chip>
|
||||
</Link>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-sidebar {
|
||||
width: 400px;
|
||||
flex-shrink: 0;
|
||||
padding: 1.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
/* no height here */
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text);
|
||||
opacity: 0.5;
|
||||
padding: 0 0.5rem 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-link:hover {
|
||||
color: var(--accent);
|
||||
background: rgba(56, 189, 248, 0.07);
|
||||
}
|
||||
|
||||
.sidebar-link.active {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import AdminLayout from "@/Pages/Admin/AdminLayout.vue";
|
||||
import GrowthCard from "@/Components/Admin/GrowthCard.vue";
|
||||
|
||||
defineOptions({ layout: AdminLayout });
|
||||
|
||||
defineProps<{
|
||||
userCount: number,
|
||||
oneWeekUserGrowth: number,
|
||||
flightCount: number,
|
||||
oneWeekFlightGrowth: number,
|
||||
latestUser:string
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<GrowthCard label="Total Users" :count="userCount" :weekly-growth="oneWeekUserGrowth" icon="mdi-account">
|
||||
Latest User: {{ latestUser }}
|
||||
</GrowthCard>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<GrowthCard label="Total Flights" :count="flightCount" :weekly-growth="oneWeekFlightGrowth" icon="mdi-airplane" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import GlassBox from "@/Components/FlightsGoneBy/GlassBox.vue";
|
||||
import { Head } from "@inertiajs/vue3";
|
||||
import {MissingLivery} from "@/Types/types";
|
||||
import MissingLiveryCard from "@/Components/Admin/MissingLiveryCard.vue";
|
||||
import AdminLayout from "@/Pages/Admin/AdminLayout.vue";
|
||||
import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue";
|
||||
|
||||
defineOptions({ layout: AdminLayout });
|
||||
|
||||
defineProps<{
|
||||
missingLiveries: MissingLivery[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MissingLiveryCard v-for="livery in missingLiveries" :key="livery.filename" :livery="livery" />
|
||||
</template>
|
||||
@@ -10,7 +10,7 @@ import { router } from "@inertiajs/vue3";
|
||||
defineOptions({ layout: MainLayout });
|
||||
|
||||
const page = usePage<SharedProps>();
|
||||
const name = computed(() => page?.props?.auth?.user?.name || 'there');
|
||||
const name = computed(() => page?.props?.auth?.user?.name || 'mate');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -7,6 +7,8 @@ import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -7,7 +7,7 @@ import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
||||
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Achievement, Airline, Alliance, Flight, User } from '@/Types/types'
|
||||
import AllianceChallenge from '@/Components/FlightsGoneBy/AllianceChallenge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
||||
import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import Panel from '@/Components/FlightsGoneBy/Panels/Panel.vue'
|
||||
import PanelHeader from '@/Components/FlightsGoneBy/Panels/PanelHeader.vue'
|
||||
import BadgeTable from '@/Components/FlightsGoneBy/GenericBadgeTable.vue'
|
||||
import FlightBadge from '@/Components/FlightsGoneBy/FlightBadge.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -6,7 +6,9 @@ import PanelSubHeader from '@/Components/FlightsGoneBy/Panels/PanelSubHeader.vue
|
||||
import AirlineAlphabetTable from '@/Components/FlightsGoneBy/AirlineAlphabetTable.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAlphabetAirlines, type CodeType } from '@/Composables/useAlphabetAirlines'
|
||||
|
||||
import {copyToClipboard} from "@/Composables/useClipboard";
|
||||
import CopyButton from "@/Components/FlightsGoneBy/CopyButton.vue";
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
@@ -47,28 +49,7 @@ const selectedYear = ref<number | null>(
|
||||
|
||||
// Copy BBCode
|
||||
const airlineTable = ref<InstanceType<typeof AirlineAlphabetTable> | null>(null)
|
||||
const copied = ref(false)
|
||||
|
||||
async function copyBBCode() {
|
||||
const text = airlineTable.value?.toBBCode()
|
||||
if (text == null) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch {
|
||||
const el = document.createElement('textarea')
|
||||
el.value = text
|
||||
el.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0'
|
||||
document.body.appendChild(el)
|
||||
el.focus()
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
|
||||
copied.value = true
|
||||
setTimeout(() => (copied.value = false), 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -133,13 +114,10 @@ async function copyBBCode() {
|
||||
class="year-select"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
|
||||
:color="copied ? 'success' : undefined"
|
||||
density="compact"
|
||||
variant="text"
|
||||
<CopyButton
|
||||
title="Copy as BBCode"
|
||||
@click="copyBBCode"
|
||||
size="large"
|
||||
:text="airlineTable?.bbCode ?? ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,8 @@ import AlphabetTable from '@/Components/FlightsGoneBy/AlphabetTable.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAlphabetFlights, type CodeType } from '@/Composables/useAlphabetFlights'
|
||||
import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue";
|
||||
|
||||
import CopyButton from "@/Components/FlightsGoneBy/CopyButton.vue";
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
@@ -45,27 +46,6 @@ const selectedYear = ref<number | null>(
|
||||
const alphabetTable = ref<InstanceType<typeof AlphabetTable> | null>(null)
|
||||
const copied = ref(false)
|
||||
|
||||
async function copyBBCode() {
|
||||
const text = alphabetTable.value?.toBBCode()
|
||||
if (text == null) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch {
|
||||
// Fallback for non-HTTPS or browsers that block clipboard API
|
||||
const el = document.createElement('textarea')
|
||||
el.value = text
|
||||
el.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0'
|
||||
document.body.appendChild(el)
|
||||
el.focus()
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
|
||||
copied.value = true
|
||||
setTimeout(() => (copied.value = false), 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -124,13 +104,10 @@ async function copyBBCode() {
|
||||
class="year-select"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
|
||||
:color="copied ? 'success' : undefined"
|
||||
density="compact"
|
||||
variant="text"
|
||||
<CopyButton
|
||||
title="Copy as BBCode"
|
||||
@click="copyBBCode"
|
||||
size="large"
|
||||
:text="alphabetTable?.bbCode ?? ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {useRegionFlights} from "@/Composables/useRegionFlights";
|
||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -12,7 +12,7 @@ import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
import Brazil from "@/Components/Maps/Brazil.vue";
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -13,7 +13,7 @@ import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
import Canada from "@/Components/Maps/Canada.vue";
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -12,7 +12,7 @@ import China from "@/Components/Maps/China.vue";
|
||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
defineOptions({ inheritAttrs: false })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
@@ -11,9 +11,8 @@ import FlightRegionTable from "@/Components/FlightsGoneBy/FlightRegionTable.vue"
|
||||
import {useRegionFlights} from "@/Composables/useRegionFlights";
|
||||
import RegionLegend from "@/Components/FlightsGoneBy/Panels/RegionLegend.vue";
|
||||
import USA from "@/Components/Maps/USA.vue";
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
|
||||
defineOptions({ layout: MainLayout })
|
||||
const props = defineProps<{
|
||||
achievement: Achievement
|
||||
user: User
|
||||
|
||||
Vendored
+9
@@ -62,6 +62,8 @@ export type SharedProps = import('@inertiajs/core').PageProps & {
|
||||
auth: {
|
||||
user: User | null
|
||||
isLoggedIn: boolean
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
},
|
||||
logo_api_url: string
|
||||
achievement_notifications: Notification[]
|
||||
@@ -259,6 +261,13 @@ export interface Flight {
|
||||
livery_url?: string
|
||||
}
|
||||
|
||||
export interface MissingLivery {
|
||||
airline_name: string;
|
||||
aircraft_display_name: string;
|
||||
filename: string;
|
||||
clipboard_text: string;
|
||||
}
|
||||
|
||||
declare module '@inertiajs/vue3' {
|
||||
interface PageProps extends SharedProps {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user