updates, i18n start

This commit is contained in:
Evert Prants 2023-11-07 22:08:56 +02:00
parent 8984ddd9dc
commit 8b2e2dd0aa
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
26 changed files with 1214 additions and 900 deletions

1651
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,37 +9,39 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.7.13", "@headlessui/vue": "^1.7.16",
"@heroicons/vue": "^2.0.18", "@heroicons/vue": "^2.0.18",
"@vuepic/vue-datepicker": "^5.1.2", "@vuepic/vue-datepicker": "^7.2.2",
"@vueuse/core": "^10.1.2", "@vueuse/core": "^10.5.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0", "lodash.pick": "^4.4.0",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"pinia": "^2.1.3", "pinia": "^2.1.7",
"sass": "^1.62.1", "sass": "^1.69.5",
"vue": "^3.3.4", "vue": "^3.3.8",
"vue-i18n": "^9.6.5",
"vue-material-design-icons": "^5.2.0", "vue-material-design-icons": "^5.2.0",
"vue-router": "^4.2.1" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.6",
"@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/line-clamp": "^0.4.4",
"@types/lodash.get": "^4.4.7", "@types/lodash.get": "^4.4.9",
"@types/lodash.omit": "^4.5.7", "@types/lodash.omit": "^4.5.9",
"@types/lodash.pick": "^4.4.7", "@types/lodash.pick": "^4.4.9",
"@types/lodash.set": "^4.3.7", "@types/lodash.set": "^4.3.9",
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.4.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.16",
"postcss": "^8.4.23", "postcss": "^8.4.31",
"prettier": "^2.8.8", "prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.3.0", "prettier-plugin-tailwindcss": "^0.5.6",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.5",
"typescript": "^5.0.4", "typescript": "^5.2.2",
"vite": "^4.3.8", "vite": "^4.5.0",
"vue-tsc": "^1.6.5" "vue-tsc": "^1.8.22"
} }
} }

View File

