additions

This commit is contained in:
Evert Prants 2023-01-28 09:30:11 +02:00
parent 74ef2848e2
commit b875c2465d
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
14 changed files with 276 additions and 147 deletions

View File

@ -0,0 +1,38 @@
<template>
<RouterLink :to="routeLink">
<div
class="hoverbox relative flex h-60 flex-col overflow-hidden rounded-lg bg-gray-50 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 hover:scale-105 md:h-80"
>
<img v-if="image" :src="image" class="absolute object-cover" />
<div class="z-10 mt-auto flex flex-col bg-white px-4 py-4">
<span class="text-lg font-bold">{{ title }}</span>
<span class="text-sm" v-if="subtitle">{{ subtitle }}</span>
</div>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { RouteLocationRaw } from 'vue-router';
const props = defineProps<{
image?: string;
title: string;
routeLink: RouteLocationRaw;
subtitle?: string;
}>();
</script>
<style scoped lang="scss">
.hoverbox {
img {
transition: all 3s;
}
&:hover {
img {
transform: scale(1.3);
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="mb-8 flex items-center justify-between border-b-2 border-gray-100 pb-2" class="mb-2 flex items-center justify-between border-b-2 border-gray-100 pb-2"
> >
<slot /> <slot />
<DropdownButton title="Actions" :position="'right'" v-if="withActions"> <DropdownButton title="Actions" :position="'right'" v-if="withActions">

View File

@ -133,7 +133,8 @@ const inputClass = computed(() => {
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500', : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '', props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`, `block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1', 'py-2 pl-3 pr-10 text-left cursor-default border-[1px] focus:ring-1',
'sm:min-h-[38px] min-h-[42px]',
]; ];
}); });

View File

@ -109,7 +109,8 @@ const inputClass = computed(() => {
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500', : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '', props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`, `block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1', 'py-2 pl-3 pr-10 text-left cursor-default border-[1px] focus:ring-1',
'sm:min-h-[38px] min-h-[42px]',
]; ];
}); });

View File

