This commit is contained in:
Evert Prants 2023-02-04 13:34:50 +02:00
parent 710a904323
commit 5a63f00f51
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
11 changed files with 117 additions and 92 deletions

29
src/components/Button.vue Normal file
View File

@ -0,0 +1,29 @@
<template>
<button
:class="[
'inline-flex justify-center rounded-md border border-transparent py-2 px-4 text-sm font-medium shadow-sm',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
classes,
]"
:type="buttonType"
>
<slot />
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = withDefaults(
defineProps<{
buttonType?: 'submit' | 'button';
variant?: 'primary' | 'secondary' | 'tertiary';
}>(),
{ buttonType: 'button', variant: 'primary' }
);
const classes = computed(() => {
if (!props.variant || props.variant === 'primary')
return 'bg-green-600 hover:bg-green-700 text-white';
});
</script>

View File

@ -101,7 +101,12 @@
/> />
</FormGroup> </FormGroup>
<button type="submit">Submit</button> <div class="flex justify-end space-x-1">
<Button button-type="submit" @click="submitlock = true"
>Submit and add another</Button
>
<Button button-type="submit">Submit</Button>
</div>
</Form> </Form>
</template> </template>
</Modal> </Modal>
@ -127,17 +132,14 @@ import FormSelectField from '../form/fields/FormSelectField.vue';
import { IsRequired, MinLength } from '../form/validators'; import { IsRequired, MinLength } from '../form/validators';
import Modal from '../Modal.vue'; import Modal from '../Modal.vue';
import { ItemType, ItemTypeName } from '../../enums/item-type.enum'; import { ItemType, ItemTypeName } from '../../enums/item-type.enum';
import { import { TransactionType } from '../../enums/transaction-type.enum';
TransactionType,
TransactionTypeDescription,
TransactionTypeName,
} from '../../enums/transaction-type.enum';
import FormGroup from '../form/FormGroup.vue'; import FormGroup from '../form/FormGroup.vue';
import FormDateField from '../form/fields/FormDateField.vue'; import FormDateField from '../form/fields/FormDateField.vue';
import FormAutocompleteField from '../form/fields/FormAutocompleteField.vue'; import FormAutocompleteField from '../form/fields/FormAutocompleteField.vue';
import { FormValidator } from '../form/validator.types'; import { FormValidator } from '../form/validator.types';
import deepUnref from '../../utils/deep-unref'; import deepUnref from '../../utils/deep-unref';
import FormTextareaField from '../form/fields/FormTextareaField.vue'; import FormTextareaField from '../form/fields/FormTextareaField.vue';
import Button from '../Button.vue';
const defaults: any = { const defaults: any = {
item: null, item: null,
@ -159,6 +161,7 @@ const data: any = ref({
...deepUnref(defaults), ...deepUnref(defaults),
}); });
const error = ref(''); const error = ref('');
const submitlock = ref(false);
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'added', storage: StoredItem): void; (e: 'added', storage: StoredItem): void;
@ -256,7 +259,10 @@ const startAddingNewItem = (insert: string) => {
const onSubmit = async (form: FormSubmit) => { const onSubmit = async (form: FormSubmit) => {
error.value = ''; error.value = '';
if (!form.isValid || !storage.value) return; if (!form.isValid || !storage.value) {
submitlock.value = false;
return;
}
const body = { const body = {
...form.formData, ...form.formData,
@ -281,6 +287,10 @@ const onSubmit = async (form: FormSubmit) => {
} }
emit('added', createdStoredItem); emit('added', createdStoredItem);
if (submitlock.value) {
submitlock.value = false;
return;
}
modalRef.value?.closeModal(); modalRef.value?.closeModal();
}; };
</script> </script>

View File

@ -19,19 +19,10 @@
</FormField> </FormField>
<FormColorField name="color" label="Color" /> <FormColorField name="color" label="Color" />
<div class="flex justify-end space-x-1"> <div class="flex justify-end space-x-1">
<button <Button button-type="submit" @click="submitlock = true"
class="inline-flex justify-center rounded-md border border-transparent bg-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" >Submit and add another</Button
type="submit"
@click="submitlock = true"
> >
Submit and add another <Button button-type="submit">Submit</Button>
</button>
<button
class="inline-flex justify-center rounded-md border border-transparent bg-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
type="submit"
>
Submit
</button>
</div> </div>
</Form> </Form>
</template> </template>
@ -60,6 +51,7 @@ import FormSelectField from '../form/fields/FormSelectField.vue';
import { IsRequired, MinLength } from '../form/validators'; import { IsRequired, MinLength } from '../form/validators';
import Modal from '../Modal.vue'; import Modal from '../Modal.vue';
import FormColorField from '../form/fields/FormColorField.vue'; import FormColorField from '../form/fields/FormColorField.vue';
import Button from '../Button.vue';
const { authHeader } = useAccessToken(); const { authHeader } = useAccessToken();
const defaults = { const defaults = {
@ -123,7 +115,10 @@ defineExpose({
const onSubmit = async (value: FormSubmit) => { const onSubmit = async (value: FormSubmit) => {
error.value = ''; error.value = '';
if (!value.isValid || !room.value) return; if (!value.isValid || !room.value) {
submitlock.value = false;
return;
}
const createURL = `${BACKEND_URL}/storage${ const createURL = `${BACKEND_URL}/storage${
set.value === true ? '/set' : '' set.value === true ? '/set' : ''

View File

@ -32,7 +32,7 @@
> >
<MenuItems <MenuItems
:class="[ :class="[
'absolute z-10 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg', 'absolute z-10 mt-2 w-56 divide-y divide-gray-100 rounded-md bg-white shadow-lg',
'ring-1 ring-black ring-opacity-5 focus:outline-none', 'ring-1 ring-black ring-opacity-5 focus:outline-none',
positionClass, positionClass,
]" ]"
@ -65,31 +65,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'; import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
import { ChevronDownIcon } from '@heroicons/vue/24/outline'; import { ChevronDownIcon } from '@heroicons/vue/24/outline';
import { Component, computed } from 'vue'; import { computed } from 'vue';
import { RouteLocationRaw, RouterLink } from 'vue-router'; import { RouterLink } from 'vue-router';
import { MenuOption as MenuOptionType } from './menu.interfaces';
import MenuOption from './MenuOption.vue'; import MenuOption from './MenuOption.vue';
export interface MenuOption {
title: string;
key?: string;
link?: RouteLocationRaw;
icon?: Component;
onClick?: () => void;
component?: Component;
}
const props = defineProps<{ const props = defineProps<{
title: string; title: string;
options: MenuOption[]; options: MenuOptionType[];
buttonClass?: string; buttonClass?: string;
hideChevron?: boolean; hideChevron?: boolean;
position?: 'left' | 'center' | 'right'; position?: 'left' | 'center' | 'right';
}>(); }>();
const positionClass = computed(() => { const positionClass = computed(() => {
if (!props.position || props.position === 'right') return 'right-0'; if (!props.position || props.position === 'right')
if (props.position === 'center') return 'left-1/2 -translate-x-1/2'; return 'right-0 origin-top-right';
if (props.position === 'left') return 'left-0'; if (props.position === 'center')
return 'left-1/2 -translate-x-1/2 origin-top-center';
if (props.position === 'left') return 'left-0 origin-top-left';
return ''; return '';
}); });
</script> </script>

View File

@ -17,7 +17,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { MenuOption } from './Menu.vue'; import { MenuOption } from './menu.interfaces';
const props = defineProps<{ const props = defineProps<{
active: boolean; active: boolean;

View File

@ -0,0 +1,11 @@
import { Component } from 'vue';
import { RouteLocationRaw } from 'vue-router';
export interface MenuOption {
title: string;
key?: string;
link?: RouteLocationRaw;
icon?: Component;
onClick?: () => void;
component?: Component;
}

View File

@ -25,54 +25,23 @@
</div> </div>
</div> </div>
</Transition> </Transition>
<form <Form
@submit.prevent="doLogin" @submit="doLogin"
class="mt-8 overflow-hidden shadow sm:rounded-md" class="mt-8 overflow-hidden shadow sm:rounded-md"
:validators="validators"
> >
<div class="bg-white px-4 py-5 sm:p-6"> <div>
<div class="space-y-5"> <div class="bg-white px-4 py-5 sm:p-6">
<div class="space-y-1"> <div class="space-y-5">
<label for="email" class="block text-sm font-medium text-gray-700" <FormField name="email" label="Email address" type="email" />
>Email address</label <FormField name="password" label="Password" type="password" />
>
<input
type="email"
name="email"
id="email"
required
autofocus
autocomplete="email"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
v-model="loginForm.email"
/>
</div>
<div class="space-y-1">
<label
for="password"
class="block text-sm font-medium text-gray-700"
>Password</label
>
<input
type="password"
name="password"
id="password"
required
autocomplete="current-password"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
v-model="loginForm.password"
/>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-4 py-3 text-right sm:px-6">
<Button button-type="submit">Sign in</Button>
</div>
</div> </div>
<div class="bg-gray-50 px-4 py-3 text-right sm:px-6"> </Form>
<button
type="submit"
class="inline-flex justify-center rounded-md border border-transparent bg-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Sign in
</button>
</div>
</form>
</div> </div>
</div> </div>
</template> </template>
@ -80,25 +49,37 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Button from '../components/Button.vue';
import { FormSubmit } from '../components/form/form.types';
import Form from '../components/form/Form.vue';
import FormField from '../components/form/FormField.vue';
import { IsEmail, IsRequired } from '../components/form/validators';
import { useUserStore } from '../store/user.store'; import { useUserStore } from '../store/user.store';
import { JFetchError } from '../utils/jfetch'; import { JFetchError } from '../utils/jfetch';
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const errorMessage = ref<string>(''); const errorMessage = ref<string>('');
const loginForm = ref<{ const validators = [
email: string; {
password: string; field: 'email',
}>({ validators: [IsRequired(), IsEmail()],
email: '', },
password: '', {
}); field: 'password',
validators: [IsRequired()],
},
];
const doLogin = async () => { const doLogin = async (submit: FormSubmit) => {
errorMessage.value = ''; errorMessage.value = '';
if (!submit.isValid) return;
try { try {
const { email, password } = loginForm.value; const { email, password } = submit.formData;
await userStore.login({ email, password }); await userStore.login({
email: email as string,
password: password as string,
});
router.replace({ name: 'dashboard' }); router.replace({ name: 'dashboard' });
} catch (e) { } catch (e) {
const error = e as JFetchError; const error = e as JFetchError;

View File

@ -10,6 +10,7 @@
<Menu <Menu
title="Actions" title="Actions"
class="self-end"
:options="[ :options="[
{ {
title: 'Edit floor plans', title: 'Edit floor plans',

View File

@ -9,6 +9,7 @@
<Menu <Menu
title="Actions" title="Actions"
class="self-end"
:options="[ :options="[
{ {
title: 'Edit floor plan', title: 'Edit floor plan',
@ -80,6 +81,7 @@
storage.id === selectedStorage?.id || storage.id === selectedStorage?.id ||
(isSet(storage) && storage.id === selectedSet?.id), (isSet(storage) && storage.id === selectedSet?.id),
'pointer-events-none': !!movingBubble, 'pointer-events-none': !!movingBubble,
'pointer-events-auto': !movingBubble,
}" }"
@mouseenter="() => (hoveredBubble = storage)" @mouseenter="() => (hoveredBubble = storage)"
@start-moving="moveBubble(storage)" @start-moving="moveBubble(storage)"

View File

@ -5,12 +5,12 @@
highlighted highlighted
? '' ? ''
: 'flex cursor-pointer items-center justify-center transition-transform hover:scale-105', : 'flex cursor-pointer items-center justify-center transition-transform hover:scale-105',
'pointer-events-auto absolute', ' absolute',
]" ]"
:style="roomPositionCSS" :style="roomPositionCSS"
> >
<div <div
class="absolute" class="pointer-events-auto absolute"
:style="roomPolygonCSS" :style="roomPolygonCSS"
@click.stop="clickOnRoom($event)" @click.stop="clickOnRoom($event)"
@mousemove="moveOverRoom($event)" @mousemove="moveOverRoom($event)"

View File

@ -5,6 +5,7 @@
<Menu <Menu
title="Actions" title="Actions"
class="self-end"
:options="[ :options="[
{ {
title: 'Add new item', title: 'Add new item',
@ -31,6 +32,7 @@
:options="[ :options="[
{ title: 'Change details' }, { title: 'Change details' },
{ title: 'Add transaction' }, { title: 'Add transaction' },
{ title: 'Move' },
]" ]"
> >
<EllipsisVerticalIcon class="h-5 w-5" /> <EllipsisVerticalIcon class="h-5 w-5" />