additions
This commit is contained in:
parent
74ef2848e2
commit
b875c2465d
38
src/components/ImageBox.vue
Normal file
38
src/components/ImageBox.vue
Normal 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>
|
@ -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">
|
||||||
|
@ -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]',
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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]',
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
32
src/store/storage.store.ts
Normal file
32
src/store/storage.store.ts
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@ -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
65
src/views/Demo.vue
Normal 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>
|
16
src/views/building/BuildingListItem.vue
Normal file
16
src/views/building/BuildingListItem.vue
Normal 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>
|
@ -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 || []"
|
||||||
|
@ -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>
|
|
||||||
|
Loading…
Reference in New Issue
Block a user