Fixes from template

This commit is contained in:
2026-06-25 00:38:47 -07:00
parent d134b480a0
commit bf1d82fa19
24 changed files with 162 additions and 323 deletions
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "alphane-web",
"name": "calebburke-web",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "alphane-web",
"name": "calebburke-web",
"version": "0.1.0",
"dependencies": {
"@auth0/auth0-vue": "^2.5.0",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "alphane-web",
"name": "calebburke-web",
"private": true,
"version": "0.1.0",
"type": "module",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg width="109" height="44" viewBox="0 0 109 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M99 26.9C101.3 26.9 102.6 28.4 102.6 31.2V42.7H108.4V29.3C108.4 24.5 105.8 21.6 101.4 21.6C98.3 21.6 96.6 23.3 95.4 24.9L95.3 25V22H89.5V42.7H95.3V31.1C95.2 28.4 96.6 26.9 99 26.9Z" fill="black"/>
<path d="M81.6 32.5C81.6 34 81.1 35.5 80.2 36.5C79.2 37.6 77.8 38.2 76.2 38.2C74.6 38.2 73.2 37.6 72.2 36.5C71.2 35.4 70.7 34 70.7 32.4V32.3C70.7 30.8 71.2 29.3 72.1 28.3C73.1 27.2 74.5 26.6 76.1 26.6C77.7 26.6 79.1 27.2 80.1 28.3C81 29.4 81.6 30.9 81.6 32.5ZM76.1 21.6C73 21.6 70.2 22.7 68.1 24.8C66 26.8 64.9 29.5 64.9 32.4V32.5C64.9 35.4 66 38.1 68.1 40.1C70.2 42.1 73 43.2 76.1 43.2C79.2 43.2 82 42.1 84.1 40C86.2 38 87.3 35.3 87.3 32.4V32.3C87.3 29.4 86.2 26.7 84.1 24.7C82.1 22.7 79.2 21.6 76.1 21.6Z" fill="black"/>
<path d="M59.8 42.7H66.4L58.3 30.1L66.1 22H59.2L52.3 29.7V15.1H46.5V42.7H52.3V36.4L54.4 34.2L59.8 42.7Z" fill="black"/>
<path d="M31.6 43.1C34.7 43.1 36.4 41.4 37.6 39.8L37.7 39.7V42.7H43.5V22H37.7V33.5C37.7 36.2 36.3 37.8 34 37.8C31.7 37.8 30.4 36.3 30.4 33.5V22H24.6V35.4C24.6 40.2 27.2 43.1 31.6 43.1Z" fill="black"/>
<path d="M20.7 15.7L14 26.3V26.2L7.5 15.7H0.699997L11.1 31.7V42.7H16.9V31.6L27.3 15.7H20.7Z" fill="black"/>
<path d="M76.1 0.800003L74 7.4L69.5 2.1L70.2 9L64 5.9L67.2 12L60.3 11.4L65.6 15.9L59 18L64.7 19.8L69.6 14.5L71.6 16.7L76.2 11.8L80.7 16.7L82.7 14.5L87.6 19.7L93.3 17.9L86.7 15.8L92 11.3L85.1 12L88.2 5.8L82.1 9L82.7 2.1L78.2 7.4L76.1 0.800003Z" fill="#F2A900"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -46,7 +46,7 @@
import { computed } from "vue"
import { useDisplay } from "vuetify"
import HeaderActionsCard from "@/components/shared/cards/HeaderActionsCard.vue"
import HeaderActionsCard from "@/components/common/HeaderActionsCard.vue"
const props = withDefaults(
defineProps<{
@@ -0,0 +1,104 @@
<template>
<v-text-field
ref="textField"
:model-value="modelValue"
:label="label"
:rules="[...rules, valueMustBeUnique]"
@update:model-value="updateModelValue"
>
<template #append-inner>
<v-icon
v-if="isNil(isUniqueValue)"
:title="uniqueValidationMessage"
>mdi-progress-clock</v-icon
>
<v-progress-circular
v-else-if="isCheckingUniqueness"
size="18"
width="2"
indeterminate
color="primary"
/>
<v-icon
v-else-if="isUniqueValue === true"
:title="isUniqueMessage"
color="success"
>mdi-check-circle-outline</v-icon
>
<v-icon
v-else
color="warning"
:title="isNotUniqueMessage"
>mdi-alert-circle-outline</v-icon
>
</template>
</v-text-field>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { debounce, isEmpty, isNil } from "lodash"
import { VTextField } from "vuetify/components"
const props = withDefaults(
defineProps<{
modelValue: string | null | undefined
label: string
checkAvailability: (value: string) => Promise<boolean>
rules?: VTextField["rules"]
uniqueValidationMessage?: string
isUniqueMessage?: string
isNotUniqueMessage?: string
}>(),
{
rules: () => [],
uniqueValidationMessage: "Value must be unique",
isUniqueMessage: "Value is unique",
isNotUniqueMessage: "Value is not unique",
}
)
const emit = defineEmits<{
"update:modelValue": [value: string]
}>()
const isUniqueValue = ref<boolean | null>(null)
const isCheckingUniqueness = ref(false)
const textField = ref<InstanceType<typeof VTextField> | null>(null)
async function updateModelValue(value: string) {
emit("update:modelValue", value)
await debouncedCheckValueIsUnique(value)
}
async function checkValueIsUnique(value: string | null | undefined) {
if (isNil(value) || isEmpty(value)) return null
isCheckingUniqueness.value = true
try {
const result = await props.checkAvailability(value)
isUniqueValue.value = result
await textField.value?.validate(false)
return result
} catch (error) {
console.error(error)
return false
} finally {
isCheckingUniqueness.value = false
}
}
const debouncedCheckValueIsUnique = debounce(checkValueIsUnique, 500)
function valueMustBeUnique() {
if (isUniqueValue.value === true || isUniqueValue.value === null) return true
return props.uniqueValidationMessage
}
defineExpose({
validate: () => checkValueIsUnique(props.modelValue),
})
</script>
@@ -0,0 +1,34 @@
<template>
<UniqueTextField
ref="uniqueTextField"
v-model="userEmail"
label="Email"
:check-availability="checkEmailAvailability"
unique-validation-message="User email must be unique"
is-unique-message="User email is available"
is-not-unique-message="User email is already taken"
/>
</template>
<script setup lang="ts">
import { ref } from "vue"
import usersApi from "@/api/users-api"
import UniqueTextField from "@/components/common/UniqueTextField.vue"
const userEmail = defineModel<string | null | undefined>({
required: true,
})
async function checkEmailAvailability(email: string) {
const { users } = await usersApi.list({
where: {
email,
},
})
return users.length === 0
}
const uniqueTextField = ref<InstanceType<typeof UniqueTextField> | null>(null)
</script>
@@ -53,114 +53,8 @@
validate-on="blur"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<v-text-field
v-model="userAttributes.title"
label="Title"
clearable
/>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
md="6"
>
<v-text-field
v-model="userAttributes.pilotLicense"
label="Pilot License"
placeholder="e.g., CPL-123456"
clearable
/>
</v-col>
<v-col
cols="12"
md="6"
>
<v-text-field
v-model="userAttributes.ameLicense"
label="AME License"
placeholder="e.g., AME-789012"
clearable
/>
</v-col>
</v-row>
<h4 class="mt-3">Images</h4>
<v-divider class="mt-1 mb-2" />
<v-row>
<v-col
cols="12"
md="6"
>
<v-file-input
v-model="profileImageFile"
accept="image/*"
prepend-icon="mdi-camera"
label="Upload Profile Image"
clearable
@change="handleProfileImageUpload"
/>
<div
v-if="profileImagePreview"
class="mt-4 d-flex justify-center"
>
<v-img
:src="profileImagePreview"
max-width="200"
max-height="200"
class="rounded elevation-2"
cover
/>
</div>
</v-col>
<v-col
cols="12"
md="6"
>
<v-file-input
v-model="signatureImageFile"
accept="image/*"
prepend-icon="mdi-draw"
label="Upload Signature"
clearable
@change="handleSignatureImageUpload"
/>
<div
v-if="signatureImagePreview"
class="mt-4 d-flex justify-center"
>
<v-img
:src="signatureImagePreview"
max-width="300"
max-height="150"
class="rounded elevation-2"
contain
/>
</div>
</v-col>
</v-row>
<h4 class="mt-3">Notification Details</h4>
<v-divider class="mt-1 mb-2" />
<v-col
cols="12"
md="6"
>
<v-switch
v-model="userAttributes.emailNotificationsEnabled"
label="Email notifications enabled?"
inset
center-affix
/>
</v-col>
<template #actions>
<div class="d-flex">
<v-btn
@@ -192,12 +86,12 @@ import { useRouter } from "vue-router"
import { isEmpty, isNil } from "lodash"
import { required, minimum, email } from "@/utils/validators"
import { resizeToStandard } from "@/utils/image-resizer"
import usersApi, { User } from "@/api/users-api"
import useBreadcrumbs from "@/use/use-breadcrumbs"
import useSnack from "@/use/use-snack"
import HeaderActionsFormCard from "@/components/shared/cards/HeaderActionsFormCard.vue"
import HeaderActionsFormCard from "@/components/common/HeaderActionsFormCard.vue"
import UserEmailUniqueTextField from "@/components/users/UserEmailUniqueTextField.vue"
const userAttributes = ref<Partial<User>>({
@@ -205,88 +99,8 @@ const userAttributes = ref<Partial<User>>({
displayName: "",
firstName: "",
lastName: "",
title: null,
pilotLicense: null,
ameLicense: null,
emailNotificationsEnabled: true,
})
// Image handling
const profileImageFile = ref<File[] | File | null>(null)
const signatureImageFile = ref<File[] | File | null>(null)
const profileImagePreview = ref<string | null>(null)
const signatureImagePreview = ref<string | null>(null)
async function handleProfileImageUpload() {
if (!profileImageFile.value) {
profileImagePreview.value = null
userAttributes.value.profileImage = null
return
}
// Handle both array and single file
const file = Array.isArray(profileImageFile.value)
? profileImageFile.value[0]
: profileImageFile.value
if (!file) {
profileImagePreview.value = null
userAttributes.value.profileImage = null
return
}
// Ensure we have a valid File object
if (!(file instanceof File)) {
console.error("Invalid file type:", file)
return
}
try {
// Resize image to 512x512 before upload
const base64DataUrl = await resizeToStandard(file)
profileImagePreview.value = base64DataUrl
userAttributes.value.profileImage = base64DataUrl
} catch (error) {
console.error("Error resizing profile image:", error)
snack.error("Failed to process profile image")
}
}
async function handleSignatureImageUpload() {
if (!signatureImageFile.value) {
signatureImagePreview.value = null
userAttributes.value.signatureImage = null
return
}
// Handle both array and single file
const file = Array.isArray(signatureImageFile.value)
? signatureImageFile.value[0]
: signatureImageFile.value
if (!file) {
signatureImagePreview.value = null
userAttributes.value.signatureImage = null
return
}
// Ensure we have a valid File object
if (!(file instanceof File)) {
console.error("Invalid file type:", file)
return
}
try {
// Resize image to 512x512 before upload
const base64DataUrl = await resizeToStandard(file)
signatureImagePreview.value = base64DataUrl
userAttributes.value.signatureImage = base64DataUrl
} catch (error) {
console.error("Error resizing signature image:", error)
snack.error("Failed to process signature image")
}
}
function autoFillDependentFields(focused: boolean) {
if (focused) return
+2 -2
View File
@@ -3,7 +3,7 @@
headline="Whoops, 403"
title="Access Forbidden"
text="You do not have permission to access that page"
:image="SplashImage"
:image="AppLogoSplash"
>
<v-row>
<v-col class="d-flex justify-center">
@@ -48,7 +48,7 @@
</template>
<script lang="ts" setup>
import SplashImage from "@/assets/SplashImage.png"
import AppLogoSplash from "@/assets/app_logo_splash.png"
import { useAuth0 } from "@auth0/auth0-vue"
import { APPLICATION_NAME } from "@/config"
@@ -3,7 +3,7 @@
headline="Whoops, 500"
title="Internal Server Error"
:text="'Oops! The server encountered an unexpected error. Please\u00a0contact\u00a0support.'"
:image="SplashImage"
:image="AppLogoSplash"
>
<v-row>
<v-col class="d-flex justify-center">
@@ -48,7 +48,7 @@
</template>
<script lang="ts" setup>
import SplashImage from "@/assets/SplashImage.png"
import AppLogoSplash from "@/assets/app_logo_splash.png"
import { useAuth0 } from "@auth0/auth0-vue"
import { APPLICATION_NAME } from "@/config"
+2 -2
View File
@@ -3,7 +3,7 @@
headline="Whoops, 404"
title="Page Not Found"
text="Oops! The page you're looking doesn't exist."
:image="SplashImage"
:image="AppLogoSplash"
>
<v-row>
<v-col class="d-flex justify-center">
@@ -48,7 +48,7 @@
</template>
<script lang="ts" setup>
import SplashImage from "@/assets/SplashImage.png"
import AppLogoSplash from "@/assets/app_logo_splash.png"
import { useAuth0 } from "@auth0/auth0-vue"
import { APPLICATION_NAME } from "@/config"
+2 -2
View File
@@ -3,7 +3,7 @@
headline="Whoops, 401"
title="Unauthorized"
text="If you think this is an error, please contact support. Alternatively, try logging out and signing back in."
:image="SplashImage"
:image="AppLogoSplash"
>
<v-row>
<v-col class="d-flex justify-center">
@@ -48,7 +48,7 @@
</template>
<script lang="ts" setup>
import SplashImage from "@/assets/SplashImage.png"
import AppLogoSplash from "@/assets/app_logo_splash.png"
import { useAuth0 } from "@auth0/auth0-vue"
import { APPLICATION_NAME } from "@/config"
-71
View File
@@ -1,71 +0,0 @@
import { type Ref, reactive, toRefs, ref, unref, watch } from "vue"
import notificationsApi, {
NotificationSourceTypes,
type Notification,
type NotificationWhereOptions,
type NotificationFiltersOptions,
} from "@/api/notifications-api"
export {
NotificationSourceTypes,
type Notification,
type NotificationWhereOptions,
type NotificationFiltersOptions,
}
export function useNotifications(
queryOptions: Ref<{
where?: Record<string, unknown>
page?: number
perPage?: number
}> = ref({}),
{ skipWatchIf = () => false }: { skipWatchIf?: () => boolean } = {}
) {
const state = reactive<{
notifications: Notification[]
totalCount: number
isLoading: boolean
isErrored: boolean
}>({
notifications: [],
totalCount: 0,
isLoading: false,
isErrored: false,
})
async function fetch(): Promise<Notification[]> {
state.isLoading = true
try {
const { notifications, totalCount } = await notificationsApi.list(unref(queryOptions))
state.isErrored = false
state.notifications = notifications
state.totalCount = totalCount
return notifications
} catch (error) {
console.error("Failed to fetch notifications:", error)
state.isErrored = true
throw error
} finally {
state.isLoading = false
}
}
watch(
() => unref(queryOptions),
async () => {
if (skipWatchIf()) return
await fetch()
},
{ deep: true, immediate: true }
)
return {
...toRefs(state),
fetch,
refresh: fetch,
}
}
export default useNotifications
-26
View File
@@ -66,31 +66,6 @@ export function useUser(id: Ref<number | null | undefined>) {
}
}
async function directorySync() {
const staticId = unref(id)
if (isNil(staticId)) {
throw new Error("id is required")
}
if (isNil(state.user)) {
throw new Error("No user to save")
}
state.isLoading = true
try {
const { user } = await usersApi.directorySync(staticId)
state.isErrored = false
state.user = user
return user
} catch (error) {
console.error("Failed to sync user:", error)
state.isErrored = true
throw error
} finally {
state.isLoading = false
}
}
watch(
() => unref(id),
async (newId) => {
@@ -106,7 +81,6 @@ export function useUser(id: Ref<number | null | undefined>) {
fetch,
refresh: fetch,
save,
directorySync,
}
}