mirror of
https://github.com/hay-kot/homebox.git
synced 2025-07-08 19:48:35 +00:00
move to nuxt
This commit is contained in:
parent
890eb55d27
commit
26ecb5a9d4
93 changed files with 5273 additions and 4749 deletions
139
frontend/components/App/Header.vue
Normal file
139
frontend/components/App/Header.vue
Normal file
|
@ -0,0 +1,139 @@
|
|||
<script lang="ts" setup>
|
||||
import { useAuthStore } from '~~/stores/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const api = useUserApi();
|
||||
|
||||
async function logout() {
|
||||
const { error } = await authStore.logout(api);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo('/');
|
||||
}
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: 'Home',
|
||||
href: '/home',
|
||||
},
|
||||
{
|
||||
name: 'Logout',
|
||||
action: logout,
|
||||
last: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dropdown = [
|
||||
{
|
||||
name: 'Location',
|
||||
action: () => {
|
||||
modal.value = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Item / Asset',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
name: 'Label',
|
||||
action: () => {},
|
||||
},
|
||||
];
|
||||
|
||||
// ----------------------------
|
||||
// Location Stuff
|
||||
// Should move to own component
|
||||
const locationLoading = ref(false);
|
||||
const locationForm = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const locationNameRef = ref(null);
|
||||
const triggerFocus = ref(false);
|
||||
const modal = ref(false);
|
||||
|
||||
whenever(
|
||||
() => modal.value,
|
||||
() => {
|
||||
triggerFocus.value = true;
|
||||
}
|
||||
);
|
||||
|
||||
async function createLocation() {
|
||||
locationLoading.value = true;
|
||||
const { data } = await api.locations.create(locationForm);
|
||||
|
||||
if (data) {
|
||||
navigateTo(`/location/${data.id}`);
|
||||
}
|
||||
|
||||
locationLoading.value = false;
|
||||
modal.value = false;
|
||||
locationForm.name = '';
|
||||
locationForm.description = '';
|
||||
triggerFocus.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalConfirm />
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Location </template>
|
||||
<form @submit.prevent="createLocation">
|
||||
<FormTextField
|
||||
:trigger-focus="triggerFocus"
|
||||
ref="locationNameRef"
|
||||
:autofocus="true"
|
||||
label="Location Name"
|
||||
v-model="locationForm.name"
|
||||
/>
|
||||
<FormTextField label="Location Description" v-model="locationForm.description" />
|
||||
<div class="modal-action">
|
||||
<BaseButton type="submit" :loading="locationLoading"> Create </BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
<BaseContainer is="header" class="py-6">
|
||||
<h2 class="mt-1 text-4xl font-bold tracking-tight text-base-content sm:text-5xl lg:text-6xl">Homebox</h2>
|
||||
<div class="ml-1 mt-2 text-lg text-base-content/50 space-x-2">
|
||||
<template v-for="link in links">
|
||||
<NuxtLink
|
||||
v-if="!link.action"
|
||||
class="hover:text-base-content transition-color duration-200 italic"
|
||||
:to="link.href"
|
||||
>
|
||||
{{ link.name }}
|
||||
</NuxtLink>
|
||||
<button
|
||||
for="location-form-modal"
|
||||
v-else
|
||||
@click="link.action"
|
||||
class="hover:text-base-content transition-color duration-200 italic"
|
||||
>
|
||||
{{ link.name }}
|
||||
</button>
|
||||
<span v-if="!link.last"> / </span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex mt-6">
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-sm">
|
||||
<span>
|
||||
<Icon name="mdi-plus" class="w-5 h-5 mr-2" />
|
||||
</span>
|
||||
Create
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li v-for="btn in dropdown">
|
||||
<button @click="btn.action">
|
||||
{{ btn.name }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</BaseContainer>
|
||||
</template>
|
58
frontend/components/App/Toast.vue
Normal file
58
frontend/components/App/Toast.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div class="force-above fixed top-2 right-2 w-[300px]">
|
||||
<TransitionGroup name="notify" tag="div">
|
||||
<div
|
||||
v-for="(notify, index) in notifications.slice(0, 4)"
|
||||
:key="notify.id"
|
||||
class="my-2 w-[300px] rounded-md p-3 text-sm text-white opacity-75"
|
||||
:class="{
|
||||
'bg-primary': notify.type === 'info',
|
||||
'bg-red-600': notify.type === 'error',
|
||||
'bg-green-600': notify.type === 'success',
|
||||
}"
|
||||
@click="dropNotification(index)"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<template v-if="notify.type == 'success'">
|
||||
<Icon name="heroicons-check" class="h-5 w-5" />
|
||||
</template>
|
||||
<template v-if="notify.type == 'info'">
|
||||
<Icon name="heroicons-information-circle" class="h-5 w-5" />
|
||||
</template>
|
||||
|
||||
<template v-if="notify.type == 'error'">
|
||||
<Icon name="heroicons-bell-alert" class="h-5 w-5" />
|
||||
</template>
|
||||
{{ notify.message }}
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNotifications } from '@/composables/use-notifier';
|
||||
|
||||
const { notifications, dropNotification } = useNotifications();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.force-above {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.notify-move,
|
||||
.notify-enter-active,
|
||||
.notify-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.notify-enter-from,
|
||||
.notify-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
.notify-leave-active {
|
||||
position: absolute;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
</style>
|
21
frontend/components/Base/ActionsDivider.vue
Normal file
21
frontend/components/Base/ActionsDivider.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-primary" />
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<span class="isolate inline-flex -space-x-px rounded-md shadow-sm">
|
||||
<div class="btn-group">
|
||||
<button @click="$emit('edit')" name="options" class="btn btn-sm btn-primary">
|
||||
<Icon name="heroicons-pencil" class="h-5 w-5 mr-1" aria-hidden="true" />
|
||||
<span> Edit </span>
|
||||
</button>
|
||||
<button @click="$emit('delete')" name="options" class="btn btn-sm btn-primary">
|
||||
<Icon name="heroicons-trash" class="h-5 w-5 mr-1" aria-hidden="true" />
|
||||
<span> Delete </span>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
24
frontend/components/Base/Button.vue
Normal file
24
frontend/components/Base/Button.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<button
|
||||
:disabled="disabled || loading"
|
||||
class="btn"
|
||||
:class="{
|
||||
loading: loading,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
14
frontend/components/Base/Container.vue
Normal file
14
frontend/components/Base/Container.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
is: {
|
||||
type: String,
|
||||
default: 'div',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="is" class="container max-w-6xl mx-auto px-4">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
37
frontend/components/Base/Details.vue
Normal file
37
frontend/components/Base/Details.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6 bg-neutral">
|
||||
<h3 class="text-lg font-medium leading-6 text-neutral-content">
|
||||
<slot name="title"></slot>
|
||||
</h3>
|
||||
<p v-if="$slots.subtitle" class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||
<slot name="subtitle"></slot>
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 px-4 py-5 sm:p-0">
|
||||
<dl class="sm:divide-y sm:divide-gray-300">
|
||||
<div v-for="(dValue, dKey) in details" class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
{{ dKey }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ dValue }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type StringLike = string | number | boolean;
|
||||
|
||||
defineProps({
|
||||
details: {
|
||||
type: Object as () => Record<string, StringLike>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
45
frontend/components/Base/Modal.vue
Normal file
45
frontend/components/Base/Modal.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="z-[9999]">
|
||||
<input type="checkbox" :id="modalId" class="modal-toggle" v-model="modal" />
|
||||
<div class="modal">
|
||||
<div class="modal-box relative">
|
||||
<button @click="close" :for="modalId" class="btn btn-sm btn-circle absolute right-2 top-2">✕</button>
|
||||
|
||||
<h3 class="font-bold text-lg">
|
||||
<slot name="title"></slot>
|
||||
</h3>
|
||||
<slot> </slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['cancel', 'update:modelValue']);
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* in readonly mode the modal only `emits` a "cancel" event to indicate
|
||||
* that the modal was closed via the "x" button. The parent component is
|
||||
* responsible for closing the modal.
|
||||
*/
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
function close() {
|
||||
if (props.readonly) {
|
||||
emit('cancel');
|
||||
return;
|
||||
}
|
||||
modal.value = false;
|
||||
}
|
||||
|
||||
const modalId = useId();
|
||||
const modal = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
10
frontend/components/Base/SectionHeader.vue
Normal file
10
frontend/components/Base/SectionHeader.vue
Normal file
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div class="border-b border-base-200 pb-3">
|
||||
<h3 class="text-xl font-medium leading-4 text-base-content">
|
||||
<slot />
|
||||
</h3>
|
||||
<p v-if="$slots.description" class="mt-2 max-w-4xl text-sm text-gray-500">
|
||||
<slot name="description" />
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
42
frontend/components/Form/TextField.vue
Normal file
42
frontend/components/Form/TextField.vue
Normal file
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<input ref="input" :type="type" v-model="value" class="input input-bordered w-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
triggerFocus: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const input = ref<HTMLElement | null>(null);
|
||||
|
||||
whenever(
|
||||
() => props.triggerFocus,
|
||||
() => {
|
||||
if (input.value) {
|
||||
input.value.focus();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const value = useVModel(props, 'modelValue');
|
||||
</script>
|
31
frontend/components/Icon.vue
Normal file
31
frontend/components/Icon.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
import type { IconifyIcon } from '@iconify/vue';
|
||||
import { Icon as Iconify, loadIcon } from '@iconify/vue';
|
||||
|
||||
const nuxtApp = useNuxtApp();
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const icon: Ref<IconifyIcon | null> = ref(null);
|
||||
const component = computed(() => nuxtApp.vueApp.component(props.name));
|
||||
|
||||
icon.value = await loadIcon(props.name).catch(_ => null);
|
||||
|
||||
watch(
|
||||
() => props.name,
|
||||
async () => {
|
||||
icon.value = await loadIcon(props.name).catch(_ => null);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Iconify v-if="icon" :icon="icon" class="inline-block w-5 h-5" />
|
||||
<Component :is="component" v-else-if="component" />
|
||||
<span v-else>{{ name }}</span>
|
||||
</template>
|
15
frontend/components/ModalConfirm.vue
Normal file
15
frontend/components/ModalConfirm.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<BaseModal @cancel="cancel(false)" v-model="isRevealed" readonly>
|
||||
<template #title> Confirm </template>
|
||||
<div>
|
||||
<p>{{ text }}</p>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<BaseButton type="submit" @click="confirm(true)"> Confirm </BaseButton>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { text, isRevealed, confirm, cancel } = useConfirm();
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue