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>
<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 />
<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',
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
`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',
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
`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"
/>
<FormField name="barcode" label="Barcode" />
<FormField
name="weight"
type="number"
step="0.001"
label="Weight (g)"
/>
<FormField name="url" label="URL" />
<FormField
name="consumable"
type="checkbox"
label="Consumable / Food item"
/>
<FormField name="public" type="checkbox" label="Public item" />
<div class="grid space-y-2 sm:grid-cols-3 sm:space-y-0 sm:space-x-2">
<FormField
name="weight"
type="number"
step="0.001"
label="Weight (g)"
/>
<FormField
name="consumable"
type="checkbox"
label="Consumable / Food item"
/>
<FormField name="public" type="checkbox" label="Public item" />
</div>
<FormTextareaField name="notes" label="Notes" />
</template>
<FormGroup name="transactionInfo">
<FormSelectField
:options="itemTransactionTypesOptions"
name="type"
required
label="Transaction type"
:disabled="!item"
/>
<FormDateField
name="actionAt"
label="Action committed at"
@ -56,13 +51,13 @@
required
/>
<div class="grid grid-cols-4 space-x-2">
<div class="grid grid-cols-2 space-x-2 sm:grid-cols-4">
<FormField
name="price"
label="Cost"
type="number"
step=".01"
class="col-span-3"
class="sm:col-span-3"
:disabled="!item"
/>
<FormSelectField
@ -81,7 +76,7 @@
</FormGroup>
<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
name="expiresAt"
label="Expires at"
@ -147,6 +142,7 @@ import FormTextareaField from '../form/fields/FormTextareaField.vue';
const defaults: any = {
item: null,
transactionInfo: {
type: TransactionType.ACQUIRED,
actionAt: new Date(),
},
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[]>([
{
field: 'displayName',
@ -204,10 +186,6 @@ const validators = ref<FormValidator[]>([
field: 'type',
validators: [IsRequired()],
},
{
field: 'transactionInfo.type',
validators: [IsRequired()],
},
{
field: 'transactionInfo.actionAt',
validators: [IsRequired()],
@ -227,8 +205,16 @@ const currencies = [
const searchForItems = async (search: string) => {
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(
`${BACKEND_URL}/storage/item?searchTerm=${search}`,
`${BACKEND_URL}/storage/item?${query.toString()}`,
{
headers: authHeader.value,
}
@ -261,7 +247,11 @@ watch(
const startAddingNewItem = (insert: string) => {
item.value = insert;
data.value['displayName'] = insert;
if (insert.startsWith('b:')) {
data.value['barcode'] = insert.substring(2);
} else {
data.value['displayName'] = insert;
}
};
const onSubmit = async (form: FormSubmit) => {

View File

@ -20,7 +20,9 @@
<span :class="`text-${size} font-bold`">{{
storedItem.item.displayName
}}</span>
<span :class="`mt-0.5 ${fontSize} text-gray-500`"
<span
:class="`mt-0.5 ${fontSize} text-gray-500`"
v-if="storedItem.addedBy"
>· Added
{{
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 { createRouter, createWebHashHistory } from 'vue-router';
import { useUserStore } from '../store/user.store';
import Demo from '../views/Demo.vue';
const routes: RouteRecordRaw[] = [
{
@ -24,6 +25,11 @@ const routes: RouteRecordRaw[] = [
path: '/planner',
component: HousePlanner,
},
{
name: 'demo',
path: '/demo',
component: Demo,
},
{
name: 'buildings',
path: '/building/:id',

View File

@ -14,7 +14,9 @@ const { authHeader } = useAccessToken();
export const useBuildingStore = defineStore('building', {
state: () => {
return {
loadingBuildings: false,
buildings: [] as BuildingListItem[],
loadingFloors: false,
floors: [] as FloorListItem[],
building: null as null | BuildingResponse,
};
@ -30,19 +32,29 @@ export const useBuildingStore = defineStore('building', {
this.building = building;
},
async getBuildings() {
const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, {
headers: authHeader.value,
});
this.buildings = buildings;
this.loadingBuildings = true;
try {
const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, {
headers: authHeader.value,
});
this.buildings = buildings;
} finally {
this.loadingBuildings = false;
}
},
async getFloors(building: number) {
const { data: floors } = await jfetch(
`${BACKEND_URL}/buildings/${building}/floors`,
{
headers: authHeader.value,
}
);
this.floors = floors;
this.loadingFloors = true;
try {
const { data: floors } = await jfetch(
`${BACKEND_URL}/buildings/${building}/floors`,
{
headers: authHeader.value,
}
);
this.floors = floors;
} finally {
this.loadingFloors = false;
}
},
async saveFloor(
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>
<StandardLayout>
<h1>Dashboard</h1>
<PageHead><h1 class="text-2xl font-bold">Dashboard</h1></PageHead>
<p>Hello, {{ user.name }}</p>
<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>
<template v-if="expiringItems?.length">
<PageHead class="mt-4"
><h2 class="text-xl font-bold">Expiring soon</h2></PageHead
>
<div
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"
/>
</div>
</template>
<PageHead class="mt-4"
><h2 class="text-xl font-bold">Buildings</h2></PageHead
>
<div
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>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import { useUserStore } from '../store/user.store';
import StandardLayout from '../components/StandardLayout.vue';
import FormDateField from '../components/form/fields/FormDateField.vue';
import Form from '../components/form/Form.vue';
import FormField from '../components/form/FormField.vue';
import { IsRequired } from '../components/form/validators';
import FormAutocompleteField from '../components/form/fields/FormAutocompleteField.vue';
import FormTextareaField from '../components/form/fields/FormTextareaField.vue';
import PageHead from '../components/PageHead.vue';
import { useBuildingStore } from '../store/building.store';
import { storeToRefs } from 'pinia';
import BuildingListItem from './building/BuildingListItem.vue';
import { useStorageStore } from '../store/storage.store';
import StoredItemCard from '../components/item/StoredItemCard.vue';
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 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, ''))
);
};
onMounted(() => {
storageStore.getExpiringItems();
});
</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>
</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
:building="building!.id"
v-for="floor of building?.floors || []"

View File

@ -1,24 +1,17 @@
<template>
<RouterLink
:to="{ name: 'floor', params: { id: building, number: floor.number } }"
>
<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
:src="`${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>
<ImageBox
:title="floor.displayName"
:subtitle="`Floor ${floor.number}`"
:route-link="{
name: 'floor',
params: { id: building, number: floor.number },
}"
:image="floor.planImage && `${BACKEND_URL}/usercontent/${floor.planImage}`"
/>
</template>
<script setup lang="ts">
import ImageBox from '../../../components/ImageBox.vue';
import { BACKEND_URL } from '../../../constants';
import { FloorListItem } from '../../../interfaces/floor.interfaces';
@ -27,17 +20,3 @@ const props = defineProps<{
floor: FloorListItem;
}>();
</script>
<style scoped lang="scss">
.hoverbox {
img {
transition: all 3s;
}
&:hover {
img {
transform: scale(1.3);
}
}
}
</style>