@ -25,30 +25,25 @@
label="Type" label="Type"
/> />
<FormField name="barcode" label="Barcode" /> <FormField name="barcode" label="Barcode" />
<FormField name="url" label="URL" />
<div class="grid space-y-2 sm:grid-cols-3 sm:space-y-0 sm:space-x-2">
<FormField <FormField
name="weight" name="weight"
type="number" type="number"
step="0.001" step="0.001"
label="Weight (g)" label="Weight (g)"
/> />
<FormField name="url" label="URL" />
<FormField <FormField
name="consumable" name="consumable"
type="checkbox" type="checkbox"
label="Consumable / Food item" label="Consumable / Food item"
/> />
<FormField name="public" type="checkbox" label="Public item" /> <FormField name="public" type="checkbox" label="Public item" />
</div>
<FormTextareaField name="notes" label="Notes" /> <FormTextareaField name="notes" label="Notes" />
</template> </template>
<FormGroup name="transactionInfo"> <FormGroup name="transactionInfo">
<FormSelectField
:options="itemTransactionTypesOptions"
name="type"
required
label="Transaction type"
:disabled="!item"
/>
<FormDateField <FormDateField
name="actionAt" name="actionAt"
label="Action committed at" label="Action committed at"
@ -56,13 +51,13 @@
required required
/> />
<div class="grid grid-cols-4 space-x-2"> <div class="grid grid-cols-2 space-x-2 sm:grid-cols-4">
<FormField <FormField
name="price" name="price"
label="Cost" label="Cost"
type="number" type="number"
step=".01" step=".01"
class="col-span-3" class="sm:col-span-3"
:disabled="!item" :disabled="!item"
/> />
<FormSelectField <FormSelectField
@ -81,7 +76,7 @@
</FormGroup> </FormGroup>
<FormGroup name="additionalInfo"> <FormGroup name="additionalInfo">
<div class="grid grid-cols-3 space-x-2"> <div class="grid space-y-2 sm:grid-cols-3 sm:space-y-0 sm:space-x-2">
<FormDateField <FormDateField
name="expiresAt" name="expiresAt"
label="Expires at" label="Expires at"
@ -147,6 +142,7 @@ import FormTextareaField from '../form/fields/FormTextareaField.vue';
const defaults: any = { const defaults: any = {
item: null, item: null,
transactionInfo: { transactionInfo: {
type: TransactionType.ACQUIRED,
actionAt: new Date(), actionAt: new Date(),
}, },
additionalInfo: { additionalInfo: {
@ -181,20 +177,6 @@ const itemTypesOptions = computed(() => {
); );
}); });
const itemTransactionTypesOptions = computed(() => {
return Object.keys(TransactionTypeName).reduce<SelectOption[]>(
(list, key) => [
...list,
{
value: key.toString(),
name: TransactionTypeName[key as TransactionType],
description: TransactionTypeDescription[key as TransactionType],
},
],
[]
);
});
const validators = ref<FormValidator[]>([ const validators = ref<FormValidator[]>([
{ {
field: 'displayName', field: 'displayName',
@ -204,10 +186,6 @@ const validators = ref<FormValidator[]>([
field: 'type', field: 'type',
validators: [IsRequired()], validators: [IsRequired()],
}, },
{
field: 'transactionInfo.type',
validators: [IsRequired()],
},
{ {
field: 'transactionInfo.actionAt', field: 'transactionInfo.actionAt',
validators: [IsRequired()], validators: [IsRequired()],
@ -227,8 +205,16 @@ const currencies = [
const searchForItems = async (search: string) => { const searchForItems = async (search: string) => {
if (!search || search.length < 3) return []; if (!search || search.length < 3) return [];
const query = new URLSearchParams();
if (search.startsWith('b:') && search.length > 3) {
query.append('barcode', search.substring(2));
} else {
query.append('searchTerm', search);
}
const { data: list } = await jfetch( const { data: list } = await jfetch(
`${BACKEND_URL}/storage/item?searchTerm=${search}`, `${BACKEND_URL}/storage/item?${query.toString()}`,
{ {
headers: authHeader.value, headers: authHeader.value,
} }
@ -261,7 +247,11 @@ watch(
const startAddingNewItem = (insert: string) => { const startAddingNewItem = (insert: string) => {
item.value = insert; item.value = insert;
if (insert.startsWith('b:')) {
data.value['barcode'] = insert.substring(2);
} else {
data.value['displayName'] = insert; data.value['displayName'] = insert;
}
}; };
const onSubmit = async (form: FormSubmit) => { const onSubmit = async (form: FormSubmit) => {

View File

@ -20,7 +20,9 @@
<span :class="`text-${size} font-bold`">{{ <span :class="`text-${size} font-bold`">{{
storedItem.item.displayName storedItem.item.displayName
}}</span> }}</span>
<span :class="`mt-0.5 ${fontSize} text-gray-500`" <span
:class="`mt-0.5 ${fontSize} text-gray-500`"
v-if="storedItem.addedBy"
>· Added >· Added
{{ {{
dateToLocaleString(storedItem.acquiredAt || storedItem.createdAt) dateToLocaleString(storedItem.acquiredAt || storedItem.createdAt)

View File

@ -7,6 +7,7 @@ import BuildingView from '../views/building/BuildingView.vue';
import FloorView from '../views/building/floors/FloorView.vue'; import FloorView from '../views/building/floors/FloorView.vue';
import { createRouter, createWebHashHistory } from 'vue-router'; import { createRouter, createWebHashHistory } from 'vue-router';
import { useUserStore } from '../store/user.store'; import { useUserStore } from '../store/user.store';
import Demo from '../views/Demo.vue';
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
@ -24,6 +25,11 @@ const routes: RouteRecordRaw[] = [
path: '/planner', path: '/planner',
component: HousePlanner, component: HousePlanner,
}, },
{
name: 'demo',
path: '/demo',
component: Demo,
},
{ {
name: 'buildings', name: 'buildings',
path: '/building/:id', path: '/building/:id',

View File

@ -14,7 +14,9 @@ const { authHeader } = useAccessToken();
export const useBuildingStore = defineStore('building', { export const useBuildingStore = defineStore('building', {
state: () => { state: () => {
return { return {
loadingBuildings: false,
buildings: [] as BuildingListItem[], buildings: [] as BuildingListItem[],
loadingFloors: false,
floors: [] as FloorListItem[], floors: [] as FloorListItem[],
building: null as null | BuildingResponse, building: null as null | BuildingResponse,
}; };
@ -30,12 +32,19 @@ export const useBuildingStore = defineStore('building', {
this.building = building; this.building = building;
}, },
async getBuildings() { async getBuildings() {
this.loadingBuildings = true;
try {
const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, { const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, {
headers: authHeader.value, headers: authHeader.value,
}); });
this.buildings = buildings; this.buildings = buildings;
} finally {
this.loadingBuildings = false;
}
}, },
async getFloors(building: number) { async getFloors(building: number) {
this.loadingFloors = true;
try {
const { data: floors } = await jfetch( const { data: floors } = await jfetch(
`${BACKEND_URL}/buildings/${building}/floors`, `${BACKEND_URL}/buildings/${building}/floors`,
{ {
@ -43,6 +52,9 @@ export const useBuildingStore = defineStore('building', {
} }
); );
this.floors = floors; this.floors = floors;
} finally {
this.loadingFloors = false;
}
}, },
async saveFloor( async saveFloor(
building: number, building: number,

View File

@ -0,0 +1,32 @@
import { defineStore } from 'pinia';
import { useAccessToken } from '../composables/useAccessToken';
import { BACKEND_URL } from '../constants';
import { StoredItem } from '../interfaces/storage.interfaces';
import jfetch from '../utils/jfetch';
const { authHeader } = useAccessToken();
export const useStorageStore = defineStore('storage', {
state: () => {
return {
loadingExpiringItems: false,
expiringItems: [] as StoredItem[],
};
},
actions: {
async getExpiringItems() {
this.loadingExpiringItems = true;
this.expiringItems = [];
try {
const { data: items } = await jfetch(
`${BACKEND_URL}/storage/expiring`,
{
headers: authHeader.value,
}
);
this.expiringItems = items;
} finally {
this.loadingExpiringItems = false;
}
},
},
});

View File

@ -1,73 +1,57 @@
<template> <template>
<StandardLayout> <StandardLayout>
<h1>Dashboard</h1> <PageHead><h1 class="text-2xl font-bold">Dashboard</h1></PageHead>
<p>Hello, {{ user.name }}</p> <p>Hello, {{ user.name }}</p>
<Form @submit="test" :validators="validators"> <template v-if="expiringItems?.length">
<FormDateField <PageHead class="mt-4"
name="date" ><h2 class="text-xl font-bold">Expiring soon</h2></PageHead
label="datepicker" >
locale="en-UK"
clearable <div
:enable-time-picker="false" class="flex flex-nowrap gap-1 overflow-x-scroll px-1 py-1 md:overflow-hidden"
>
<StoredItemCard
v-for="item of expiringItems"
:stored-item="item"
class="whitespace-nowrap rounded-md bg-white px-2 py-2 shadow-md ring-1 ring-black ring-opacity-5"
/> />
<FormField name="test" label="test" /> </div>
<FormAutocompleteField </template>
:search-fn="searchFn"
name="autocomplete" <PageHead class="mt-4"
label="autocomplete" ><h2 class="text-xl font-bold">Buildings</h2></PageHead
/> >
<FormTextareaField name="textarea" label="textarea" rows="3" />
<button type="submit">test</button> <div
</Form> class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
v-if="buildings?.length"
>
<BuildingListItem v-for="building of buildings" :building="building" />
</div>
</StandardLayout> </StandardLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useUserStore } from '../store/user.store'; import { useUserStore } from '../store/user.store';
import StandardLayout from '../components/StandardLayout.vue'; import StandardLayout from '../components/StandardLayout.vue';
import FormDateField from '../components/form/fields/FormDateField.vue'; import PageHead from '../components/PageHead.vue';
import Form from '../components/form/Form.vue'; import { useBuildingStore } from '../store/building.store';
import FormField from '../components/form/FormField.vue'; import { storeToRefs } from 'pinia';
import { IsRequired } from '../components/form/validators'; import BuildingListItem from './building/BuildingListItem.vue';
import FormAutocompleteField from '../components/form/fields/FormAutocompleteField.vue'; import { useStorageStore } from '../store/storage.store';
import FormTextareaField from '../components/form/fields/FormTextareaField.vue'; import StoredItemCard from '../components/item/StoredItemCard.vue';
const userStore = useUserStore(); const userStore = useUserStore();
const storageStore = useStorageStore();
const buildingStore = useBuildingStore();
const { buildings, loadingBuildings } = storeToRefs(buildingStore);
const { expiringItems, loadingExpiringItems } = storeToRefs(storageStore);
const user = ref(userStore.user); const user = ref(userStore.user);
const test = console.log; onMounted(() => {
const validators = [{ field: 'date', validators: [IsRequired()] }]; storageStore.getExpiringItems();
});
const autocompleteTest = [
{
id: 1,
displayName: 'search things',
},
{
id: 2,
displayName: 'potato',
},
{
id: 3,
displayName: 'carrot',
},
{
id: 4,
displayName: 'beet',
},
];
const searchFn = async (query: string) => {
await new Promise((resolve) => setTimeout(resolve, 800));
return query === ''
? autocompleteTest
: autocompleteTest.filter((person) =>
person.displayName
.toLowerCase()
.replace(/\s+/g, '')
.includes(query.toLowerCase().replace(/\s+/g, ''))
);
};
</script> </script>

65
src/views/Demo.vue Normal file
View File

@ -0,0 +1,65 @@
<template>
<StandardLayout>
<Form @submit="test" :validators="validators">
<FormDateField
name="date"
label="datepicker"
locale="en-UK"
clearable
:enable-time-picker="false"
/>
<FormField name="test" label="test" />
<FormAutocompleteField
:search-fn="searchFn"
name="autocomplete"
label="autocomplete"
/>
<FormTextareaField name="textarea" label="textarea" rows="3" />
<button type="submit">test</button>
</Form>
</StandardLayout>
</template>
<script setup lang="ts">
import StandardLayout from '../components/StandardLayout.vue';
import { IsRequired } from '../components/form/validators';
import FormDateField from '../components/form/fields/FormDateField.vue';
import Form from '../components/form/Form.vue';
import FormField from '../components/form/FormField.vue';
import FormAutocompleteField from '../components/form/fields/FormAutocompleteField.vue';
import FormTextareaField from '../components/form/fields/FormTextareaField.vue';
const test = console.log;
const validators = [{ field: 'date', validators: [IsRequired()] }];
const autocompleteTest = [
{
id: 1,
displayName: 'search things',
},
{
id: 2,
displayName: 'potato',
},
{
id: 3,
displayName: 'carrot',
},
{
id: 4,
displayName: 'beet',
},
];
const searchFn = async (query: string) => {
await new Promise((resolve) => setTimeout(resolve, 800));
return query === ''
? autocompleteTest
: autocompleteTest.filter((person) =>
person.displayName
.toLowerCase()
.replace(/\s+/g, '')
.includes(query.toLowerCase().replace(/\s+/g, ''))
);
};
</script>

View File

@ -0,0 +1,16 @@
<template>
<ImageBox
:title="building.displayName"
:subtitle="building.address"
:route-link="{ name: 'building', params: { id: building.id } }"
/>
</template>
<script setup lang="ts">
import ImageBox from '../../components/ImageBox.vue';
import { BuildingListItem } from '../../interfaces/building.interfaces';
const props = defineProps<{
building: BuildingListItem;
}>();
</script>

View File

@ -23,7 +23,10 @@
<h2 class="text-xl font-bold">Floors</h2> <h2 class="text-xl font-bold">Floors</h2>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3" v-if="building"> <div
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
v-if="building"
>
<FloorListItem <FloorListItem
:building="building!.id" :building="building!.id"
v-for="floor of building?.floors || []" v-for="floor of building?.floors || []"

View File

@ -1,24 +1,17 @@
<template> <template>
<RouterLink <ImageBox
:to="{ name: 'floor', params: { id: building, number: floor.number } }" :title="floor.displayName"
> :subtitle="`Floor ${floor.number}`"
<div :route-link="{
class="hoverbox relative flex h-60 flex-col overflow-hidden rounded-lg bg-gray-50 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 hover:scale-105 md:h-80" name: 'floor',
> params: { id: building, number: floor.number },
<img }"
:src="`${BACKEND_URL}/usercontent/${floor.planImage}`" :image="floor.planImage && `${BACKEND_URL}/usercontent/${floor.planImage}`"
v-if="floor.planImage"
class="absolute object-cover"
/> />
<div class="z-10 mt-auto flex flex-col bg-white px-4 py-4">
<span class="text-lg font-bold">{{ floor.displayName }}</span>
<span class="text-sm">Floor {{ floor.number }}</span>
</div>
</div>
</RouterLink>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import ImageBox from '../../../components/ImageBox.vue';
import { BACKEND_URL } from '../../../constants'; import { BACKEND_URL } from '../../../constants';
import { FloorListItem } from '../../../interfaces/floor.interfaces'; import { FloorListItem } from '../../../interfaces/floor.interfaces';
@ -27,17 +20,3 @@ const props = defineProps<{
floor: FloorListItem; floor: FloorListItem;
}>(); }>();
</script> </script>
<style scoped lang="scss">
.hoverbox {
img {
transition: all 3s;
}
&:hover {
img {
transform: scale(1.3);
}
}
}
</style>