@ -37,6 +37,7 @@
:step="step" :step="step"
:min="min" :min="min"
:max="max" :max="max"
:autocomplete="autocomplete"
@input="onInput" @input="onInput"
@change="onChange" @change="onChange"
@focus="onFocus" @focus="onFocus"
@ -73,13 +74,14 @@ const props = withDefaults(
step?: string; step?: string;
min?: string; min?: string;
max?: string; max?: string;
autocomplete?: string;
}>(), }>(),
{ {
type: 'text', type: 'text',
required: false, required: false,
disabled: false, disabled: false,
readonly: false, readonly: false,
} },
); );
const emit = defineEmits<{ const emit = defineEmits<{
@ -96,11 +98,11 @@ const registerField = inject<(field: string) => void>('registerField');
const unregisterField = inject<(field: string) => void>('unregisterField'); const unregisterField = inject<(field: string) => void>('unregisterField');
const fieldName = computed(() => const fieldName = computed(() =>
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name formGroup?.value ? `${formGroup.value}.${props.name}` : props.name,
); );
const forId = computed( const forId = computed(
() => `${props.forIdPrefix || 'form'}-${fieldName.value}` () => `${props.forIdPrefix || 'form'}-${fieldName.value}`,
); );
const value = computed(() => get(formData?.value, fieldName.value)); const value = computed(() => get(formData?.value, fieldName.value));
const errors = computed(() => formErrors?.value[fieldName.value] || []); const errors = computed(() => formErrors?.value[fieldName.value] || []);

View File

@ -11,7 +11,6 @@
v-bind="props" v-bind="props"
:class="[invalid ? 'dp__invalid' : '']" :class="[invalid ? 'dp__invalid' : '']"
:uid="fieldFQN" :uid="fieldFQN"
:placeholder="placeholder"
:state="undefined" :state="undefined"
:format="format" :format="format"
:model-value="(value as string)" :model-value="(value as string)"

View File

@ -11,7 +11,7 @@
<PopoverButton <PopoverButton
class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
> >
<span class="sr-only">Open menu</span> <span class="sr-only">{{ $t('common.openMenu') }}</span>
<Bars3Icon class="h-6 w-6" aria-hidden="true" /> <Bars3Icon class="h-6 w-6" aria-hidden="true" />
</PopoverButton> </PopoverButton>
</div> </div>
@ -23,7 +23,7 @@
'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2', 'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
]" ]"
> >
<span>Buildings</span> <span>{{ $t('common.buildings') }}</span>
<ChevronDownIcon <ChevronDownIcon
:class="[ :class="[
open ? 'text-gray-600' : 'text-gray-400', open ? 'text-gray-600' : 'text-gray-400',
@ -47,7 +47,7 @@
<div <div
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5" class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
> >
<div class="relative bg-white pt-4 pb-4 sm:gap-5"> <div class="relative bg-white pb-4 pt-4 sm:gap-5">
<template v-for="building of buildings"> <template v-for="building of buildings">
<Building :building="building" /> <Building :building="building" />
</template> </template>
@ -60,7 +60,7 @@
<a <a
href="#" href="#"
class="text-base font-medium text-gray-500 hover:text-gray-900" class="text-base font-medium text-gray-500 hover:text-gray-900"
>Groups</a >{{ $t('common.groups') }}</a
> >
</PopoverGroup> </PopoverGroup>
<div class="hidden items-center justify-end md:flex md:flex-1 lg:w-0"> <div class="hidden items-center justify-end md:flex md:flex-1 lg:w-0">
@ -84,7 +84,7 @@
<div <div
class="divide-y-2 divide-gray-50 rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5" class="divide-y-2 divide-gray-50 rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5"
> >
<div class="px-5 pt-5 pb-6"> <div class="px-5 pb-6 pt-5">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<router-link to="/"><HomeIcon class="h-8 w-8" /></router-link> <router-link to="/"><HomeIcon class="h-8 w-8" /></router-link>
@ -93,13 +93,15 @@
<PopoverButton <PopoverButton
class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
> >
<span class="sr-only">Close menu</span> <span class="sr-only">{{ $t('common.closeMenu') }}</span>
<XMarkIcon class="h-6 w-6" aria-hidden="true" /> <XMarkIcon class="h-6 w-6" aria-hidden="true" />
</PopoverButton> </PopoverButton>
</div> </div>
</div> </div>
<div class="mt-6"> <div class="mt-6">
<span class="text-lg font-bold">Buildings</span> <span class="text-lg font-bold">{{
$t('common.buildings')
}}</span>
<div class="-mx-5"> <div class="-mx-5">
<template v-for="building of buildings"> <template v-for="building of buildings">
<Building :building="building" /> <Building :building="building" />
@ -107,7 +109,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="space-y-6 py-6 px-5"> <div class="space-y-6 px-5 py-6">
<div class="flex justify-center"> <div class="flex justify-center">
<UserPill :user="user" /> <UserPill :user="user" />
</div> </div>

View File

@ -1,5 +1,5 @@
<template> <template>
<Menu title="User" :options="[{ title: 'Logout' }]"> <Menu :title="$t('common.user')" :options="[{ title: $t('login.logout') }]">
<template #trigger> <template #trigger>
<MenuButton <MenuButton
class="flex flex-row items-center space-x-2 rounded-full px-1 py-1 pr-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500" class="flex flex-row items-center space-x-2 rounded-full px-1 py-1 pr-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
@ -22,7 +22,7 @@ import { ChevronDownIcon, UserIcon } from '@heroicons/vue/24/outline';
import { User } from '../../interfaces/user.interfaces'; import { User } from '../../interfaces/user.interfaces';
import Menu from '../menu/Menu.vue'; import Menu from '../menu/Menu.vue';
const props = defineProps<{ defineProps<{
user: User; user: User;
}>(); }>();
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="pointer-events-none absolute top-0 left-0 z-10 w-screen sm:w-auto" class="pointer-events-none absolute left-0 top-0 z-10 w-screen sm:w-auto"
> >
<Transition <Transition
enter-active-class="transition-max-height ease-out duration-200 overflow-hidden" enter-active-class="transition-max-height ease-out duration-200 overflow-hidden"
@ -16,12 +16,12 @@
> >
<div class="h-14 px-2 py-2.5"> <div class="h-14 px-2 py-2.5">
<div class="flex flex-row items-center space-x-4 px-4"> <div class="flex flex-row items-center space-x-4 px-4">
<button @click="router.go(-1)" title="Go back"> <button @click="router.go(-1)" :title="$t('common.back')">
<span class="sr-only">Go back</span <span class="sr-only">{{ $t('common.back') }}</span
><ChevronLeftIcon class="-ml-4 h-6 w-6" /> ><ChevronLeftIcon class="-ml-4 h-6 w-6" />
</button> </button>
<div class="flex flex-row items-center space-x-4"> <div class="flex flex-row items-center space-x-4">
<label for="building">Building:</label> <label for="building">{{ $t('common.building') }}:</label>
<select <select
id="building" id="building"
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200" class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
@ -29,7 +29,7 @@
@change=" @change="
emit( emit(
'update:selectedBuildingId', 'update:selectedBuildingId',
Number(($event.target as HTMLSelectElement).value) Number(($event.target as HTMLSelectElement).value),
) )
" "
> >
@ -42,7 +42,7 @@
class="flex flex-row items-center space-x-4" class="flex flex-row items-center space-x-4"
v-if="selectedBuildingId" v-if="selectedBuildingId"
> >
<label for="floor">Floor:</label> <label for="floor">{{ $t('common.floor') }}:</label>
<select <select
id="floor" id="floor"
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200" class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
@ -50,7 +50,7 @@
@change=" @change="
emit( emit(
'update:selectedFloorId', 'update:selectedFloorId',
Number(($event.target as HTMLSelectElement).value) Number(($event.target as HTMLSelectElement).value),
) )
" "
> >
@ -59,7 +59,9 @@
</option> </option>
</select> </select>
</div> </div>
<div v-if="selectedFloorId">{{ status }}</div> <div v-if="selectedFloorId">
{{ $t('planner.status.' + status) }}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -67,7 +69,7 @@
<button <button
:class="[ :class="[
'bg-white', 'bg-white',
'pointer-events-auto -mt-0.5 ml-2 h-8 rounded-br-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5', 'pointer-events-auto -mt-0.5 ml-2 h-8 rounded-bl-md rounded-br-md px-2 py-2 ring-1 ring-black ring-opacity-5',
]" ]"
:title="`${open ? 'Hide' : 'Open'} panel`" :title="`${open ? 'Hide' : 'Open'} panel`"
:aria-expanded="open" :aria-expanded="open"

View File

@ -1,5 +1,5 @@
<template> <template>
<PlannerSidebar title="Layers"> <PlannerSidebar :title="$t('planner.panel.layers.title')">
<div class="bg-white-50 flex h-full flex-col overflow-auto"> <div class="bg-white-50 flex h-full flex-col overflow-auto">
<template v-for="layer of layers"> <template v-for="layer of layers">
<button <button

View File

@ -1,5 +1,5 @@
<template> <template>
<PlannerSidebar title="Properties"> <PlannerSidebar :title="$t('planner.panel.properties.title')">
<div <div
class="bg-white-50 flex h-full flex-col overflow-auto" class="bg-white-50 flex h-full flex-col overflow-auto"
v-if="selectedObject && applicableProperties" v-if="selectedObject && applicableProperties"
@ -24,6 +24,9 @@ import {
} from './interfaces/properties.interfaces'; } from './interfaces/properties.interfaces';
import { computed } from 'vue'; import { computed } from 'vue';
import PropertyFormItem from './PropertyFormItem.vue'; import PropertyFormItem from './PropertyFormItem.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
layers: Layer[]; layers: Layer[];
@ -35,44 +38,64 @@ const emit = defineEmits<{
layerId: number, layerId: number,
objectId: number, objectId: number,
key: string, key: string,
value: unknown value: unknown,
): void; ): void;
}>(); }>();
const commonProps: ObjectProperty[] = [ const commonProps: ObjectProperty[] = [
{ key: 'name', title: 'Name', type: 'string' }, { key: 'name', title: t('planner.properties.name'), type: 'string' },
{ key: 'visible', title: 'Visible', type: 'boolean', groupable: true }, {
key: 'visible',
title: t('planner.properties.visible'),
type: 'boolean',
groupable: true,
},
]; ];
const lineProps: ObjectProperty[] = [ const lineProps: ObjectProperty[] = [
...commonProps, ...commonProps,
{ key: 'width', title: 'Line Width', type: 'number', groupable: true }, {
{ key: 'color', title: 'Color', type: 'color', groupable: true }, key: 'width',
title: t('planner.properties.width'),
type: 'number',
groupable: true,
},
{
key: 'color',
title: t('planner.properties.color'),
type: 'color',
groupable: true,
},
{ {
key: 'lineCap', key: 'lineCap',
title: 'Line Cap Style', title: t('planner.properties.lineCap'),
type: 'select', type: 'select',
groupable: true, groupable: true,
options: [ options: [
{ value: undefined, title: '' }, { value: undefined, title: '' },
{ value: 'butt', title: 'Butt' }, { value: 'butt', title: t('planner.properties.butt') },
{ value: 'round', title: 'Round' }, { value: 'round', title: t('planner.properties.round') },
{ value: 'square', title: 'Square' }, { value: 'square', title: t('planner.properties.square') },
], ],
}, },
{ {
key: 'lineJoin', key: 'lineJoin',
title: 'Line Join Style', title: t('planner.properties.lineJoin'),
type: 'select', type: 'select',
groupable: true, groupable: true,
options: [ options: [
{ value: undefined, title: '' }, { value: undefined, title: '' },
{ value: 'miter', title: 'Miter' }, { value: 'miter', title: t('planner.properties.miter') },
{ value: 'bevel', title: 'Bevel' }, { value: 'bevel', title: t('planner.properties.bevel') },
{ value: 'round', title: 'Round' }, { value: 'round', title: t('planner.properties.round') },
], ],
}, },
{ key: 'closed', title: 'Closed', type: 'boolean', groupable: true }, {
key: 'closed',
title: t('planner.properties.closed'),
type: 'boolean',
groupable: true,
},
]; ];
const objectTypeProperties: ObjectProperties[] = [ const objectTypeProperties: ObjectProperties[] = [
@ -95,20 +118,20 @@ const objectTypeProperties: ObjectProperties[] = [
]; ];
const currentLayer = computed(() => const currentLayer = computed(() =>
props.layers.find((layer) => layer.active && layer.visible) props.layers.find((layer) => layer.active && layer.visible),
); );
// TODO multi edit // TODO multi edit
const selectedObject = computed( const selectedObject = computed(
() => currentLayer.value?.contents?.filter((obj) => obj.selected)[0] () => currentLayer.value?.contents?.filter((obj) => obj.selected)[0],
); );
const applicableProperties = computed( const applicableProperties = computed(
() => () =>
selectedObject.value && selectedObject.value &&
objectTypeProperties.find( objectTypeProperties.find(
(prop) => prop.type === selectedObject.value?.type (prop) => prop.type === selectedObject.value?.type,
) ),
); );
const updateProp = (prop: ObjectProperty, value: unknown) => { const updateProp = (prop: ObjectProperty, value: unknown) => {
@ -119,7 +142,7 @@ const updateProp = (prop: ObjectProperty, value: unknown) => {
currentLayer.value!.id, currentLayer.value!.id,
selectedObject.value!.id, selectedObject.value!.id,
prop.key, prop.key,
value value,
); );
}; };
</script> </script>

