frontend: cleanup

* dummy commit

* cleanup workflows

* setup and run eslint

* add linter to CI

* use eslint for formatting

* reorder rules

* drop editor config
This commit is contained in:
Hayden 2022-09-09 14:46:53 -08:00 committed by GitHub
parent 78fa714297
commit 75c633dcb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2048 additions and 641 deletions

View File

@ -32,6 +32,3 @@ jobs:
- name: Test
run: task api:coverage
- name: Upload coverage to Codecov
run: cd backend && bash <(curl -s https://codecov.io/bash)

View File

@ -32,5 +32,9 @@ jobs:
run: pnpm install
working-directory: frontend
- name: Run linter 👀
run: pnpm lint
working-directory: "frontend"
- name: Run Integration Tests
run: task test:ci

View File

@ -11,11 +11,11 @@ env:
jobs:
backend-tests:
name: "Backend Server Tests"
uses: hay-kot/homebox/.github/workflows/go.yaml@main
uses: hay-kot/homebox/.github/workflows/partial-backend.yaml@main
frontend-tests:
name: "Frontend and End-to-End Tests"
uses: hay-kot/homebox/.github/workflows/frontend.yaml@main
uses: hay-kot/homebox/.github/workflows/partial-frontend.yaml@main
deploy:
name: "Deploy Nightly to Fly.io"

View File

@ -8,8 +8,8 @@ on:
jobs:
backend-tests:
name: "Backend Server Tests"
uses: hay-kot/homebox/.github/workflows/go.yaml@main
uses: hay-kot/homebox/.github/workflows/partial-backend.yaml@main
frontend-tests:
name: "Frontend and End-to-End Tests"
uses: hay-kot/homebox/.github/workflows/frontend.yaml@main
uses: hay-kot/homebox/.github/workflows/partial-frontend.yaml@main

View File

@ -8,6 +8,7 @@
</p>
## MVP Todo
- [x] Locations

View File

@ -1,12 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
[*.{js,jsx,html,sass,vue,ts,tsx,json}]
charset = utf-8
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true

51
frontend/.eslintrc.js Normal file
View File

@ -0,0 +1,51 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/essential",
"plugin:@typescript-eslint/recommended",
"@nuxtjs/eslint-config-typescript",
"plugin:vue/vue3-recommended",
"plugin:prettier/recommended",
],
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint"],
rules: {
"no-console": 0,
"no-unused-vars": "off",
"vue/multi-word-component-names": "off",
"vue/no-setup-props-destructure": 0,
"vue/no-multiple-template-root": 0,
"vue/no-v-model-argument": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-unused-vars": [
"error",
{
ignoreRestSiblings: true,
destructuredArrayIgnorePattern: "_",
caughtErrors: "none",
},
],
"prettier/prettier": [
"warn",
{
arrowParens: "avoid",
semi: true,
tabWidth: 2,
useTabs: false,
vueIndentScriptAndStyle: true,
singleQuote: false,
trailingComma: "es5",
printWidth: 120,
},
],
},
};

View File

@ -1,10 +0,0 @@
{
"arrowParens": "avoid",
"semi": true,
"tabWidth": 2,
"useTabs": false,
"vueIndentScriptAndStyle": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 120
}

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { useAuthStore } from '~~/stores/auth';
import { useAuthStore } from "~~/stores/auth";
const authStore = useAuthStore();
const api = useUserApi();
@ -10,16 +10,16 @@
return;
}
navigateTo('/');
navigateTo("/");
}
const links = [
{
name: 'Home',
href: '/home',
name: "Home",
href: "/home",
},
{
name: 'Logout',
name: "Logout",
action: logout,
last: true,
},
@ -33,19 +33,19 @@
const dropdown = [
{
name: 'Item / Asset',
name: "Item / Asset",
action: () => {
modals.item = true;
},
},
{
name: 'Location',
name: "Location",
action: () => {
modals.location = true;
},
},
{
name: 'Label',
name: "Label",
action: () => {
modals.label = true;
},
@ -66,7 +66,7 @@
<div class="bg-neutral absolute shadow-xl top-0 h-[50vh] max-h-96 sm:h-[28vh] -z-10 w-full"></div>
<BaseContainer is="header" class="py-6 max-w-none">
<BaseContainer cmp="header" class="py-6 max-w-none">
<BaseContainer>
<h2 class="mt-1 text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl flex">
HomeB
@ -77,20 +77,22 @@
<template v-for="link in links">
<NuxtLink
v-if="!link.action"
:key="link.name"
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"
:key="link.name + 'link'"
for="location-form-modal"
class="hover:text-base-content transition-color duration-200 italic"
@click="link.action"
>
{{ link.name }}
</button>
<span v-if="!link.last"> / </span>
<span v-if="!link.last" :key="link.name"> / </span>
</template>
</div>
<div class="flex mt-6">
@ -102,7 +104,7 @@
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">
<li v-for="btn in dropdown" :key="btn.name">
<button @click="btn.action">
{{ btn.name }}
</button>

View File

@ -31,7 +31,7 @@
</template>
<script setup lang="ts">
import { useNotifications } from '@/composables/use-notifier';
import { useNotifications } from "@/composables/use-notifier";
const { notifications, dropNotification } = useNotifications();
</script>

View File

@ -1,11 +1,11 @@
<template>
<div class="divider">
<div class="btn-group min-w-[180px] flex-nowrap">
<button @click="$emit('edit')" name="options" class="btn btn-sm btn-primary">
<button name="options" class="btn btn-sm btn-primary" @click="$emit('edit')">
<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">
<button name="options" class="btn btn-sm btn-primary" @click="$emit('delete')">
<Icon name="heroicons-trash" class="h-5 w-5 mr-1" aria-hidden="true" />
<span> Delete </span>
</button>

View File

@ -1,10 +1,10 @@
<template>
<NuxtLink
v-if="to"
:to="to"
v-bind="attributes"
class="btn"
ref="submitBtn"
:to="to"
class="btn"
:class="{
loading: loading,
'btn-sm': size === 'sm',
@ -19,8 +19,8 @@
<button
v-else
v-bind="attributes"
class="btn"
ref="submitBtn"
class="btn"
:class="{
loading: loading,
'btn-sm': size === 'sm',
@ -35,7 +35,7 @@
</template>
<script setup lang="ts">
type Sizes = 'sm' | 'md' | 'lg';
type Sizes = "sm" | "md" | "lg";
const props = defineProps({
loading: {
@ -48,7 +48,7 @@
},
size: {
type: String as () => Sizes,
default: 'md',
default: "md",
},
to: {
type: String as () => string | null,
@ -67,13 +67,6 @@
};
});
const is = computed(() => {
if (props.to) {
return 'a';
}
return 'button';
});
const submitBtn = ref(null);
const isHover = useElementHover(submitBtn);
</script>

View File

@ -1,14 +1,14 @@
<script lang="ts" setup>
defineProps({
is: {
cmp: {
type: String,
default: 'div',
default: "div",
},
});
</script>
<template>
<component :is="is" class="container max-w-6xl mx-auto px-4">
<component :is="cmp" class="container max-w-6xl mx-auto px-4">
<slot />
</component>
</template>

View File

@ -10,7 +10,7 @@
</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">
<div v-for="(dValue, dKey) in details" :key="dKey" 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>

View File

@ -1,9 +1,9 @@
<template>
<div class="z-[999]">
<input type="checkbox" :id="modalId" class="modal-toggle" v-model="modal" />
<input :id="modalId" v-model="modal" type="checkbox" class="modal-toggle" />
<div class="modal modal-bottom sm:modal-middle overflow-visible">
<div class="modal-box overflow-visible relative">
<button @click="close" :for="modalId" class="btn btn-sm btn-circle absolute right-2 top-2"></button>
<button :for="modalId" class="btn btn-sm btn-circle absolute right-2 top-2" @click="close"></button>
<h3 class="font-bold text-lg">
<slot name="title"></slot>
@ -15,7 +15,7 @@
</template>
<script setup lang="ts">
const emit = defineEmits(['cancel', 'update:modelValue']);
const emit = defineEmits(["cancel", "update:modelValue"]);
const props = defineProps({
modelValue: {
type: Boolean,
@ -34,12 +34,12 @@
function close() {
if (props.readonly) {
emit('cancel');
emit("cancel");
return;
}
modal.value = false;
}
const modalId = useId();
const modal = useVModel(props, 'modelValue', emit);
const modal = useVModel(props, "modelValue", emit);
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="dropdown dropdown-end w-full" ref="label">
<FormTextField tabindex="0" label="Date" v-model="dateText" :inline="inline" readonly />
<div @blur="resetTime" tabindex="0" class="mt-1 card compact dropdown-content shadow bg-base-100 rounded-box w-64">
<div ref="label" class="dropdown dropdown-end w-full">
<FormTextField v-model="dateText" tabindex="0" label="Date" :inline="inline" readonly />
<div tabindex="0" class="mt-1 card compact dropdown-content shadow bg-base-100 rounded-box w-64" @blur="resetTime">
<div class="card-body">
<div class="flex justify-between items-center">
<button class="btn btn-xs" @click="prevMonth">
@ -13,7 +13,7 @@
</button>
</div>
<div class="grid grid-cols-7 gap-2">
<div v-for="d in daysIdx">
<div v-for="d in daysIdx" :key="d">
<p class="text-center">
{{ d }}
</p>
@ -21,12 +21,13 @@
<template v-for="day in days">
<button
v-if="day.number != ''"
:key="day.number"
class="text-center btn-xs btn btn-outline"
@click="select($event, day.date)"
>
{{ day.number }}
</button>
<div v-else></div>
<div v-else :key="`${day.number}-empty`"></div>
</template>
</div>
</div>
@ -35,7 +36,7 @@
</template>
<script setup lang="ts">
const emit = defineEmits(['update:modelValue', 'update:text']);
const emit = defineEmits(["update:modelValue", "update:text"]);
const props = defineProps({
modelValue: {
@ -49,12 +50,12 @@
},
});
const selected = useVModel(props, 'modelValue', emit);
const selected = useVModel(props, "modelValue", emit);
const dateText = computed(() => {
if (selected.value) {
return selected.value.toLocaleDateString();
}
return '';
return "";
});
const time = ref(new Date());
@ -68,7 +69,7 @@
});
const month = computed(() => {
return time.value.toLocaleString('default', { month: 'long' });
return time.value.toLocaleString("default", { month: "long" });
});
const year = computed(() => {
@ -86,14 +87,14 @@
}
const daysIdx = computed(() => {
return ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
return ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
});
function select(e: MouseEvent, day: Date) {
console.log(day);
selected.value = day;
console.log(selected.value);
// @ts-ignore
// @ts-ignore - this is a vue3 bug
e.target.blur();
resetTime();
}
@ -116,7 +117,7 @@
for (let i = 0; i < firstDay; i++) {
days.push({
number: '',
number: "",
date: new Date(),
});
}

View File

@ -1,11 +1,13 @@
<template>
<div class="form-control w-full" ref="menu">
<div ref="menu" class="form-control w-full">
<label class="label">
<span class="label-text">{{ label }}</span>
</label>
<div class="dropdown dropdown-top sm:dropdown-end">
<div tabindex="0" class="w-full min-h-[48px] flex gap-2 p-4 flex-wrap border border-gray-400 rounded-lg">
<span class="badge" v-for="itm in value"> {{ name != '' ? itm[name] : itm }} </span>
<span v-for="itm in value" :key="name != '' ? itm[name] : itm" class="badge">
{{ name != "" ? itm[name] : itm }}
</span>
</div>
<ul
tabindex="0"
@ -13,12 +15,13 @@
>
<li
v-for="(obj, idx) in items"
:key="idx"
:class="{
bordered: selectedIndexes[idx],
}"
>
<button type="button" @click="toggle(idx)">
{{ name != '' ? obj[name] : obj }}
{{ name != "" ? obj[name] : obj }}
</button>
</li>
</ul>
@ -27,11 +30,11 @@
</template>
<script lang="ts" setup>
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
label: {
type: String,
default: '',
default: "",
},
modelValue: {
type: Array as () => any[],
@ -43,7 +46,7 @@
},
name: {
type: String,
default: 'name',
default: "name",
},
selectFirst: {
type: Boolean,
@ -74,5 +77,5 @@
}
);
const value = useVModel(props, 'modelValue', emit);
const value = useVModel(props, "modelValue", emit);
</script>

View File

@ -3,10 +3,10 @@
<label class="label">
<span class="label-text">{{ label }}</span>
</label>
<select class="select select-bordered" v-model="value">
<select v-model="value" class="select select-bordered">
<option disabled selected>Pick one</option>
<option v-for="obj in items" :value="obj">
{{ name != '' ? obj[name] : obj }}
<option v-for="obj in items" :key="name != '' ? obj[name] : obj" :value="obj">
{{ name != "" ? obj[name] : obj }}
</option>
</select>
<!-- <label class="label">
@ -17,11 +17,11 @@
</template>
<script lang="ts" setup>
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
label: {
type: String,
default: '',
default: "",
},
modelValue: {
type: Object as any,
@ -33,7 +33,7 @@
},
name: {
type: String,
default: 'name',
default: "name",
},
selectFirst: {
type: Boolean,
@ -50,5 +50,5 @@
}
);
const value = useVModel(props, 'modelValue', emit);
const value = useVModel(props, "modelValue", emit);
</script>

View File

@ -1,9 +1,9 @@
<template>
<div class="form-control" v-if="!inline">
<div v-if="!inline" class="form-control">
<label class="label">
<span class="label-text">{{ label }}</span>
</label>
<textarea class="textarea textarea-bordered h-24" v-model="value" :placeholder="placeholder" />
<textarea v-model="value" class="textarea textarea-bordered h-24" :placeholder="placeholder" />
<label v-if="limit" class="label">
<span class="label-text-alt"></span>
<span class="label-text-alt"> {{ valueLen }}/{{ limit }}</span>
@ -13,12 +13,17 @@
<label class="label">
<span class="label-text">{{ label }}</span>
</label>
<textarea class="textarea textarea-bordered col-span-3 mt-3 h-24" auto-grow v-model="value" :placeholder="placeholder" />
<textarea
v-model="value"
class="textarea textarea-bordered col-span-3 mt-3 h-24"
auto-grow
:placeholder="placeholder"
/>
</div>
</template>
<script lang="ts" setup>
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
type: [String],
@ -30,7 +35,7 @@
},
type: {
type: String,
default: 'text',
default: "text",
},
limit: {
type: [Number, String],
@ -38,7 +43,7 @@
},
placeholder: {
type: String,
default: '',
default: "",
},
inline: {
type: Boolean,
@ -46,7 +51,7 @@
},
});
const value = useVModel(props, 'modelValue', emit);
const value = useVModel(props, "modelValue", emit);
const valueLen = computed(() => {
return value.value ? value.value.length : 0;
});

