Type Issues Fixed and First Pass New Item Form

This commit is contained in:
Annika Merris 2024-02-13 09:13:48 -05:00
parent a1aa171bb1
commit aafd3c19de
15 changed files with 422 additions and 164 deletions

View file

@ -22,6 +22,10 @@ COPY . .
# [optional] tests & build
ENV NODE_ENV=production
# I can't run the type checking because of some sort of issue with
# bun and vue-tsc.
# see https://github.com/oven-sh/bun/issues/4754
# RUN bunx --bun vue-tsc --build --force
RUN bunx --bun vite build
# Copy the distribution folder into the final image.

BIN
bun.lockb

Binary file not shown.

View file

@ -18,7 +18,6 @@
"axios": "^1.6.5",
"axios-retry": "^4.0.0",
"pinia": "^2.1.7",
"sass": "^1.69.7",
"vue": "^3.3.11",
"vue-router": "^4.2.5",
"vuetify": "^3.4.10"
@ -43,6 +42,8 @@
"typescript": "~5.3.0",
"vite": "^5.0.10",
"vitest": "^1.0.4",
"vue-tsc": "^1.8.25"
"vue-tsc": "^1.8.25",
"sass": "^1.69.7",
"vite-plugin-vuetify": "^2.0.1"
}
}

View file

@ -0,0 +1,160 @@
<script setup lang="ts">
import { ref } from 'vue'
type EventItemListing = {
name: string
id: number
}
const firstName = ref('')
const itemType = ref(0)
const itemName = ref('')
const iconURL = ref('')
const minPower = ref(0)
const maxPower = ref(0)
const rarity = ref(0)
const origin = ref('')
const tooltip = ref('')
const isEventItem = ref(false)
const eventItems = [
{
name: 'Blessing Power',
id: 1
},
{
name: 'Intimacy Power',
id: 2
},
{
name: 'Fellow Power',
id: 3
}
]
const rarities = [
{
name: 'Green',
id: 1
},
{
name: 'Blue',
id: 2
},
{
name: 'Purple',
id: 3
},
{
name: 'Yellow',
id: 4
},
{
name: 'Red',
id: 5
}
]
const yesNo = [
{
name: 'Yes',
id: 1
},
{
name: 'No',
id: 0
}
]
// Yes, I do want any, I don't know WHAT they have typed, but this rule is only checking that they typed SOMETHING.
const requiredField = (v: any) => (v ? true : 'This field is required.')
const textRules = [requiredField]
const minPowerRules = [
requiredField,
(v: number) =>
v <= maxPower.value ? true : 'Minimum power must be less than or equal to maximum power.'
]
const maxPowerRules = [
requiredField,
(v: number) =>
v >= minPower.value ? true : 'Maximum power must be greater than or equal to minimum power.'
]
const listToProps = (item: EventItemListing) => ({ title: item.name })
</script>
<template>
<v-container>
<v-row justify="center">
<v-col lg="8" sm="12">
<v-card fluid>
<v-card-title>Add New Item</v-card-title>
<v-card-item>
<v-form @submit.prevent>
<v-sheet>
<v-container>
<v-row>
<v-select
:items="eventItems"
:item-props="listToProps"
label="Item Type"
></v-select>
</v-row>
<v-row>
<v-text-field
v-model="itemName"
:rules="textRules"
label="Item Name"
></v-text-field>
</v-row>
<v-row>
<v-text-field
v-model="iconURL"
:rules="textRules"
label="Image URL"
disabled
></v-text-field>
</v-row>
<v-row>
<v-col class="pl-0">
<v-text-field
v-model="minPower"
:rules="minPowerRules"
label="Minimum Power"
></v-text-field>
</v-col>
<v-col class="pr-0">
<v-text-field
v-model="maxPower"
:rules="maxPowerRules"
label="Maximum Power"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-select :items="rarities" :item-props="listToProps" label="Rarity"></v-select>
</v-row>
<v-row>
<v-text-field v-model="origin" :rules="textRules" label="Origin"></v-text-field>
</v-row>
<v-row>
<v-text-field
v-model="tooltip"
:rules="textRules"
label="Tooltip"
></v-text-field>
</v-row>
<v-row>
<v-select
:items="yesNo"
:item-props="listToProps"
label="Is From An Event?"
></v-select>
</v-row>
</v-container>
</v-sheet>
<v-btn type="submit" block>Submit</v-btn>
</v-form>
</v-card-item>
</v-card>
</v-col>
</v-row>
</v-container>
</template>

View file

@ -1,72 +1,82 @@
<script setup lang="ts">
import { usePowerItems } from '@/stores/powerItems'
import type { PowerItem } from '@/types/PowerItem';
import type { DataTableHeader } from '@/types/DataTableHeader'
import type { PowerItem } from '@/types/PowerItem'
import type { Ref } from 'vue'
import { computed, ref } from 'vue'
import { useDisplay } from 'vuetify'
import type { VDataTable } from 'vuetify/components'
type SortItem = { key: string, order?: boolean | 'asc' | 'desc' }
type ReadonlyHeaders = VDataTable['headers']
type SortItem = { key: string; order?: boolean | 'asc' | 'desc' }
const { lgAndUp } = useDisplay()
export interface Props {
items: Map<string, PowerItem>
minimumTotal: number
maximumTotal: number
averageTotal: number
items: Map<string, PowerItem>
minimumTotal: number
maximumTotal: number
averageTotal: number
}
const props = withDefaults(defineProps<Props>(), {
items: () => new Map<string, PowerItem>
items: () => new Map<string, PowerItem>()
})
const headers = ref([
{
title: 'Event',
align: ' d-none d-lg-table-cell start',
sortable: true,
value: '1.origin'
},
{
title: 'Name',
align: 'start',
sortable: true,
value: '1.itemName'
},
{
title: 'Min.',
align: 'end d-none d-lg-table-cell',
sortable: true,
key: '1.minItemPower'
},
{
title: 'Max.',
align: 'end d-none d-lg-table-cell',
sortable: true,
key: '1.maxItemPower'
},
{
title: 'Owned',
align: 'end',
sortable: true,
key: `1.owned`
},
{
title: 'Min. Total',
align: 'end d-none d-lg-table-cell',
sortable: false,
key: `1.minTotalPower`
},
{
title: 'Max. Total',
align: 'end d-none d-lg-table-cell',
sortable: false,
key: `1.maxTotalPower`
},
{
title: 'Mean Total',
align: 'end',
sortable: false,
key: `1.aveTotalPower`
const computedHeaders = computed((): ReadonlyHeaders => {
let initialHeaders: DataTableHeader[] = [
{
title: 'Name',
align: 'start',
sortable: true,
value: '1.itemName'
},
{
title: 'Owned',
align: 'end',
sortable: true,
key: `1.owned`
},
{
title: 'Mean Total',
align: 'end',
sortable: false,
key: `1.aveTotalPower`
}
]
if (lgAndUp.value) {
initialHeaders!.unshift({
title: 'Event',
align: 'start',
sortable: true,
value: '1.origin'
})
initialHeaders.splice(2, 0, {
title: 'Min.',
align: 'end',
sortable: true,
key: '1.minItemPower'
})
initialHeaders.splice(3, 0, {
title: 'Max.',
align: 'end',
sortable: true,
key: '1.maxItemPower'
})
initialHeaders.splice(5, 0, {
title: 'Min. Total',
align: 'end',
sortable: false,
key: `1.minTotalPower`
})
initialHeaders.splice(6, 0, {
title: 'Max. Total',
align: 'end',
sortable: false,
key: `1.maxTotalPower`
})
}
])
return initialHeaders as ReadonlyHeaders
})
const sortBy: Ref<SortItem[]> = ref([
{
key: '1.minItemPower',
@ -101,7 +111,7 @@ const filteredItems = computed(() =>
? value
: undefined
)
.filter((value) => value !== undefined)
.filter((value): value is [string, PowerItem] => !!value)
)
const getColor = computed(() => (rarity: number): string => {
if (rarity === 5) {
@ -151,9 +161,9 @@ toggle()
density="compact"
v-model:sort-by="sortBy"
:items="filteredItems"
:headers="headers"
:headers="computedHeaders"
>
<template v-slot:item.1.origin="{ item }">
<template v-slot:[`item.1.origin`]="{ item }">
<v-card
class="my-2"
elevation="2"
@ -163,10 +173,10 @@ toggle()
color="transparent"
>
<v-img :src="getEventImageUrl(item[1].origin)"></v-img>
<v-tooltip activator="parent">{{ item?.[1].origin }}</v-tooltip>
</v-card>
<v-tooltip activator="parent">{{ item[1].origin }}</v-tooltip> </v-card
>
</template>
<template v-slot:item.1.itemName="{ item }">
<template v-slot:[`item.1.itemName`]="{ item }">
<v-tooltip
activator="parent"
v-if="item[1].tooltip != undefined"
@ -183,7 +193,7 @@ toggle()
{{ item[1].itemName }}
</v-chip>
</template>
<template v-slot:item.1.owned="{ item }">
<template v-slot:[`item.1.owned`]="{ item }">
<v-text-field
density="compact"
hide-details="auto"
@ -191,13 +201,13 @@ toggle()
@update:model-value="usePowerItems().updateOwned(item[0], item[1].owned)"
></v-text-field>
</template>
<template v-slot:item.1.minTotalPower="{ item }">
<template v-slot:[`item.1.minTotalPower`]="{ item }">
{{ item[1].minItemPower * item[1].owned }}
</template>
<template v-slot:item.1.maxTotalPower="{ item }">
<template v-slot:[`item.1.maxTotalPower`]="{ item }">
{{ item[1].maxItemPower * item[1].owned }}
</template>
<template v-slot:item.1.aveTotalPower="{ item }">
<template v-slot:[`item.1.aveTotalPower`]="{ item }">
{{ ((item[1].minItemPower + item[1].maxItemPower) / 2) * item[1].owned }}
</template>
<template v-slot:tfoot>

View file

@ -1,7 +1,14 @@
<script setup lang="ts">
import { usePowerItems } from '@/stores/powerItems'
import type { PowerItem } from '@/types/PowerItem'
import type { Ref } from 'vue';
import { computed, ref } from 'vue'
import { useDisplay } from 'vuetify';
import type { VDataTable } from 'vuetify/components';
type ReadonlyHeaders = VDataTable['headers']
type SortItem = { key: string; order?: boolean | 'asc' | 'desc' }
const { lgAndUp } = useDisplay()
export interface Props {
items: Map<string, PowerItem>
@ -12,39 +19,46 @@ const props = withDefaults(defineProps<Props>(), {
items: () => new Map<string, PowerItem>()
})
const headers = ref([
{
title: '',
align: 'start',
sortable: false,
value: '1.iconURL'
},
{
title: 'Name',
align: ' d-none d-lg-table-cell start',
sortable: true,
value: '1.itemName'
},
{
title: 'Power',
align: ' d-none d-lg-table-cell',
sortable: true,
key: '1.minItemPower'
},
{
title: 'Owned',
align: 'end',
sortable: true,
key: `1.owned`
},
{
title: 'Total',
align: 'end',
sortable: false,
key: `1.totalPower`
const headers = computed((): ReadonlyHeaders => {
const initialHeaders = [
{
title: '',
align: 'start',
sortable: false,
value: '1.iconURL'
},
{
title: 'Owned',
align: 'end',
sortable: true,
key: `1.owned`
},
{
title: 'Total',
align: 'end',
sortable: false,
key: `1.totalPower`
}
]
if (lgAndUp.value) {
initialHeaders!.splice(1, 0,
{
title: 'Name',
align: 'start',
sortable: true,
value: '1.itemName'
})
initialHeaders!.splice(2, 0,
{
title: 'Power',
align: 'end',
sortable: true,
key: '1.minItemPower'
})
}
])
const sortBy = ref([
return initialHeaders as ReadonlyHeaders
})
const sortBy: Ref<SortItem[]> = ref([
{
key: '1.minItemPower',
order: 'asc'
@ -77,7 +91,7 @@ const getColor = computed(() => (rarity: number): string => {
:items="[...items.entries()]"
:headers="headers"
>
<template v-slot:item.1.iconURL="{ item }">
<template v-slot:[`item.1.iconURL`]="{ item }">
<v-card class="my-2" elevation="2" rounded width="41">
<v-tooltip
activator="parent"
@ -94,7 +108,7 @@ const getColor = computed(() => (rarity: number): string => {
<v-img :src="`/images/${item[1].iconURL}`" height="41" width="41" cover></v-img>
</v-card>
</template>
<template v-slot:item.1.itemName="{ item }">
<template v-slot:[`item.1.itemName`]="{ item }">
<v-tooltip
activator="parent"
v-if="item[1].tooltip != undefined"
@ -111,7 +125,7 @@ const getColor = computed(() => (rarity: number): string => {
{{ item[1].itemName }}
</v-chip>
</template>
<template v-slot:item.1.owned="{ item }">
<template v-slot:[`item.1.owned`]="{ item }">
<v-text-field
density="compact"
hide-details="auto"
@ -119,7 +133,7 @@ const getColor = computed(() => (rarity: number): string => {
@update:model-value="usePowerItems().updateOwned(item[0], item[1].owned)"
></v-text-field>
</template>
<template v-slot:item.1.totalPower="{ item }">
<template v-slot:[`item.1.totalPower`]="{ item }">
{{ item[1].minItemPower * item[1].owned }}
</template>
<template v-slot:tfoot>

View file

@ -10,15 +10,11 @@ import router from './router'
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import axios from 'axios'
import zitadelAuth from '@/services/zitadelAuth'
import { apiBaseURL } from './types/ConfigSymbols'
const vuetify = createVuetify({
components,
directives,
icons: {
defaultSet: 'mdi'
},

View file

@ -27,7 +27,7 @@ const router = createRouter({
{
path: '/test',
name: 'test',
component: () => import('@/views/Test.vue')
component: () => import('@/views/TestView.vue')
},
{

View file

@ -0,0 +1,110 @@
import type { ComputedRef } from "vue"
import type { Ref } from "vue"
export type DataTableHeader = {
key?: 'data-table-group' | 'data-table-select' | 'data-table-expand' | (string & {})
value?: SelectItemKey
title?: string
fixed?: boolean
align?: 'start' | 'end' | 'center'
width?: number | string
minWidth?: string
maxWidth?: string
headerProps?: Record<string, any>
cellProps?: HeaderCellProps
sortable?: boolean
sort?: DataTableCompareFunction
filter?: FilterFunction
children?: DataTableHeader[]
}
type SelectItemKey<T = Record<string, any>> = boolean | null | undefined | string | readonly (string | number)[] | ((item: T, fallback?: any) => any);
type HeaderCellProps = Record<string, any> | ((data: Pick<ItemKeySlot<any>, 'index' | 'item' | 'internalItem' | 'value'>) => Record<string, any>);
type DataTableCompareFunction<T = any> = (a: T, b: T) => number;
type FilterFunction = (value: string, query: string, item?: InternalItem) => FilterMatch;
type ItemKeySlot<T> = ItemSlotBase<T> & {
value: any;
column: InternalDataTableHeader;
};
type FilterMatch = boolean | number | [number, number] | [number, number][];
type ItemSlotBase<T> = {
index: number;
item: T;
internalItem: DataTableItem<T>;
isExpanded: ReturnType<typeof provideExpanded>['isExpanded'];
toggleExpand: ReturnType<typeof provideExpanded>['toggleExpand'];
isSelected: ReturnType<typeof provideSelection>['isSelected'];
toggleSelect: ReturnType<typeof provideSelection>['toggleSelect'];
};
type InternalDataTableHeader = Omit<DataTableHeader, 'key' | 'value' | 'children'> & {
key: string | null;
value: SelectItemKey | null;
sortable: boolean;
fixedOffset?: number;
lastFixed?: boolean;
colspan?: number;
rowspan?: number;
children?: InternalDataTableHeader[];
};
type ExpandProps = {
expandOnClick: boolean;
expanded: readonly string[];
'onUpdate:expanded': ((value: any[]) => void) | undefined;
};
type SelectionProps = Pick<DataTableItemProps, 'itemValue'> & {
modelValue: readonly any[];
selectStrategy: 'single' | 'page' | 'all';
valueComparator: typeof deepEqual;
'onUpdate:modelValue': EventProp<[any[]]> | undefined;
};
type EventProp<T extends any[] = any[], F = (...args: T) => void> = F;
declare function provideExpanded(props: ExpandProps): {
expand: (item: DataTableItem, value: boolean) => void;
expanded: Ref<Set<string>> & {
readonly externalValue: readonly string[];
};
expandOnClick: Ref<boolean>;
isExpanded: (item: DataTableItem) => boolean;
toggleExpand: (item: DataTableItem) => void;
};
declare function provideSelection(props: SelectionProps, { allItems, currentPage }: {
allItems: Ref<SelectableItem[]>;
currentPage: Ref<SelectableItem[]>;
}): {
toggleSelect: (item: SelectableItem) => void;
select: (items: SelectableItem[], value: boolean) => void;
selectAll: (value: boolean) => void;
isSelected: (items: SelectableItem | SelectableItem[]) => boolean;
isSomeSelected: (items: SelectableItem | SelectableItem[]) => boolean;
someSelected: ComputedRef<boolean>;
allSelected: ComputedRef<boolean>;
showSelectAll: boolean;
};
declare function deepEqual(a: any, b: any): boolean;
interface InternalItem<T = any> {
value: any;
raw: T;
}
interface DataTableItemProps {
items: any[];
itemValue: SelectItemKey;
itemSelectable: SelectItemKey;
returnObject: boolean;
}
interface DataTableItem<T = any> extends InternalItem<T>, GroupableItem<T>, SelectableItem {
key: any;
index: number;
columns: {
[key: string]: any;
};
}
interface SelectableItem {
value: any;
selectable: boolean;
}
interface GroupableItem<T = any> {
type: 'item';
raw: T;
}

View file

@ -16,7 +16,7 @@ const {
</script>
<template>
<v-card>
<v-card class="flex-fill">
<v-card-title>Blessing Power</v-card-title>
<v-card-text>
<v-sheet class="d-flex flex-wrap flex-fill">

View file

@ -16,7 +16,7 @@ const {
</script>
<template>
<v-card>
<v-card class="flex-fill">
<v-card-title>Fellow Power</v-card-title>
<v-card-text>
<v-sheet class="d-flex flex-wrap flex-fill">

View file

@ -16,7 +16,7 @@ const {
</script>
<template>
<v-card>
<v-card class="flex-fill">
<v-card-title>Intimacy Power</v-card-title>
<v-card-text>
<v-sheet class="d-flex flex-wrap flex-fill">

View file

@ -1,52 +0,0 @@
<script setup lang="ts">
import SpecialItemsCard from '@/components/SpecialItemsCard.vue'
import StandardItemsCard from '@/components/StandardItemsCard.vue'
import SummaryCard from '@/components/SummaryCard.vue'
import { storeToRefs } from 'pinia'
import { usePowerItems } from '@/stores/powerItems'
const {
standardBlessingItems,
standardBlessingItemTotal,
specialBlessingItems,
specialBlessingItemsMinTotal,
specialBlessingItemsMaxTotal,
specialBlessingItemsAveTotal
} = storeToRefs(usePowerItems())
</script>
<template>
<v-card>
<v-card-title>Intimacy Power</v-card-title>
<v-card-text>
<v-sheet class="d-flex flex-wrap flex-fill">
<StandardItemsCard
class="ma-2 align-self-start"
:items="standardBlessingItems"
:total="standardBlessingItemTotal"
/>
<SpecialItemsCard
class="ma-2 align-self-start"
:items="specialBlessingItems"
:minimum-total="specialBlessingItemsMinTotal"
:maximum-total="specialBlessingItemsMaxTotal"
:average-total="specialBlessingItemsAveTotal"
/>
<SummaryCard
class="ma-2 align-self-start"
:standard-total="standardBlessingItemTotal"
:minimum-total="specialBlessingItemsMinTotal"
:maximum-total="specialBlessingItemsMaxTotal"
:average-total="specialBlessingItemsAveTotal"
/>
</v-sheet>
</v-card-text>
</v-card>
</template>
<style lang="scss" scoped>
@use '@/styles/settings.scss';
:deep(tbody) tr:nth-of-type(even) {
background-color: rgba(var(--v-theme-primary-darken-1), 0.25);
}
</style>

13
src/views/TestView.vue Normal file
View file

@ -0,0 +1,13 @@
<script setup lang="ts">
import NewItemForm from '@/components/NewItemForm.vue';
</script>
<template>
<v-sheet class="flex-fill">
<NewItemForm />
</v-sheet>
</template>
<style lang="scss" scoped>
</style>

View file

@ -2,11 +2,13 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vuetify from 'vite-plugin-vuetify'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vuetify(),
],
resolve: {
alias: {