View File

@ -3,13 +3,18 @@
<button <button
:class="[ :class="[
open ? 'bg-gray-200' : 'bg-white', open ? 'bg-gray-200' : 'bg-white',
'h-8 rounded-tl-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5', 'h-8 rounded-bl-md rounded-tl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
]" ]"
:title="`${open ? 'Hide' : 'Open'} panel`" :title="`${$t(open ? 'common.hide' : 'common.open')} ${$t(
'planner.panel.open',
)}`"
:aria-expanded="open" :aria-expanded="open"
@click="() => (open = !open)" @click="() => (open = !open)"
> >
<span class="sr-only">{{ open ? 'Hide' : 'Open' }} panel</span> <span class="sr-only"
>{{ $t(open ? 'common.hide' : 'common.open') }}
{{ $t('planner.panel.open') }}</span
>
<ChevronDoubleRightIcon class="h-4 w-4" v-if="open" /> <ChevronDoubleRightIcon class="h-4 w-4" v-if="open" />
<ChevronDoubleLeftIcon class="h-4 w-4" v-else /> <ChevronDoubleLeftIcon class="h-4 w-4" v-else />
</button> </button>

12
src/i18n.ts Normal file
View File

@ -0,0 +1,12 @@
import { createI18n } from 'vue-i18n';
import en from '@/locales/en.json';
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
globalInjection: true,
legacy: false,
messages: { en },
});
export default i18n;

86
src/locales/en.json Normal file
View File

@ -0,0 +1,86 @@
{
"login": {
"title": "Sign in to your account",
"submit": "Sign in",
"email": "Email address",
"password": "Password",
"logout": "Logout"
},
"common": {
"user": "User",
"users": "Users",
"actions": "Actions",
"back": "Go back",
"building": "Building",
"buildings": "Buildings",
"group": "Group",
"groups": "Groups",
"floor": "Floor",
"floors": "Floors",
"hide": "Hide",
"show": "Show",
"open": "Open",
"close": "Close",
"storage": "Storage",
"storages": "Storages",
"set": "Set",
"storageSet": "Storage set",
"openMenu": "Open menu",
"closeMenu": "Close menu"
},
"dashboard": {
"title": "Dashboard",
"expiringSoon": "Expiring soon",
"intro": "Hello, {name}"
},
"buildings": {
"editPlans": "Edit floor plans",
"editPlan": "Edit floor plan",
"floorIn": "Floor {floor} in {building}",
"floor": "Floor {floor}",
"interactive": "interactive floor plan"
},
"planner": {
"choose": "Choose a floor to edit from the top left",
"status": {
"modified": "Modified",
"noChanges": "No changes",
"saving": "Saving...",
"saved": "Saved!",
"failed": "Failed to save!"
},
"panel": {
"open": "panel",
"layers": {
"title": "Layers"
},
"properties": {
"title": "Properties"
}
},
"properties": {
"name": "Name",
"visible": "Visible",
"width": "Line Width",
"color": "Color",
"lineCap": "Line Cap Style",
"butt": "Butt",
"round": "Round",
"square": "Square",
"lineJoin": "Line Join Style",
"miter": "Miter",
"bevel": "Bevel",
"closed": "Closed"
}
},
"storage": {
"selectRoom": "Select Room",
"selectStorageSet": "Select Storage / Set",
"selectStorage": "Select Storage",
"move": "Move storage",
"set": "set",
"add": "Add Storage",
"addSet": "Add Storage Set",
"storedItems": "Stored items"
}
}

View File

@ -3,11 +3,13 @@ import './style.scss';
import App from './App.vue'; import App from './App.vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import router from './router'; import router from './router';
import i18n from './i18n';
const pinia = createPinia(); const pinia = createPinia();
const app = createApp(App); const app = createApp(App);
app.use(i18n);
app.use(pinia); app.use(pinia);
app.use(router); app.use(router);

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { User } from '../interfaces/user.interfaces'; import { User } from '../interfaces/user.interfaces';
import { useLocalStorage } from '@vueuse/core'; import { useLocalStorage } from '@vueuse/core';
import jwtDecode from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
import { BACKEND_URL } from '../constants'; import { BACKEND_URL } from '../constants';
import jfetch from '../utils/jfetch'; import jfetch from '../utils/jfetch';
@ -53,7 +53,7 @@ export const useUserStore = defineStore('user', {
Authorization: `Basic ${header}`, Authorization: `Basic ${header}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}), }),
} },
); );
this.accessToken = tokenResponse.access_token; this.accessToken = tokenResponse.access_token;