View File

@ -3,13 +3,13 @@
<label class="label">
<span class="label-text">{{ label }}</span>
</label>
<input ref="input" :type="type" v-model="value" class="input input-bordered w-full" />
<input ref="input" v-model="value" :type="type" class="input input-bordered w-full" />
</div>
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
<label class="label">
<span class="label-text">{{ label }}</span>
</label>
<input class="input input-bordered col-span-3 w-full mt-2" v-model="value" />
<input v-model="value" class="input input-bordered col-span-3 w-full mt-2" />
</div>
</template>
@ -17,7 +17,7 @@
const props = defineProps({
label: {
type: String,
default: '',
default: "",
},
modelValue: {
type: [String, Number],
@ -25,7 +25,7 @@
},
type: {
type: String,
default: 'text',
default: "text",
},
triggerFocus: {
type: Boolean,
@ -48,5 +48,5 @@
}
);
const value = useVModel(props, 'modelValue');
const value = useVModel(props, "modelValue");
</script>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { IconifyIcon } from '@iconify/vue';
import { Icon as Iconify, loadIcon } from '@iconify/vue';
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({
@ -14,12 +14,12 @@
const icon: Ref<IconifyIcon | null> = ref(null);
const component = computed(() => nuxtApp.vueApp.component(props.name));
icon.value = await loadIcon(props.name).catch(_ => null);
icon.value = await loadIcon(props.name).catch(() => null);
watch(
() => props.name,
async () => {
icon.value = await loadIcon(props.name).catch(_ => null);
icon.value = await loadIcon(props.name).catch(() => null);
}
);
</script>

View File

@ -10,14 +10,19 @@
</h2>
<p>{{ description }}</p>
<div class="flex gap-2 flex-wrap justify-end">
<LabelChip v-for="label in item.labels" :label="label" class="badge-primary group-hover:badge-secondary" />
<LabelChip
v-for="label in item.labels"
:key="label.id"
:label="label"
class="badge-primary group-hover:badge-secondary"
/>
</div>
</div>
</NuxtLink>
</template>
<script setup lang="ts">
import { Item } from '~~/lib/api/classes/items';
import { Item } from "~~/lib/api/classes/items";
const props = defineProps({
item: {

View File

@ -2,16 +2,16 @@
<BaseModal v-model="modal">
<template #title> Create Item </template>
<form @submit.prevent="create">
<FormSelect label="Location" v-model="form.location" :items="locations ?? []" select-first />
<FormSelect v-model="form.location" label="Location" :items="locations ?? []" select-first />
<FormTextField
:trigger-focus="focused"
ref="locationNameRef"
v-model="form.name"
:trigger-focus="focused"
:autofocus="true"
label="Item Name"
v-model="form.name"
/>
<FormTextField label="Item Description" v-model="form.description" />
<FormMultiselect label="Labels" v-model="form.labels" :items="labels ?? []" />
<FormTextField v-model="form.description" label="Item Description" />
<FormMultiselect v-model="form.labels" label="Labels" :items="labels ?? []" />
<div class="modal-action">
<BaseButton ref="submitBtn" type="submit" :loading="loading">
<template #icon>
@ -26,7 +26,7 @@
</template>
<script setup lang="ts">
import { type Location } from '~~/lib/api/classes/locations';
import { type Location } from "~~/lib/api/classes/locations";
const props = defineProps({
modelValue: {
type: Boolean,
@ -36,21 +36,21 @@
const submitBtn = ref(null);
const modal = useVModel(props, 'modelValue');
const modal = useVModel(props, "modelValue");
const loading = ref(false);
const focused = ref(false);
const form = reactive({
location: {} as Location,
name: '',
description: '',
color: '', // Future!
name: "",
description: "",
color: "", // Future!
labels: [],
});
function reset() {
form.name = '';
form.description = '';
form.color = '';
form.name = "";
form.description = "";
form.color = "";
focused.value = false;
modal.value = false;
loading.value = false;
@ -87,13 +87,13 @@
labelIds: form.labels.map(l => l.id) as string[],
};
const { data, error } = await api.items.create(out);
const { error } = await api.items.create(out);
if (error) {
toast.error("Couldn't create label");
return;
}
toast.success('Item created');
toast.success("Item created");
reset();
}
</script>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
export type sizes = 'sm' | 'md' | 'lg';
import { Label } from "~~/lib/api/classes/labels";
import { Label } from '~~/lib/api/classes/labels';
export type sizes = "sm" | "md" | "lg";
defineProps({
label: {
type: Object as () => Label,
@ -9,7 +9,7 @@
},
size: {
type: String as () => sizes,
default: 'md',
default: "md",
},
});
@ -22,12 +22,12 @@
<template>
<NuxtLink
ref="badge"
class="badge"
:class="{
'p-3': size !== 'sm',
'p-2 badge-sm': size === 'sm',
}"
ref="badge"
:to="`/label/${label.id}`"
>
<label class="swap swap-rotate" :class="isActive ? 'swap-active' : ''">

View File

@ -3,13 +3,13 @@
<template #title> Create Label </template>
<form @submit.prevent="create">
<FormTextField
:trigger-focus="focused"
ref="locationNameRef"
v-model="form.name"
:trigger-focus="focused"
:autofocus="true"
label="Label Name"
v-model="form.name"
/>
<FormTextField label="Label Description" v-model="form.description" />
<FormTextField v-model="form.description" label="Label Description" />
<div class="modal-action">
<BaseButton type="submit" :loading="loading"> Create </BaseButton>
</div>
@ -25,19 +25,19 @@
},
});
const modal = useVModel(props, 'modelValue');
const modal = useVModel(props, "modelValue");
const loading = ref(false);
const focused = ref(false);
const form = reactive({
name: '',
description: '',
color: '', // Future!
name: "",
description: "",
color: "", // Future!
});
function reset() {
form.name = '';
form.description = '';
form.color = '';
form.name = "";
form.description = "";
form.color = "";
focused.value = false;
modal.value = false;
loading.value = false;
@ -54,13 +54,13 @@
const toast = useNotifier();
async function create() {
const { data, error } = await api.labels.create(form);
const { error } = await api.labels.create(form);
if (error) {
toast.error("Couldn't create label");
return;
}
toast.success('Label created');
toast.success("Label created");
reset();
}
</script>

View File

@ -26,7 +26,7 @@
</template>
<script lang="ts" setup>
import { Location } from '~~/lib/api/classes/locations';
import { Location } from "~~/lib/api/classes/locations";
defineProps({
location: {

View File

@ -3,13 +3,13 @@
<template #title> Create Location </template>
<form @submit.prevent="create">
<FormTextField
:trigger-focus="focused"
ref="locationNameRef"
v-model="form.name"
:trigger-focus="focused"
:autofocus="true"
label="Location Name"
v-model="form.name"
/>
<FormTextField label="Location Description" v-model="form.description" />
<FormTextField v-model="form.description" label="Location Description" />
<div class="modal-action">
<BaseButton type="submit" :loading="loading"> Create </BaseButton>
</div>
@ -25,12 +25,12 @@
},
});
const modal = useVModel(props, 'modelValue');
const modal = useVModel(props, "modelValue");
const loading = ref(false);
const focused = ref(false);
const form = reactive({
name: '',
description: '',
name: "",
description: "",
});
whenever(
@ -41,8 +41,8 @@
);
function reset() {
form.name = '';
form.description = '';
form.name = "";
form.description = "";
focused.value = false;
modal.value = false;
loading.value = false;
@ -61,7 +61,7 @@
}
if (data) {
toast.success('Location created');
toast.success("Location created");
navigateTo(`/location/${data.id}`);
}

View File

@ -1,5 +1,5 @@
<template>
<BaseModal @cancel="cancel(false)" v-model="isRevealed" readonly>
<BaseModal v-model="isRevealed" readonly @cancel="cancel(false)">
<template #title> Confirm </template>
<div>
<p>{{ text }}</p>

View File

@ -1,21 +1,21 @@
import { PublicApi } from '~~/lib/api/public';
import { UserApi } from '~~/lib/api/user';
import { Requests } from '~~/lib/requests';
import { useAuthStore } from '~~/stores/auth';
import { PublicApi } from "~~/lib/api/public";
import { UserApi } from "~~/lib/api/user";
import { Requests } from "~~/lib/requests";
import { useAuthStore } from "~~/stores/auth";
function logger(r: Response) {
console.log(`${r.status} ${r.url} ${r.statusText}`);
}
export function usePublicApi(): PublicApi {
const requests = new Requests('', '', {});
const requests = new Requests("", "", {});
return new PublicApi(requests);
}
export function useUserApi(): UserApi {
const authStore = useAuthStore();
const requests = new Requests('', () => authStore.token, {});
const requests = new Requests("", () => authStore.token, {});
requests.addResponseInterceptor(logger);
requests.addResponseInterceptor(r => {
if (r.status === 401) {

View File

@ -1,13 +1,13 @@
import { UseConfirmDialogReturn } from '@vueuse/core';
import { Ref } from 'vue';
import { UseConfirmDialogReturn } from "@vueuse/core";
import { Ref } from "vue";
type Store = UseConfirmDialogReturn<any, Boolean, Boolean> & {
type Store = UseConfirmDialogReturn<any, boolean, boolean> & {
text: Ref<string>;
setup: boolean;
};
const store: Partial<Store> = {
text: ref('Are you sure you want to delete this item? '),
text: ref("Are you sure you want to delete this item? "),
setup: false,
};
@ -21,7 +21,7 @@ const store: Partial<Store> = {
export function useConfirm(): Store {
if (!store.setup) {
store.setup = true;
const { isRevealed, reveal, confirm, cancel } = useConfirmDialog<any, Boolean, Boolean>();
const { isRevealed, reveal, confirm, cancel } = useConfirmDialog<any, boolean, boolean>();
store.isRevealed = isRevealed;
store.reveal = reveal;
store.confirm = confirm;

View File

@ -1,31 +1,29 @@
function slugify(text: string) {
return text
.toString()
.toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, ''); // Trim - from end of text
}
function idGenerator(): string {
const id =
Math.random().toString(32).substring(2, 6) +
Math.random().toString(36).substring(2, 6);
return slugify(id);
}
/**
* useFormIds uses the provided label to generate a unique id for the
* form element. If no label is provided the id is generated using a
* random string.
*/
export function useFormIds(label: string): string {
const slug = label ? slugify(label) : idGenerator();
return `${slug}-${idGenerator()}`;
}
export function useId(): string {
return idGenerator();
}
function slugify(text: string) {
return text
.toString()
.toLowerCase()
.replace(/\s+/g, "-") // Replace spaces with -
.replace(/[^\w-]+/g, "") // Remove all non-word chars
.replace(/--+/g, "-") // Replace multiple - with single -
.replace(/^-+/, "") // Trim - from start of text
.replace(/-+$/, ""); // Trim - from end of text
}
function idGenerator(): string {
const id = Math.random().toString(32).substring(2, 6) + Math.random().toString(36).substring(2, 6);
return slugify(id);
}
/**
* useFormIds uses the provided label to generate a unique id for the
* form element. If no label is provided the id is generated using a
* random string.
*/
export function useFormIds(label: string): string {
const slug = label ? slugify(label) : idGenerator();
return `${slug}-${idGenerator()}`;
}
export function useId(): string {
return idGenerator();
}

View File

@ -1,57 +1,55 @@
import { useId } from './use-ids';
import { useId } from "./use-ids";
interface Notification {
id: string;
message: string;
type: 'success' | 'error' | 'info';
id: string;
message: string;
type: "success" | "error" | "info";
}
const notifications = ref<Notification[]>([]);
function addNotification(notification: Notification) {
notifications.value.unshift(notification);
notifications.value.unshift(notification);
if (notifications.value.length > 4) {
notifications.value.pop();
} else {
setTimeout(() => {
// Remove notification with ID
notifications.value = notifications.value.filter(
n => n.id !== notification.id
);
}, 5000);
}
if (notifications.value.length > 4) {
notifications.value.pop();
} else {
setTimeout(() => {
// Remove notification with ID
notifications.value = notifications.value.filter(n => n.id !== notification.id);
}, 5000);
}
}
export function useNotifications() {
return {
notifications,
dropNotification: (idx: number) => notifications.value.splice(idx, 1),
};
return {
notifications,
dropNotification: (idx: number) => notifications.value.splice(idx, 1),
};
}
export function useNotifier() {
return {
success: (message: string) => {
addNotification({
id: useId(),
message,
type: 'success',
});
},
error: (message: string) => {
addNotification({
id: useId(),
message,
type: 'error',
});
},
info: (message: string) => {
addNotification({
id: useId(),
message,
type: 'info',
});
},
};
return {
success: (message: string) => {
addNotification({
id: useId(),
message,
type: "success",
});
},
error: (message: string) => {
addNotification({
id: useId(),
message,
type: "error",
});
},
info: (message: string) => {
addNotification({
id: useId(),
message,
type: "info",
});
},
};
}

View File

@ -1,4 +1,4 @@
import { Ref } from 'vue';
import { Ref } from "vue";
export type LocationViewPreferences = {
showDetails: boolean;
@ -11,7 +11,7 @@ export type LocationViewPreferences = {
*/
export function useViewPreferences(): Ref<LocationViewPreferences> {
const results = useLocalStorage(
'homebox/preferences/location',
"homebox/preferences/location",
{
showDetails: true,
showEmpty: true,

View File

@ -1,5 +1,5 @@
export function truncate(str: string, length: number) {
return str.length > length ? str.substring(0, length) + '...' : str;
return str.length > length ? str.substring(0, length) + "..." : str;
}
export function capitalize(str: string) {

View File

@ -1,8 +1,8 @@
import { describe, test, expect } from 'vitest';
import { client, userClient } from './test-utils';
import { describe, test, expect } from "vitest";
import { client, userClient } from "./test-utils";
describe('[GET] /api/v1/status', () => {
test('server should respond', async () => {
describe("[GET] /api/v1/status", () => {
test("server should respond", async () => {
const api = client();
const { response, data } = await api.status();
expect(response.status).toBe(200);
@ -10,23 +10,23 @@ describe('[GET] /api/v1/status', () => {
});
});
describe('first time user workflow (register, login)', () => {
describe("first time user workflow (register, login)", () => {
const api = client();
const userData = {
groupName: 'test-group',
groupName: "test-group",
user: {
email: 'test-user@email.com',
name: 'test-user',
password: 'test-password',
email: "test-user@email.com",
name: "test-user",
password: "test-password",
},
};
test('user should be able to register', async () => {
test("user should be able to register", async () => {
const { response } = await api.register(userData);
expect(response.status).toBe(204);
});
test('user should be able to login', async () => {
test("user should be able to login", async () => {
const { response, data } = await api.login(userData.user.email, userData.user.password);
expect(response.status).toBe(200);
expect(data.token).toBeTruthy();

View File

@ -1,24 +1,24 @@
import { beforeAll, expect } from 'vitest';
import { Requests } from '../../requests';
import { overrideParts } from '../base/urls';
import { PublicApi } from '../public';
import * as config from '../../../test/config';
import { UserApi } from '../user';
import { beforeAll, expect } from "vitest";
import { Requests } from "../../requests";
import { overrideParts } from "../base/urls";
import { PublicApi } from "../public";
import * as config from "../../../test/config";
import { UserApi } from "../user";
export function client() {
overrideParts(config.BASE_URL, '/api/v1');
const requests = new Requests('');
overrideParts(config.BASE_URL, "/api/v1");
const requests = new Requests("");
return new PublicApi(requests);
}
export function userClient(token: string) {
overrideParts(config.BASE_URL, '/api/v1');
const requests = new Requests('', token);
overrideParts(config.BASE_URL, "/api/v1");
const requests = new Requests("", token);
return new UserApi(requests);
}
const cache = {
token: '',
token: "",
};
/*
@ -30,11 +30,11 @@ export async function sharedUserClient(): Promise<UserApi> {
return userClient(cache.token);
}
const testUser = {
groupName: 'test-group',
groupName: "test-group",
user: {
email: '__test__@__test__.com',
name: '__test__',
password: '__test__',
email: "__test__@__test__.com",
name: "__test__",
password: "__test__",
},
};

View File

@ -1,9 +1,9 @@
import { describe, expect, test } from 'vitest';
import { Label } from '../../classes/labels';
import { UserApi } from '../../user';
import { sharedUserClient } from '../test-utils';
import { describe, expect, test } from "vitest";
import { Label } from "../../classes/labels";
import { UserApi } from "../../user";
import { sharedUserClient } from "../test-utils";
describe('locations lifecycle (create, update, delete)', () => {
describe("locations lifecycle (create, update, delete)", () => {
let increment = 0;
/**
@ -14,7 +14,7 @@ describe('locations lifecycle (create, update, delete)', () => {
const { response, data } = await api.labels.create({
name: `__test__.label.name_${increment}`,
description: `__test__.label.description_${increment}`,
color: '',
color: "",
});
expect(response.status).toBe(201);
increment++;
@ -26,13 +26,13 @@ describe('locations lifecycle (create, update, delete)', () => {
return [data, cleanup];
}
test('user should be able to create a label', async () => {
test("user should be able to create a label", async () => {
const api = await sharedUserClient();
const labelData = {
name: 'test-label',
description: 'test-description',
color: '',
name: "test-label",
description: "test-description",
color: "",
};
const { response, data } = await api.labels.create(labelData);
@ -53,14 +53,14 @@ describe('locations lifecycle (create, update, delete)', () => {
expect(deleteResponse.status).toBe(204);
});
test('user should be able to update a label', async () => {
test("user should be able to update a label", async () => {
const api = await sharedUserClient();
const [label, cleanup] = await useLabel(api);
const labelData = {
name: 'test-label',
description: 'test-description',
color: '',
name: "test-label",
description: "test-description",
color: "",
};
const { response, data } = await api.labels.update(label.id, labelData);
@ -78,7 +78,7 @@ describe('locations lifecycle (create, update, delete)', () => {
await cleanup();
});
test('user should be able to delete a label', async () => {
test("user should be able to delete a label", async () => {
const api = await sharedUserClient();
const [label, _] = await useLabel(api);

View File

@ -1,9 +1,9 @@
import { describe, expect, test } from 'vitest';
import { Location } from '../../classes/locations';
import { UserApi } from '../../user';
import { sharedUserClient } from '../test-utils';
import { describe, expect, test } from "vitest";
import { Location } from "../../classes/locations";
import { UserApi } from "../../user";
import { sharedUserClient } from "../test-utils";
describe('locations lifecycle (create, update, delete)', () => {
describe("locations lifecycle (create, update, delete)", () => {
let increment = 0;
/**
@ -26,12 +26,12 @@ describe('locations lifecycle (create, update, delete)', () => {
return [data, cleanup];
}
test('user should be able to create a location', async () => {
test("user should be able to create a location", async () => {
const api = await sharedUserClient();
const locationData = {
name: 'test-location',
description: 'test-description',
name: "test-location",
description: "test-description",
};
const { response, data } = await api.locations.create(locationData);
@ -52,13 +52,13 @@ describe('locations lifecycle (create, update, delete)', () => {
expect(deleteResponse.status).toBe(204);
});
test('user should be able to update a location', async () => {
test("user should be able to update a location", async () => {
const api = await sharedUserClient();
const [location, cleanup] = await useLocation(api);
const updateData = {
name: 'test-location-updated',
description: 'test-description-updated',
name: "test-location-updated",
description: "test-description-updated",
};
const { response } = await api.locations.update(location.id, updateData);
@ -75,7 +75,7 @@ describe('locations lifecycle (create, update, delete)', () => {
await cleanup();
});
test('user should be able to delete a location', async () => {
test("user should be able to delete a location", async () => {
const api = await sharedUserClient();
const [location, _] = await useLocation(api);

View File

@ -1,4 +1,4 @@
import { Requests } from '../../requests';
import { Requests } from "../../requests";
// <
// TGetResult,
// TPostData,

View File

@ -1,24 +1,24 @@
import { describe, expect, it } from 'vitest';
import { route } from '.';
import { describe, expect, it } from "vitest";
import { route } from ".";
describe('UrlBuilder', () => {
it('basic query parameter', () => {
const result = route('/test', { a: 'b' });
expect(result).toBe('/api/v1/test?a=b');
describe("UrlBuilder", () => {
it("basic query parameter", () => {
const result = route("/test", { a: "b" });
expect(result).toBe("/api/v1/test?a=b");
});
it('multiple query parameters', () => {
const result = route('/test', { a: 'b', c: 'd' });
expect(result).toBe('/api/v1/test?a=b&c=d');
it("multiple query parameters", () => {
const result = route("/test", { a: "b", c: "d" });
expect(result).toBe("/api/v1/test?a=b&c=d");
});
it('no query parameters', () => {
const result = route('/test');
expect(result).toBe('/api/v1/test');
it("no query parameters", () => {
const result = route("/test");
expect(result).toBe("/api/v1/test");
});
it('list-like query parameters', () => {
const result = route('/test', { a: ['b', 'c'] });
expect(result).toBe('/api/v1/test?a=b&a=c');
it("list-like query parameters", () => {
const result = route("/test", { a: ["b", "c"] });
expect(result).toBe("/api/v1/test?a=b&a=c");
});
});

View File

@ -1,2 +1,2 @@
export { BaseAPI } from './base-api';
export { route } from './urls';
export { BaseAPI } from "./base-api";
export { route } from "./urls";

View File

@ -1,6 +1,6 @@
const parts = {
host: 'http://localhost.com',
prefix: '/api/v1',
host: "http://localhost.com",
prefix: "/api/v1",
};
export function overrideParts(host: string, prefix: string) {
@ -32,5 +32,5 @@ export function route(rest: string, params: Record<string, QueryValue> = {}): st
}
}
return url.toString().replace('http://localhost.com', '');
return url.toString().replace("http://localhost.com", "");
}

View File

@ -1,7 +1,7 @@
import { BaseAPI, route } from '../base';
import { Label } from './labels';
import { Location } from './locations';
import { Results } from './types';
import { BaseAPI, route } from "../base";
import { Label } from "./labels";
import { Location } from "./locations";
import { Results } from "./types";
export interface ItemCreate {
name: string;
@ -35,12 +35,12 @@ export interface Item {
}
export class ItemsApi extends BaseAPI {
async getAll() {
return this.http.get<Results<Item>>({ url: route('/items') });
getAll() {
return this.http.get<Results<Item>>({ url: route("/items") });
}
async create(item: ItemCreate) {
return this.http.post<ItemCreate, Item>({ url: route('/items'), body: item });
create(item: ItemCreate) {
return this.http.post<ItemCreate, Item>({ url: route("/items"), body: item });
}
async get(id: string) {
@ -58,18 +58,18 @@ export class ItemsApi extends BaseAPI {
return payload;
}
async delete(id: string) {
delete(id: string) {
return this.http.delete<void>({ url: route(`/items/${id}`) });
}
async update(id: string, item: ItemCreate) {
update(id: string, item: ItemCreate) {
return this.http.put<ItemCreate, Item>({ url: route(`/items/${id}`), body: item });
}
async import(file: File) {
import(file: File) {
const formData = new FormData();
formData.append('csv', file);
formData.append("csv", file);
return this.http.post<FormData, void>({ url: route('/items/import'), data: formData });
return this.http.post<FormData, void>({ url: route("/items/import"), data: formData });
}
}

View File

@ -1,6 +1,6 @@
import { BaseAPI, route } from '../base';
import { Item } from './items';
import { Details, OutType, Results } from './types';
import { BaseAPI, route } from "../base";
import { Item } from "./items";
import { Details, OutType, Results } from "./types";
export type LabelCreate = Details & {
color: string;
@ -15,23 +15,23 @@ export type Label = LabelCreate &
};
export class LabelsApi extends BaseAPI {
async getAll() {
return this.http.get<Results<Label>>({ url: route('/labels') });
getAll() {
return this.http.get<Results<Label>>({ url: route("/labels") });
}
async create(body: LabelCreate) {
return this.http.post<LabelCreate, Label>({ url: route('/labels'), body });
create(body: LabelCreate) {
return this.http.post<LabelCreate, Label>({ url: route("/labels"), body });
}
async get(id: string) {
get(id: string) {
return this.http.get<Label>({ url: route(`/labels/${id}`) });
}
async delete(id: string) {
delete(id: string) {
return this.http.delete<void>({ url: route(`/labels/${id}`) });
}
async update(id: string, body: LabelUpdate) {
update(id: string, body: LabelUpdate) {
return this.http.put<LabelUpdate, Label>({ url: route(`/labels/${id}`), body });
}
}

View File

@ -1,6 +1,6 @@
import { BaseAPI, route } from '../base';
import { Item } from './items';
import { Details, OutType, Results } from './types';
import { BaseAPI, route } from "../base";
import { Item } from "./items";
import { Details, OutType, Results } from "./types";
export type LocationCreate = Details;
@ -14,22 +14,23 @@ export type Location = LocationCreate &
export type LocationUpdate = LocationCreate;
export class LocationsApi extends BaseAPI {
async getAll() {
return this.http.get<Results<Location>>({ url: route('/locations') });
getAll() {
return this.http.get<Results<Location>>({ url: route("/locations") });
}
async create(body: LocationCreate) {
return this.http.post<LocationCreate, Location>({ url: route('/locations'), body });
create(body: LocationCreate) {
return this.http.post<LocationCreate, Location>({ url: route("/locations"), body });
}
async get(id: string) {
get(id: string) {
return this.http.get<Location>({ url: route(`/locations/${id}`) });
}
async delete(id: string) {
delete(id: string) {
return this.http.delete<void>({ url: route(`/locations/${id}`) });
}
async update(id: string, body: LocationUpdate) {
update(id: string, body: LocationUpdate) {
return this.http.put<LocationUpdate, Location>({ url: route(`/locations/${id}`), body });
}
}

View File

@ -1,4 +1,4 @@
import { BaseAPI, route } from './base';
import { BaseAPI, route } from "./base";
export type LoginResult = {
token: string;
@ -28,12 +28,12 @@ export type StatusResult = {
export class PublicApi extends BaseAPI {
public status() {
return this.http.get<StatusResult>({ url: route('/status') });
return this.http.get<StatusResult>({ url: route("/status") });
}
public login(username: string, password: string) {
return this.http.post<LoginPayload, LoginResult>({
url: route('/users/login'),
url: route("/users/login"),
body: {
username,
password,
@ -42,6 +42,6 @@ export class PublicApi extends BaseAPI {
}
public register(body: RegisterPayload) {
return this.http.post<RegisterPayload, LoginResult>({ url: route('/users/register'), body });
return this.http.post<RegisterPayload, LoginResult>({ url: route("/users/register"), body });
}
}

View File

@ -1,8 +1,8 @@
import { Requests } from '~~/lib/requests';
import { BaseAPI, route } from './base';
import { ItemsApi } from './classes/items';
import { LabelsApi } from './classes/labels';
import { LocationsApi } from './classes/locations';
import { BaseAPI, route } from "./base";
import { ItemsApi } from "./classes/items";
import { LabelsApi } from "./classes/labels";
import { LocationsApi } from "./classes/locations";
import { Requests } from "~~/lib/requests";
export type Result<T> = {
item: T;
@ -30,14 +30,14 @@ export class UserApi extends BaseAPI {
}
public self() {
return this.http.get<Result<User>>({ url: route('/users/self') });
return this.http.get<Result<User>>({ url: route("/users/self") });
}
public logout() {
return this.http.post<object, void>({ url: route('/users/logout') });
return this.http.post<object, void>({ url: route("/users/logout") });
}
public deleteAccount() {
return this.http.delete<void>({ url: route('/users/self') });
return this.http.delete<void>({ url: route("/users/self") });
}
}

View File

@ -1 +1 @@
export { Requests, type TResponse } from './requests';
export { Requests, type TResponse } from "./requests";

View File

@ -1,8 +1,8 @@
export enum Method {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
}
export type RequestInterceptor = (r: Response) => void;
@ -40,9 +40,9 @@ export class Requests {
return this.baseUrl + rest;
}
constructor(baseUrl: string, token: string | (() => string) = '', headers: Record<string, string> = {}) {
constructor(baseUrl: string, token: string | (() => string) = "", headers: Record<string, string> = {}) {
this.baseUrl = baseUrl;
this.token = typeof token === 'string' ? () => token : token;
this.token = typeof token === "string" ? () => token : token;
this.headers = headers;
}
@ -72,19 +72,19 @@ export class Requests {
headers: {
...rargs.headers,
...this.headers,
},
} as Record<string, string>,
};
const token = this.token();
if (token !== '' && payload.headers !== undefined) {
payload.headers['Authorization'] = token;
if (token !== "" && payload.headers !== undefined) {
payload.headers["Authorization"] = token; // eslint-disable-line dot-notation
}
if (this.methodSupportsBody(method)) {
if (rargs.data) {
payload.body = rargs.data;
} else {
payload.headers['Content-Type'] = 'application/json';
payload.headers["Content-Type"] = "application/json";
payload.body = JSON.stringify(rargs.body);
}
}

View File

@ -1,20 +1,20 @@
import { defineNuxtConfig } from 'nuxt';
import { defineNuxtConfig } from "nuxt";
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
target: 'static',
target: "static",
ssr: false,
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt', '@vueuse/nuxt'],
modules: ["@nuxtjs/tailwindcss", "@pinia/nuxt", "@vueuse/nuxt"],
meta: {
title: 'Homebox',
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.svg' }],
title: "Homebox",
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.svg" }],
},
outDir: '../backend/app/api/public',
vite: {
server: {
proxy: {
'/api': 'http://localhost:7745',
"/api": "http://localhost:7745",
},
},
plugins: [],
},
});

View File

@ -5,13 +5,25 @@
"dev": "nuxt dev",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore .",
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore . --fix",
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts",
"test:local": "TEST_SHUTDOWN_API_SERVER=false && vitest --run --config ./test/vitest.config.ts",
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
},
"devDependencies": {
"@nuxtjs/eslint-config-typescript": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"eslint": "^8.23.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.4.0",
"isomorphic-fetch": "^3.0.0",
"nuxt": "3.0.0-rc.8",
"prettier": "^2.7.1",
"typescript": "^4.8.3",
"vite-plugin-eslint": "^1.8.1",
"vitest": "^0.22.1"
},
"dependencies": {

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
useHead({
title: "404. Not Found",
});
definePageMeta({
layout: "404",
});
useHead({
title: "404. Not Found",
});
definePageMeta({
layout: "404",
});
</script>
<template>

View File

@ -1,24 +1,24 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
layout: "home",
});
useHead({
title: 'Homebox | Home',
title: "Homebox | Home",
});
const api = useUserApi();
const { data: locations } = useAsyncData('locations', async () => {
const { data: locations } = useAsyncData("locations", async () => {
const { data } = await api.locations.getAll();
return data.items;
});
const { data: labels } = useAsyncData('labels', async () => {
const { data: labels } = useAsyncData("labels", async () => {
const { data } = await api.labels.getAll();
return data.items;
});
const { data: items } = useAsyncData('items', async () => {
const { data: items } = useAsyncData("items", async () => {
const { data } = await api.items.getAll();
return data.items;
});
@ -29,15 +29,15 @@
const stats = [
{
label: 'Locations',
label: "Locations",
value: totalLocations,
},
{
label: 'Items',
label: "Items",
value: totalItems,
},
{
label: 'Labels',
label: "Labels",
value: totalLabels,
},
];
@ -55,7 +55,7 @@
function setFile(e: Event & { target: HTMLInputElement }) {
importCsv.value = e.target.files[0];
console.log('importCsv.value', importCsv.value);
console.log("importCsv.value", importCsv.value);
}
const toast = useNotifier();
@ -74,7 +74,7 @@
const { error } = await api.items.import(importCsv.value);
if (error) {
toast.error('Import failed. Please try again later.');
toast.error("Import failed. Please try again later.");
}
// Reset
@ -114,7 +114,7 @@
<section aria-labelledby="profile-overview-title" class="mt-8">
<div class="overflow-hidden rounded-lg bg-white shadow">
<h2 class="sr-only" id="profile-overview-title">Profile Overview</h2>
<h2 id="profile-overview-title" class="sr-only">Profile Overview</h2>
<div class="bg-white p-6">
<div class="sm:flex sm:items-center sm:justify-between">
<div class="sm:flex sm:space-x-5">
@ -138,7 +138,7 @@
>
<div v-for="stat in stats" :key="stat.label" class="px-6 py-5 text-center text-sm font-medium">
<span class="text-gray-900">{{ stat.value.value }}</span>
{{ ' ' }}
{{ " " }}
<span class="text-gray-600">{{ stat.label }}</span>
</div>
</div>
@ -148,7 +148,7 @@
<section>
<BaseSectionHeader class="mb-5"> Storage Locations </BaseSectionHeader>
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
<LocationCard v-for="location in locations" :location="location" />
<LocationCard v-for="location in locations" :key="location.id" :location="location" />
</div>
</section>
@ -157,7 +157,7 @@
Items
<template #description>
<div class="tooltip" data-tip="Import CSV File">
<button @click="openDialog" class="btn btn-primary btn-sm">
<button class="btn btn-primary btn-sm" @click="openDialog">
<Icon name="mdi-database" class="mr-2"></Icon>
Import
</button>
@ -165,14 +165,14 @@
</template>
</BaseSectionHeader>
<div class="grid sm:grid-cols-2 gap-4">
<ItemCard v-for="item in items" :item="item" />
<ItemCard v-for="item in items" :key="item.id" :item="item" />
</div>
</section>
<section>
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
<div class="flex gap-2 flex-wrap">
<LabelChip v-for="label in labels" size="lg" :label="label" />
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" />
</div>
</section>
</BaseContainer>

View File

@ -1,43 +1,43 @@
<script setup lang="ts">
import TextField from '@/components/Form/TextField.vue';
import { useNotifier } from '@/composables/use-notifier';
import { usePublicApi } from '@/composables/use-api';
import { useAuthStore } from '~~/stores/auth';
import TextField from "@/components/Form/TextField.vue";
import { useNotifier } from "@/composables/use-notifier";
import { usePublicApi } from "@/composables/use-api";
import { useAuthStore } from "~~/stores/auth";
useHead({
title: 'Homebox | Organize and Tag Your Stuff',
title: "Homebox | Organize and Tag Your Stuff",
});
definePageMeta({
layout: 'empty',
layout: "empty",
});
const authStore = useAuthStore();
if (!authStore.isTokenExpired) {
navigateTo('/home');
navigateTo("/home");
}
const registerFields = [
{
label: "What's your name?",
value: '',
value: "",
},
{
label: "What's your email?",
value: '',
value: "",
},
{
label: 'Name your group',
value: '',
label: "Name your group",
value: "",
},
{
label: 'Set your password',
value: '',
type: 'password',
label: "Set your password",
value: "",
type: "password",
},
{
label: 'Confirm your password',
value: '',
type: 'password',
label: "Confirm your password",
value: "",
type: "password",
},
];
@ -57,11 +57,11 @@
});
if (error) {
toast.error('Problem registering user');
toast.error("Problem registering user");
return;
}
toast.success('User registered');
toast.success("User registered");
loading.value = false;
loginFields[0].value = registerFields[1].value;
@ -70,13 +70,13 @@
const loginFields = [
{
label: 'Email',
value: '',
label: "Email",
value: "",
},
{
label: 'Password',
value: '',
type: 'password',
label: "Password",
value: "",
type: "password",
},
];
@ -88,16 +88,16 @@
const { data, error } = await api.login(loginFields[0].value, loginFields[1].value);
if (error) {
toast.error('Invalid email or password');
toast.error("Invalid email or password");
} else {
toast.success('Logged in successfully');
toast.success("Logged in successfully");
authStore.$patch({
token: data.token,
expires: data.expiresAt,
});
navigateTo('/home');
navigateTo("/home");
}
loading.value = false;
}
@ -161,9 +161,9 @@
</h2>
<TextField
v-for="field in registerFields"
:key="field.label"
v-model="field.value"
:label="field.label"
:key="field.label"
:type="field.type"
/>
<div class="card-actions justify-end">
@ -188,9 +188,9 @@
</h2>
<TextField
v-for="field in loginFields"
:key="field.label"
v-model="field.value"
:label="field.label"
:key="field.label"
:type="field.type"
/>
<div class="card-actions justify-end mt-2">
@ -204,10 +204,10 @@
</Transition>
<div class="text-center mt-6">
<button
@click="toggleLogin"
class="text-base-content text-lg hover:bg-primary hover:text-primary-content px-3 py-1 rounded-xl transition-colors duration-200"
@click="toggleLogin"
>
{{ registerForm ? 'Already a User? Login' : 'Not a User? Register' }}
{{ registerForm ? "Already a User? Login" : "Not a User? Register" }}
</button>
</div>
</div>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
layout: "home",
});
const route = useRoute();
@ -12,85 +12,85 @@
const { data: item } = useAsyncData(async () => {
const { data, error } = await api.items.get(itemId.value);
if (error) {
toast.error('Failed to load item');
navigateTo('/home');
toast.error("Failed to load item");
navigateTo("/home");
return;
}
return data;
});
type FormField = {
type: 'text' | 'textarea' | 'select' | 'date';
type: "text" | "textarea" | "select" | "date";
label: string;
ref: string;
};
const mainFields: FormField[] = [
{
type: 'text',
label: 'Name',
ref: 'name',
type: "text",
label: "Name",
ref: "name",
},
{
type: 'textarea',
label: 'Description',
ref: 'description',
type: "textarea",
label: "Description",
ref: "description",
},
{
type: 'text',
label: 'Serial Number',
ref: 'serialNumber',
type: "text",
label: "Serial Number",
ref: "serialNumber",
},
{
type: 'text',
label: 'Model Number',
ref: 'modelNumber',
type: "text",
label: "Model Number",
ref: "modelNumber",
},
{
type: 'text',
label: 'Manufacturer',
ref: 'manufacturer',
type: "text",
label: "Manufacturer",
ref: "manufacturer",
},
{
type: 'textarea',
label: 'Notes',
ref: 'notes',
type: "textarea",
label: "Notes",
ref: "notes",
},
];
const purchaseFields: FormField[] = [
{
type: 'text',
label: 'Purchased From',
ref: 'purchaseFrom',
type: "text",
label: "Purchased From",
ref: "purchaseFrom",
},
{
type: 'text',
label: 'Purchased Price',
ref: 'purchasePrice',
type: "text",
label: "Purchased Price",
ref: "purchasePrice",
},
{
type: 'date',
label: 'Purchased At',
ref: 'purchaseTime',
type: "date",
label: "Purchased At",
ref: "purchaseTime",
},
];
const soldFields = [
{
type: 'text',
label: 'Sold To',
ref: 'soldTo',
type: "text",
label: "Sold To",
ref: "soldTo",
},
{
type: 'text',
label: 'Sold Price',
ref: 'soldPrice',
type: "text",
label: "Sold Price",
ref: "soldPrice",
},
{
type: 'date',
label: 'Sold At',
ref: 'soldTime',
type: "date",
label: "Sold At",
ref: "soldTime",
},
];
</script>
@ -103,7 +103,7 @@
<h3 class="text-lg font-medium leading-6">Item Details</h3>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div class="sm:divide-y sm:divide-gray-300 grid grid-cols-1" v-for="field in mainFields">
<div v-for="field in mainFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />
@ -118,7 +118,7 @@
<h3 class="text-lg font-medium leading-6">Purchase Details</h3>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div class="sm:divide-y sm:divide-gray-300 grid grid-cols-1" v-for="field in purchaseFields">
<div v-for="field in purchaseFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />
@ -133,7 +133,7 @@
<h3 class="text-lg font-medium leading-6">Sold Details</h3>
</div>
<div class="border-t border-gray-300 sm:p-0">
<div class="sm:divide-y sm:divide-gray-300 grid grid-cols-1" v-for="field in soldFields">
<div v-for="field in soldFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
definePageMeta({
layout: 'home',
layout: "home",
});
const route = useRoute();
@ -13,8 +13,8 @@
const { data: item } = useAsyncData(async () => {
const { data, error } = await api.items.get(itemId.value);
if (error) {
toast.error('Failed to load item');
navigateTo('/home');
toast.error("Failed to load item");
navigateTo("/home");
return;
}
return data;
@ -22,12 +22,12 @@
const itemSummary = computed(() => {
return {
Description: item.value?.description || '',
'Serial Number': item.value?.serialNumber || '',
'Model Number': item.value?.modelNumber || '',
Manufacturer: item.value?.manufacturer || '',
Notes: item.value?.notes || '',
Attachments: '', // TODO: Attachments
Description: item.value?.description || "",
"Serial Number": item.value?.serialNumber || "",
"Model Number": item.value?.modelNumber || "",
Manufacturer: item.value?.manufacturer || "",
Notes: item.value?.notes || "",
Attachments: "", // TODO: Attachments
};
});
@ -42,12 +42,12 @@
const payload = {};
if (item.value.lifetimeWarranty) {
payload['Lifetime Warranty'] = 'Yes';
payload["Lifetime Warranty"] = "Yes";
} else {
payload['Warranty Expires'] = item.value?.warrantyExpires || '';
payload["Warranty Expires"] = item.value?.warrantyExpires || "";
}
payload['Warranty Details'] = item.value?.warrantyDetails || '';
payload["Warranty Details"] = item.value?.warrantyDetails || "";
return payload;
});
@ -61,9 +61,9 @@
const purchaseDetails = computed(() => {
return {
'Purchased From': item.value?.purchaseFrom || '',
'Purchased Price': item.value?.purchasePrice || '',
'Purchased At': item.value?.purchaseTime || '',
"Purchased From": item.value?.purchaseFrom || "",
"Purchased Price": item.value?.purchasePrice || "",
"Purchased At": item.value?.purchaseTime || "",
};
});
@ -77,16 +77,16 @@
const soldDetails = computed(() => {
return {
'Sold To': item.value?.soldTo || '',
'Sold Price': item.value?.soldPrice || '',
'Sold At': item.value?.soldTime || '',
"Sold To": item.value?.soldTo || "",
"Sold Price": item.value?.soldPrice || "",
"Sold At": item.value?.soldTime || "",
};
});
const confirm = useConfirm();
async function deleteItem() {
const confirmed = await confirm.reveal('Are you sure you want to delete this item?');
const confirmed = await confirm.reveal("Are you sure you want to delete this item?");
if (!confirmed.data) {
return;
@ -94,11 +94,11 @@
const { error } = await api.items.delete(itemId.value);
if (error) {
toast.error('Failed to delete item');
toast.error("Failed to delete item");
return;
}
toast.success('Item deleted');
navigateTo('/home');
toast.success("Item deleted");
navigateTo("/home");
}
</script>
@ -118,11 +118,11 @@
</span>
<template #after>
<div class="flex flex-wrap gap-3 mt-3">
<LabelChip class="badge-primary" v-for="label in item.labels" :label="label"></LabelChip>
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" />
</div>
<div class="modal-action">
<label class="label cursor-pointer mr-auto">
<input type="checkbox" v-model="preferences.showEmpty" class="toggle toggle-primary" />
<input v-model="preferences.showEmpty" type="checkbox" class="toggle toggle-primary" />
<span class="label-text ml-4"> Show Empty </span>
</label>
<BaseButton size="sm" :to="`/item/${itemId}/edit`">
@ -164,13 +164,13 @@
</ul>
</template>
</BaseDetails>
<BaseDetails :details="purchaseDetails" v-if="showPurchase">
<BaseDetails v-if="showPurchase" :details="purchaseDetails">
<template #title> Purchase Details </template>
</BaseDetails>
<BaseDetails :details="warrantyDetails" v-if="showWarranty">
<BaseDetails v-if="showWarranty" :details="warrantyDetails">
<template #title> Warranty </template>
</BaseDetails>
<BaseDetails :details="soldDetails" v-if="showSold">
<BaseDetails v-if="showSold" :details="soldDetails">
<template #title> Sold </template>
</BaseDetails>
</div>

View File

@ -1,6 +1,6 @@
<script setup>
definePageMeta({
layout: 'home',
layout: "home",
});
const show = reactive({
@ -11,83 +11,85 @@
});
const form = reactive({
name: '',
description: '',
notes: '',
name: "",
description: "",
notes: "",
// Item Identification
serialNumber: '',
modelNumber: '',
manufacturer: '',
serialNumber: "",
modelNumber: "",
manufacturer: "",
// Purchase Information
purchaseTime: '',
purchasePrice: '',
purchaseFrom: '',
purchaseTime: "",
purchasePrice: "",
purchaseFrom: "",
// Sold Information
soldTime: '',
soldPrice: '',
soldTo: '',
soldNotes: '',
soldTime: "",
soldPrice: "",
soldTo: "",
soldNotes: "",
});
function submit() {}
function submit() {
console.log("Submitted!");
}
</script>
<template>
<BaseContainer is="section">
<BaseContainer cmp="section">
<BaseSectionHeader> Add an Item To Your Inventory </BaseSectionHeader>
<form @submit.prevent="submit" class="max-w-3xl mx-auto my-5 space-y-6">
<form class="max-w-3xl mx-auto my-5 space-y-6" @submit.prevent="submit">
<div class="divider collapse-title px-0 cursor-pointer">Required Information</div>
<div class="bg-base-200 card">
<div class="card-body">
<FormTextField label="Name" v-model="form.name" />
<FormTextArea label="Description" v-model="form.description" limit="1000" />
<FormTextField v-model="form.name" label="Name" />
<FormTextArea v-model="form.description" label="Description" limit="1000" />
</div>
</div>
<div class="divider">
<button class="btn btn-sm" @click="show.identification = !show.identification">Product Information</button>
</div>
<div class="card bg-base-200" v-if="show.identification">
<div v-if="show.identification" class="card bg-base-200">
<div class="card-body grid md:grid-cols-2">
<FormTextField label="Serial Number" v-model="form.serialNumber" />
<FormTextField label="Model Number" v-model="form.modelNumber" />
<FormTextField label="Manufacturer" v-model="form.manufacturer" />
<FormTextField v-model="form.serialNumber" label="Serial Number" />
<FormTextField v-model="form.modelNumber" label="Model Number" />
<FormTextField v-model="form.manufacturer" label="Manufacturer" />
</div>
</div>
<div class="">
<button class="btn btn-sm" @click="show.purchase = !show.purchase">Purchase Information</button>
<div class="divider"></div>
</div>
<div class="card bg-base-200" v-if="show.purchase">
<div v-if="show.purchase" class="card bg-base-200">
<div class="card-body grid md:grid-cols-2">
<FormTextField label="Purchase Time" v-model="form.purchaseTime" />
<FormTextField label="Purchase Price" v-model="form.purchasePrice" />
<FormTextField label="Purchase From" v-model="form.purchaseFrom" />
<FormTextField v-model="form.purchaseTime" label="Purchase Time" />
<FormTextField v-model="form.purchasePrice" label="Purchase Price" />
<FormTextField v-model="form.purchaseFrom" label="Purchase From" />
</div>
</div>
<div class="divider">
<button class="btn btn-sm" @click="show.sold = !show.sold">Sold Information</button>
</div>
<div class="card bg-base-200" v-if="show.sold">
<div v-if="show.sold" class="card bg-base-200">
<div class="card-body">
<div class="grid md:grid-cols-2 gap-2">
<FormTextField label="Sold Time" v-model="form.soldTime" />
<FormTextField label="Sold Price" v-model="form.soldPrice" />
<FormTextField label="Sold To" v-model="form.soldTo" />
<FormTextField v-model="form.soldTime" label="Sold Time" />
<FormTextField v-model="form.soldPrice" label="Sold Price" />
<FormTextField v-model="form.soldTo" label="Sold To" />
</div>
<FormTextArea label="Sold Notes" v-model="form.soldNotes" limit="1000" />
<FormTextArea v-model="form.soldNotes" label="Sold Notes" limit="1000" />
</div>
</div>
<div class="divider">
<button class="btn btn-sm" @click="show.extras = !show.extras">Extras</button>
</div>
<div class="card bg-base-200" v-if="show.extras">
<div v-if="show.extras" class="card bg-base-200">
<div class="card-body">
<FormTextArea label="Notes" v-model="form.notes" limit="1000" />
<FormTextArea v-model="form.notes" label="Notes" limit="1000" />
</div>
</div>
</form>

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import ActionsDivider from '../../components/Base/ActionsDivider.vue';
import ActionsDivider from "../../components/Base/ActionsDivider.vue";
definePageMeta({
layout: 'home',
layout: "home",
});
const route = useRoute();
@ -16,8 +16,8 @@
const { data: label } = useAsyncData(labelId.value, async () => {
const { data, error } = await api.labels.get(labelId.value);
if (error) {
toast.error('Failed to load label');
navigateTo('/home');
toast.error("Failed to load label");
navigateTo("/home");
return;
}
return data;
@ -25,25 +25,25 @@
function maybeTimeAgo(date?: string): string {
if (!date) {
return '??';
return "??";
}
const time = new Date(date);
return `${useTimeAgo(time).value} (${useDateFormat(time, 'MM-DD-YYYY').value})`;
return `${useTimeAgo(time).value} (${useDateFormat(time, "MM-DD-YYYY").value})`;
}
const details = computed(() => {
const dt = {
Name: label.value?.name || '',
Description: label.value?.description || '',
Name: label.value?.name || "",
Description: label.value?.description || "",
};
if (preferences.value.showDetails) {
dt['Created At'] = maybeTimeAgo(label.value?.createdAt);
dt['Updated At'] = maybeTimeAgo(label.value?.updatedAt);
dt['Database ID'] = label.value?.id || '';
dt['Group Id'] = label.value?.groupId || '';
dt["Created At"] = maybeTimeAgo(label.value?.createdAt);
dt["Updated At"] = maybeTimeAgo(label.value?.updatedAt);
dt["Database ID"] = label.value?.id || "";
dt["Group Id"] = label.value?.groupId || "";
}
return dt;
@ -52,7 +52,7 @@
const { reveal } = useConfirm();
async function confirmDelete() {
const { isCanceled } = await reveal('Are you sure you want to delete this label? This action cannot be undone.');
const { isCanceled } = await reveal("Are you sure you want to delete this label? This action cannot be undone.");
if (isCanceled) {
return;
@ -61,24 +61,24 @@
const { error } = await api.labels.delete(labelId.value);
if (error) {
toast.error('Failed to delete label');
toast.error("Failed to delete label");
return;
}
toast.success('Label deleted');
navigateTo('/home');
toast.success("Label deleted");
navigateTo("/home");
}
const updateModal = ref(false);
const updating = ref(false);
const updateData = reactive({
name: '',
description: '',
color: '',
name: "",
description: "",
color: "",
});
function openUpdate() {
updateData.name = label.value?.name || '';
updateData.description = label.value?.description || '';
updateData.name = label.value?.name || "";
updateData.description = label.value?.description || "";
updateModal.value = true;
}
@ -87,11 +87,11 @@
const { error, data } = await api.labels.update(labelId.value, updateData);
if (error) {
toast.error('Failed to update label');
toast.error("Failed to update label");
return;
}
toast.success('Label updated');
toast.success("Label updated");
label.value = data;
updateModal.value = false;
updating.value = false;
@ -103,8 +103,8 @@
<BaseModal v-model="updateModal">
<template #title> Update Label </template>
<form v-if="label" @submit.prevent="update">
<FormTextField :autofocus="true" label="Label Name" v-model="updateData.name" />
<FormTextField label="Label Description" v-model="updateData.description" />
<FormTextField v-model="updateData.name" :autofocus="true" label="Label Name" />
<FormTextField v-model="updateData.description" label="Label Description" />
<div class="modal-action">
<BaseButton type="submit" :loading="updating"> Update </BaseButton>
</div>
@ -112,14 +112,14 @@
</BaseModal>
<section>
<BaseSectionHeader class="mb-5" dark>
{{ label ? label.name : '' }}
{{ label ? label.name : "" }}
</BaseSectionHeader>
<BaseDetails class="mb-2" :details="details">
<template #title> Label Details </template>
</BaseDetails>
<div class="form-control ml-auto mr-2 max-w-[130px]">
<label class="label cursor-pointer">
<input type="checkbox" v-model.checked="preferences.showDetails" class="checkbox" />
<input v-model="preferences.showDetails" type="checkbox" class="toggle" />
<span class="label-text"> Detailed View </span>
</label>
</div>
@ -129,7 +129,7 @@
<section v-if="label">
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
<div class="grid gap-2 grid-cols-2">
<ItemCard v-for="item in label.items" :item="item" :key="item.id" />
<ItemCard v-for="item in label.items" :key="item.id" :item="item" />
</div>
</section>
</BaseContainer>

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import ActionsDivider from '../../components/Base/ActionsDivider.vue';
import ActionsDivider from "../../components/Base/ActionsDivider.vue";
definePageMeta({
layout: 'home',
layout: "home",
});
const route = useRoute();
@ -16,8 +16,8 @@
const { data: location } = useAsyncData(locationId.value, async () => {
const { data, error } = await api.locations.get(locationId.value);
if (error) {
toast.error('Failed to load location');
navigateTo('/home');
toast.error("Failed to load location");
navigateTo("/home");
return;
}
return data;
@ -25,25 +25,25 @@
function maybeTimeAgo(date?: string): string {
if (!date) {
return '??';
return "??";
}
const time = new Date(date);
return `${useTimeAgo(time).value} (${useDateFormat(time, 'MM-DD-YYYY').value})`;
return `${useTimeAgo(time).value} (${useDateFormat(time, "MM-DD-YYYY").value})`;
}
const details = computed(() => {
const dt = {
Name: location.value?.name || '',
Description: location.value?.description || '',
Name: location.value?.name || "",
Description: location.value?.description || "",
};
if (preferences.value.showDetails) {
dt['Created At'] = maybeTimeAgo(location.value?.createdAt);
dt['Updated At'] = maybeTimeAgo(location.value?.updatedAt);
dt['Database ID'] = location.value?.id || '';
dt['Group Id'] = location.value?.groupId || '';
dt["Created At"] = maybeTimeAgo(location.value?.createdAt);
dt["Updated At"] = maybeTimeAgo(location.value?.updatedAt);
dt["Database ID"] = location.value?.id || "";
dt["Group Id"] = location.value?.groupId || "";
}
return dt;
@ -52,7 +52,7 @@
const { reveal } = useConfirm();
async function confirmDelete() {
const { isCanceled } = await reveal('Are you sure you want to delete this location? This action cannot be undone.');
const { isCanceled } = await reveal("Are you sure you want to delete this location? This action cannot be undone.");
if (isCanceled) {
return;
@ -61,23 +61,23 @@
const { error } = await api.locations.delete(locationId.value);
if (error) {
toast.error('Failed to delete location');
toast.error("Failed to delete location");
return;
}
toast.success('Location deleted');
navigateTo('/home');
toast.success("Location deleted");
navigateTo("/home");
}
const updateModal = ref(false);
const updating = ref(false);
const updateData = reactive({
name: '',
description: '',
name: "",
description: "",
});
function openUpdate() {
updateData.name = location.value?.name || '';
updateData.description = location.value?.description || '';
updateData.name = location.value?.name || "";
updateData.description = location.value?.description || "";
updateModal.value = true;
}
@ -86,11 +86,11 @@
const { error, data } = await api.locations.update(locationId.value, updateData);
if (error) {
toast.error('Failed to update location');
toast.error("Failed to update location");
return;
}
toast.success('Location updated');
toast.success("Location updated");
location.value = data;
updateModal.value = false;
updating.value = false;
@ -102,8 +102,8 @@
<BaseModal v-model="updateModal">
<template #title> Update Location </template>
<form v-if="location" @submit.prevent="update">
<FormTextField :autofocus="true" label="Location Name" v-model="updateData.name" />
<FormTextField label="Location Description" v-model="updateData.description" />
<FormTextField v-model="updateData.name" :autofocus="true" label="Location Name" />
<FormTextField v-model="updateData.description" label="Location Description" />
<div class="modal-action">
<BaseButton type="submit" :loading="updating"> Update </BaseButton>
</div>
@ -111,14 +111,14 @@
</BaseModal>
<section>
<BaseSectionHeader class="mb-5" dark>
{{ location ? location.name : '' }}
{{ location ? location.name : "" }}
</BaseSectionHeader>
<BaseDetails class="mb-2" :details="details">
<template #title> Location Details </template>
</BaseDetails>
<div class="form-control ml-auto mr-2 max-w-[130px]">
<label class="label cursor-pointer">
<input type="checkbox" v-model.checked="preferences.showDetails" class="checkbox" />
<input v-model="preferences.showDetails" type="checkbox" class="toggle" />
<span class="label-text"> Detailed View </span>
</label>
</div>
@ -128,7 +128,7 @@
<section v-if="location">
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
<div class="grid gap-2 grid-cols-2">
<ItemCard v-for="item in location.items" :item="item" :key="item.id" />
<ItemCard v-for="item in location.items" :key="item.id" :item="item" />
</div>
</section>
</BaseContainer>

File diff suppressed because it is too large Load Diff

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@ -1,11 +1,11 @@
import { UserApi } from '~~/lib/api/user';
import { defineStore } from 'pinia';
import { useLocalStorage } from '@vueuse/core';
import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core";
import { UserApi } from "~~/lib/api/user";
export const useAuthStore = defineStore('auth', {
export const useAuthStore = defineStore("auth", {
state: () => ({
token: useLocalStorage('pinia/auth/token', ''),
expires: useLocalStorage('pinia/auth/expires', ''),
token: useLocalStorage("pinia/auth/token", ""),
expires: useLocalStorage("pinia/auth/expires", ""),
}),
getters: {
isTokenExpired: state => {
@ -13,7 +13,7 @@ export const useAuthStore = defineStore('auth', {
return true;
}
if (typeof state.expires === 'string') {
if (typeof state.expires === "string") {
return new Date(state.expires) < new Date();
}
@ -28,8 +28,8 @@ export const useAuthStore = defineStore('auth', {
return result;
}
this.token = '';
this.expires = '';
this.token = "";
this.expires = "";
return result;
},
@ -38,9 +38,9 @@ export const useAuthStore = defineStore('auth', {
* must clear it's local session, usually when a 401 is received.
*/
clearSession() {
this.token = '';
this.expires = '';
navigateTo('/');
this.token = "";
this.expires = "";
navigateTo("/");
},
},
});

View File

@ -1,15 +1,11 @@
module.exports = {
content: ['./app.vue', './{components,pages,layouts}/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: 'class', // or 'media' or 'class'
content: ["./app.vue", "./{components,pages,layouts}/**/*.{vue,js,ts,jsx,tsx}"],
darkMode: "class", // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/typography'),
require('daisyui'),
],
plugins: [require("@tailwindcss/aspect-ratio"), require("@tailwindcss/typography"), require("daisyui")],
};

View File

@ -1,4 +1,3 @@
export const PORT = "7745";
export const HOST = "http://127.0.0.1";
export const BASE_URL = HOST + ":" + PORT;

View File

@ -1,8 +1,8 @@
import { exec } from 'child_process';
import * as config from './config';
import { exec } from "child_process";
import * as config from "./config";
export const setup = () => {
console.log('Starting Client Tests');
console.log("Starting Client Tests");
console.log({
PORT: config.PORT,
HOST: config.HOST,
@ -12,8 +12,8 @@ export const setup = () => {
export const teardown = () => {
if (process.env.TEST_SHUTDOWN_API_SERVER) {
const pc = exec('pkill -SIGTERM api'); // Kill background API process
pc.stdout.on('data', data => {
const pc = exec("pkill -SIGTERM api"); // Kill background API process
pc.stdout.on("data", data => {
console.log(`stdout: ${data}`);
});
}