View File

@ -1,12 +1,16 @@
<template> <template>
<StandardLayout> <StandardLayout>
<PageHead bordered><h1 class="text-2xl font-bold">Dashboard</h1></PageHead> <PageHead bordered
><h1 class="text-2xl font-bold">{{ $t('dashboard.title') }}</h1></PageHead
>
<p>Hello, {{ user.name }}</p> <p>{{ $t('dashboard.intro', { name: user.name }) }}</p>
<template v-if="expiringItems?.length"> <template v-if="expiringItems?.length">
<PageHead class="mt-4" <PageHead class="mt-4"
><h2 class="text-xl font-bold">Expiring soon</h2></PageHead ><h2 class="text-xl font-bold">
{{ $t('dashboard.expiringSoon') }}
</h2></PageHead
> >
<div <div
@ -21,7 +25,7 @@
</template> </template>
<PageHead class="mt-4"> <PageHead class="mt-4">
<h2 class="text-xl font-bold">Buildings</h2> <h2 class="text-xl font-bold">{{ $t('common.buildings') }}</h2>
</PageHead> </PageHead>
<div <div

View File

@ -20,15 +20,15 @@
(layers, rooms, boundingBox) => (layers, rooms, boundingBox) =>
updateDocument(layers, rooms, boundingBox) updateDocument(layers, rooms, boundingBox)
" "
@edited="status = 'Modified'" @edited="status = 'modified'"
/> />
<div <div
class="flex h-full w-full select-none items-center justify-center text-lg" class="flex h-full w-full select-none items-center justify-center text-lg"
v-else v-else
> >
<span class="font-bold uppercase text-gray-300" <span class="font-bold uppercase text-gray-300">{{
>Choose a floor to edit from the top left</span $t('planner.choose')
> }}</span>
</div> </div>
</div> </div>
</template> </template>
@ -52,13 +52,13 @@ const route = useRoute();
const { buildings, floors } = storeToRefs(building); const { buildings, floors } = storeToRefs(building);
const selectedBuildingId = ref<number>(); const selectedBuildingId = ref<number>();
const selectedFloorId = ref<number>(); const selectedFloorId = ref<number>();
const status = ref('No changes'); const status = ref('noChanges');
const plannerRef = ref<InstanceType<typeof HousePlanner>>(); const plannerRef = ref<InstanceType<typeof HousePlanner>>();
const currentFloor = computed( const currentFloor = computed(
() => () =>
selectedFloorId.value && selectedFloorId.value &&
floors.value.find((floor) => floor.id === selectedFloorId.value) floors.value.find((floor) => floor.id === selectedFloorId.value),
); );
const floorPlan = computed( const floorPlan = computed(
() => () =>
@ -67,7 +67,7 @@ const floorPlan = computed(
...defaultRoomData, ...defaultRoomData,
...JSON.parse(currentFloor.value.plan || '{}'), ...JSON.parse(currentFloor.value.plan || '{}'),
id: selectedFloorId.value, id: selectedFloorId.value,
}) }),
); );
const buildingSelected = async (id: number) => { const buildingSelected = async (id: number) => {
@ -99,7 +99,7 @@ const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
const createdRooms = await building.upsertFloorRooms( const createdRooms = await building.upsertFloorRooms(
selectedBuildingId.value, selectedBuildingId.value,
currentFloor.value.number, currentFloor.value.number,
extractedRooms extractedRooms,
); );
if (createdRooms?.length) { if (createdRooms?.length) {
@ -122,25 +122,25 @@ const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
const updateDocument = async ( const updateDocument = async (
data: FloorDocument, data: FloorDocument,
rooms: Line[], rooms: Line[],
boundingBox?: Vec2Box boundingBox?: Vec2Box,
) => { ) => {
if ( if (
!selectedBuildingId.value || !selectedBuildingId.value ||
!selectedFloorId.value || !selectedFloorId.value ||
selectedFloorId.value !== data.id || selectedFloorId.value !== data.id ||
!currentFloor.value || !currentFloor.value ||
status.value === 'Saving...' status.value === 'saving'
) )
return; return;
status.value = 'Saving...'; status.value = 'saving';
try { try {
data = await updateRooms(data, rooms); data = await updateRooms(data, rooms);
// Prevent useless requests // Prevent useless requests
const floorPlan = JSON.stringify({ ...data, boundingBox }); const floorPlan = JSON.stringify({ ...data, boundingBox });
if (currentFloor.value.plan === floorPlan) { if (currentFloor.value.plan === floorPlan) {
status.value = 'Saved!'; status.value = 'saved';
return; return;
} }
@ -149,12 +149,12 @@ const updateDocument = async (
currentFloor.value.number, currentFloor.value.number,
{ {
plan: floorPlan, plan: floorPlan,
} },
); );
status.value = 'Saved!'; status.value = 'saved';
} catch (e) { } catch (e) {
console.error(`Failed to save floor document: ${(e as Error).stack}`); console.error(`Failed to save floor document: ${(e as Error).stack}`);
status.value = 'Failed to save!'; status.value = 'failed';
} }
}; };

View File

@ -1,13 +1,13 @@
<template> <template>
<div <div
class="flex min-h-full items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8" class="flex min-h-full items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8"
> >
<div class="w-full max-w-md space-y-8"> <div class="w-full max-w-md space-y-8">
<div> <div>
<h2 <h2
class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900" class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900"
> >
Sign in to your account {{ $t('login.title') }}
</h2> </h2>
</div> </div>
<Transition <Transition
@ -33,12 +33,22 @@
<div> <div>
<div class="bg-white px-4 py-5 sm:p-6"> <div class="bg-white px-4 py-5 sm:p-6">
<div class="space-y-5"> <div class="space-y-5">
<FormField name="email" label="Email address" type="email" /> <FormField
<FormField name="password" label="Password" type="password" /> name="email"
:label="$t('login.email')"
type="email"
autocomplete="username"
/>
<FormField
name="password"
:label="$t('login.password')"
type="password"
autocomplete="current-password"
/>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-4 py-3 text-right sm:px-6"> <div class="bg-gray-50 px-4 py-3 text-right sm:px-6">
<Button button-type="submit">Sign in</Button> <Button button-type="submit">{{ $t('login.submit') }}</Button>
</div> </div>
</div> </div>
</Form> </Form>

View File

@ -3,17 +3,17 @@
<PageHead bordered> <PageHead bordered>
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="text-2xl font-bold">{{ building?.displayName }}</h1> <h1 class="text-2xl font-bold">{{ building?.displayName }}</h1>
<span class="text-sm font-light text-gray-800 line-clamp-1">{{ <span class="line-clamp-1 text-sm font-light text-gray-800">{{
building?.address building?.address
}}</span> }}</span>
</div> </div>
<Menu <Menu
title="Actions" :title="$t('common.actions')"
class="self-end" class="self-end"
:options="[ :options="[
{ {
title: 'Edit floor plans', title: $t('buildings.editPlans'),
link: { name: 'planner', query: { buildingId: building?.id } }, link: { name: 'planner', query: { buildingId: building?.id } },
}, },
]" ]"
@ -21,7 +21,7 @@
</PageHead> </PageHead>
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h2 class="text-xl font-bold">Floors</h2> <h2 class="text-xl font-bold">{{ $t('common.floors') }}</h2>
</div> </div>
<div <div

View File

@ -1,7 +1,7 @@
<template> <template>
<ImageBox <ImageBox
:title="floor.displayName" :title="floor.displayName"
:subtitle="`Floor ${floor.number}`" :subtitle="$t('buildings.floor', { floor: floor.number })"
:route-link="{ :route-link="{
name: 'floor', name: 'floor',
params: { id: building, number: floor.number }, params: { id: building, number: floor.number },

View File

@ -3,17 +3,20 @@
<PageHead bordered> <PageHead bordered>
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="text-2xl font-bold">{{ floor?.displayName }}</h1> <h1 class="text-2xl font-bold">{{ floor?.displayName }}</h1>
<span class="text-sm font-light text-gray-800 line-clamp-1" <span class="line-clamp-1 text-sm font-light text-gray-800">{{
>Floor {{ floor?.number }} in {{ building?.displayName }}</span $t('buildings.floorIn', {
> floor: floor?.number,
building: building?.displayName,
})
}}</span>
</div> </div>
<Menu <Menu
title="Actions" :title="$t('common.actions')"
class="self-end" class="self-end"
:options="[ :options="[
{ {
title: 'Edit floor plan', title: $t('buildings.editPlan'),
link: { link: {
name: 'planner', name: 'planner',
query: { buildingId: building?.id, floorId: floor?.id }, query: { buildingId: building?.id, floorId: floor?.id },
@ -31,7 +34,10 @@
'focus:outline-none focus:ring-2 focus:ring-blue-500', 'focus:outline-none focus:ring-2 focus:ring-blue-500',
]" ]"
> >
<span>{{ showMap ? 'Hide' : 'Show' }} interactive floor plan</span> <span
>{{ $t(showMap ? 'common.hide' : 'common.show') }}
{{ $t('buildings.interactive') }}</span
>
<ChevronUpIcon v-if="showMap" class="h-4 w-4" /> <ChevronUpIcon v-if="showMap" class="h-4 w-4" />
<ChevronDownIcon v-else class="h-4 w-4" /> <ChevronDownIcon v-else class="h-4 w-4" />
</button> </button>
@ -95,7 +101,7 @@
<span <span
v-if="isSet(storage)" v-if="isSet(storage)"
class="text-[0.75rem] font-bold uppercase text-gray-400" class="text-[0.75rem] font-bold uppercase text-gray-400"
>(Storage Set)</span >({{ $t('common.storageSet') }})</span
> >
</div> </div>
@ -124,11 +130,11 @@
</div> </div>
</div> </div>
<span class="text-sm" v-if="!isSet(storage)" <span class="text-sm" v-if="!isSet(storage)"
>Stored items: >{{ $t('storage.storedItems') }}:
{{ (storage as StorageListItem).itemCount }}</span {{ (storage as StorageListItem).itemCount }}</span
> >
<span class="text-sm" v-else <span class="text-sm" v-else
>Storages: >{{ $t('common.storages') }}:
{{ (storage as StorageSetListItem).storages.length }}</span {{ (storage as StorageSetListItem).storages.length }}</span
> >
</StorageBubble> </StorageBubble>
@ -218,14 +224,15 @@ const selectedStorage = ref<StorageListItem>();
const newStorage = ref<InstanceType<typeof NewStorageModal>>(); const newStorage = ref<InstanceType<typeof NewStorageModal>>();
const storageViewRef = ref<InstanceType<typeof StorageView>>(); const storageViewRef = ref<InstanceType<typeof StorageView>>();
const floor = computed(() => const floor = computed(
building.value?.floors.find( () =>
(floor) => floor.number === Number(route.params.number) building.value?.floors.find(
) (floor) => floor.number === Number(route.params.number),
),
); );
const floorDocument = computed( const floorDocument = computed(
() => floor.value?.plan && JSON.parse(floor.value.plan) () => floor.value?.plan && JSON.parse(floor.value.plan),
); );
const clickOnRoom = (room?: RoomLayoutObject, x?: number, y?: number) => { const clickOnRoom = (room?: RoomLayoutObject, x?: number, y?: number) => {
@ -266,7 +273,7 @@ const setBubbleLocation = async (x: number, y: number) => {
location: `${x},${y}`, location: `${x},${y}`,
}, },
headers: authHeader.value, headers: authHeader.value,
} },
); );
movingBubble.value = undefined; movingBubble.value = undefined;
}; };
@ -276,7 +283,7 @@ const addNewStorage = (set: boolean | StorageSetListItem) => {
newStorage.value?.openModal( newStorage.value?.openModal(
building.value, building.value,
selectedRoom.value as unknown as RoomListItem, selectedRoom.value as unknown as RoomListItem,
set set,
); );
}; };
@ -290,7 +297,7 @@ const selectRoomFromList = (room: RoomLayoutObject) => {
const selectStorage = async ( const selectStorage = async (
storage: StorageSharedType, storage: StorageSharedType,
parentSet?: StorageSetListItem parentSet?: StorageSetListItem,
) => { ) => {
if (isSet(storage)) { if (isSet(storage)) {
selectedSet.value = storage as StorageSetListItem; selectedSet.value = storage as StorageSetListItem;
@ -325,7 +332,7 @@ const rooms = computed<RoomLayoutObject[]>(
storages: [], storages: [],
} as RoomLayoutObject; } as RoomLayoutObject;
})) || })) ||
[] [],
); );
const getRoomStorages = async (room: RoomLayoutObject) => { const getRoomStorages = async (room: RoomLayoutObject) => {
@ -335,13 +342,13 @@ const getRoomStorages = async (room: RoomLayoutObject) => {
`${BACKEND_URL}/storage/room/${room.id}?includeWithSets=false`, `${BACKEND_URL}/storage/room/${room.id}?includeWithSets=false`,
{ {
headers: authHeader.value, headers: authHeader.value,
} },
); );
const { data: setList } = await jfetch( const { data: setList } = await jfetch(
`${BACKEND_URL}/storage/set/room/${room.id}`, `${BACKEND_URL}/storage/set/room/${room.id}`,
{ {
headers: authHeader.value, headers: authHeader.value,
} },
); );
storages.value = [...storageList, ...setList]; storages.value = [...storageList, ...setList];
} catch (e) { } catch (e) {

View File

@ -1,5 +1,6 @@
<template> <template>
<div <div
@click.stop
class="custom-storage absolute z-10" class="custom-storage absolute z-10"
:style="getStoragePosition(storage)" :style="getStoragePosition(storage)"
> >
@ -13,11 +14,11 @@
class="absolute -bottom-2 left-1/2 h-0 w-0 -translate-x-1/2 border-x-8 border-t-[16px] border-x-transparent border-t-white" class="absolute -bottom-2 left-1/2 h-0 w-0 -translate-x-1/2 border-x-8 border-t-[16px] border-x-transparent border-t-white"
></div> ></div>
<button <button
class="absolute left-1/2 bottom-0.5 -translate-x-1/2 rounded-full bg-gray-50 px-1 py-1 ring-1 ring-black ring-opacity-5" class="absolute bottom-0.5 left-1/2 -translate-x-1/2 rounded-full bg-gray-50 px-1 py-1 ring-1 ring-black ring-opacity-5"
@click.stop="emit('startMoving')" @click.stop="emit('startMoving')"
title="Move storage" :title="$t('storage.move')"
> >
<span class="sr-only">Move storage</span> <span class="sr-only">{{ $t('storage.move') }}</span>
<ArrowsPointingOutIcon class="h-4 w-4" /> <ArrowsPointingOutIcon class="h-4 w-4" />
</button> </button>
</div> </div>

View File

@ -10,7 +10,7 @@
<div <div
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold" class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
> >
Select Room {{ $t('storage.selectRoom') }}
</div> </div>
<button <button
v-for="room of rooms" v-for="room of rooms"
@ -20,7 +20,7 @@
selectedRoom?.id === room.id selectedRoom?.id === room.id
? 'bg-blue-100 hover:bg-blue-200' ? 'bg-blue-100 hover:bg-blue-200'
: 'hover:bg-blue-100', : 'hover:bg-blue-100',
'flex items-center justify-between border-b-1 border-gray-100 py-2 px-2', 'flex items-center justify-between border-b-1 border-gray-100 px-2 py-2',
]" ]"
> >
<span>{{ room.displayName }}</span> <span>{{ room.displayName }}</span>
@ -37,7 +37,7 @@
<div <div
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold" class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
> >
Select Storage / Set {{ $t('storage.selectStorageSet') }}
</div> </div>
<button <button
v-for="storage of storages" v-for="storage of storages"
@ -47,7 +47,7 @@
isStorageOrSetSelected(storage) isStorageOrSetSelected(storage)
? 'bg-blue-100 hover:bg-blue-200' ? 'bg-blue-100 hover:bg-blue-200'
: 'hover:bg-blue-100', : 'hover:bg-blue-100',
'flex items-center justify-between border-b-1 border-gray-100 py-2 px-2', 'flex items-center justify-between border-b-1 border-gray-100 px-2 py-2',
]" ]"
> >
<span <span
@ -57,7 +57,7 @@
>({{ (storage as StorageListItem).itemCount }})</span >({{ (storage as StorageListItem).itemCount }})</span
> >
<span v-else> <span v-else>
(set) ({{ ({{ $t('storage.set') }}) ({{
(storage as StorageSetListItem).storages.length (storage as StorageSetListItem).storages.length
}})</span }})</span
> >
@ -70,14 +70,16 @@
@click="emit('newStorage', false)" @click="emit('newStorage', false)"
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline" class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
> >
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span> <PlusIcon class="h-4 w-4" />
<span>{{ $t('storage.add') }}</span>
</button> </button>
<span>·</span> <span>·</span>
<button <button
@click="emit('newStorage', true)" @click="emit('newStorage', true)"
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline" class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
> >
<PlusIcon class="h-4 w-4" /> <span>Add Storage Set</span> <PlusIcon class="h-4 w-4" />
<span>{{ $t('storage.addSet') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -92,7 +94,7 @@
<div <div
class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold" class="border-b-4 border-gray-300 bg-gray-100 px-2 py-2 text-center font-bold"
> >
Select Storage {{ $t('storage.selectStorage') }}
</div> </div>
<button <button
v-for="storage of selectedSet.storages" v-for="storage of selectedSet.storages"
@ -102,7 +104,7 @@
isStorageSelected(storage) isStorageSelected(storage)
? 'bg-blue-100 hover:bg-blue-200' ? 'bg-blue-100 hover:bg-blue-200'
: 'hover:bg-blue-100', : 'hover:bg-blue-100',
'flex items-center justify-between border-b-1 border-gray-100 py-2 px-2', 'flex items-center justify-between border-b-1 border-gray-100 px-2 py-2',
]" ]"
> >
<span>{{ storage.displayName }} ({{ storage.itemCount }})</span> <span>{{ storage.displayName }} ({{ storage.itemCount }})</span>
@ -114,7 +116,7 @@
@click="emit('newStorage', selectedSet!)" @click="emit('newStorage', selectedSet!)"
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline" class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
> >
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span> <PlusIcon class="h-4 w-4" /> <span>{{ $t('storage.add') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -153,7 +155,7 @@ const emit = defineEmits<{
( (
e: 'selectStorage', e: 'selectStorage',
storage: StorageSharedType, storage: StorageSharedType,
parentSet?: StorageSetListItem parentSet?: StorageSetListItem,
): void; ): void;
(e: 'selectRoom', room: RoomLayoutObject): void; (e: 'selectRoom', room: RoomLayoutObject): void;
(e: 'newStorage', set: boolean | StorageSetListItem): void; (e: 'newStorage', set: boolean | StorageSetListItem): void;

View File

@ -4,7 +4,7 @@
<h2 class="text-2xl font-bold">{{ storage.displayName }}</h2> <h2 class="text-2xl font-bold">{{ storage.displayName }}</h2>
<Menu <Menu
title="Actions" :title="$t('common.actions')"
class="self-end" class="self-end"
:options="[ :options="[
{ {
@ -28,7 +28,7 @@
<template #actions> <template #actions>
<Menu <Menu
title="" title=""
button-class="px-0.5 py-0.5" button-class="!px-0.5 !py-0.5"
:options="[ :options="[
{ title: 'Change details' }, { title: 'Change details' },
{ title: 'Add transaction' }, { title: 'Add transaction' },

View File

@ -8,5 +8,5 @@ module.exports = {
}, },
}, },
}, },
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/line-clamp')], plugins: [require('@tailwindcss/forms')],
}; };

View File

@ -9,10 +9,27 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["ESNext", "DOM"], "lib": [
"ESNext",
"DOM"
],
"skipLibCheck": true, "skipLibCheck": true,
"noEmit": true "noEmit": true,
"paths": {
"@/*": [
"./src/*"
]
}
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "include": [
"references": [{ "path": "./tsconfig.node.json" }] "src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
} }

View File

@ -4,4 +4,9 @@ import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
resolve: {
alias: {
'@': '/src',
},
},
}); });